Structural overhaul

Split the tipbot in modules:
- main modules to contain base functions by theme
- coin specs, to define a coin's specifics
- optional modules, defining commands/behaviors
This commit is contained in:
moneromooo 2014-12-29 17:08:13 +00:00
parent 7cfc14faf6
commit 6b6a1a67e7
17 changed files with 1460 additions and 1127 deletions

14
README
View File

@ -9,13 +9,15 @@ Installation requirements:
The daemon needs a running redis, daemon and simplewallet. Set the connection parameters
for these in tipbot.py.
Before starting, read the configuration parameters at the top of tipbot.py and change
Before starting, read the configuration parameters in tipbot/config.py and change
as appropriate.
Start the bot with the coin name as parameter (eg, python tipbot.py monero). Coin specs
are defined in a file called tipbot_<coin-name>.py. If you want to add a coin that the
tipbot does not support yet, simply copy an existing spec module and adapt to that coin's
particular specs.
Start the bot with the coin name as parameter to -c (eg, python tipbot.py -c monero).
Coin specs are defined in a file called tipbot/coinspecs.py. If you want to add a coin
that the tipbot does not support yet, simply copy and adapt an existing spec.
Modules are loaded with -m (eg, python tipbot.py -m payment). Available modules are
in the tipbot/modules directory.
The tipbot will need a wallet. Any wallet can do, but it is recommended to use a separate
wallet. This wallet should be loaded in the simplewallet the tipbot connects to.
@ -24,7 +26,7 @@ A file called tipbot-password.txt shall be created where the tpibot runs, contai
Freenode account password for the tipbot. This is so the tipbot can identify, to avoid
others passing off for the tipbot.
Tipbot commands are prefix with "!". Try !help to get a list of available commands.
Tipbot commands are prefix with "!". Try !commands to get a list of available commands.
The withdrawal fee is currently set to the default network fee. For coins with per kB fees,
if a withdraw transaction happens to be larger than 1 kB, more will be charged by

1183
tipbot.py

File diff suppressed because it is too large Load Diff

1
tipbot/__init__.py Normal file
View File

@ -0,0 +1 @@
__all__ = [ 'config', 'coins', 'utils', 'log', 'irc', 'commands' ]

41
tipbot/coinspecs.py Normal file
View File

@ -0,0 +1,41 @@
#!/bin/python
#
# Cryptonote tipbot - coins specifications
# Copyright 2014 moneromooo
#
# The Cryptonote tipbot is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License as published
# by the Free Software Foundation; either version 2, or (at your option)
# any later version.
#
coinspecs = {
"monero": {
"name": "Monero",
"atomic_units": 1e12,
"denominations": [[1000000, 1, "piconero"], [1000000000, 1e6, "micronero"], [1000000000000, 1e9, "millinero"]],
"address_length": [95, 95], # min/max size of addresses
"address_prefix": ['4', '9'], # allowed prefixes of addresses
"min_withdrawal_fee": 10000000000,
"web_wallet_url": "https://mymonero.com/", # None is there's none
},
"ducknote": {
"name": "Darknote",
"atomic_units": 1e8,
"denominations": [],
"address_length": [95, 98], # min/max size of addresses
"address_prefix": ['dd'], # allowed prefixes of addresses
"min_withdrawal_fee": 1000000,
"web_wallet_url": None,
},
"dashcoin": {
"name": "Dashcoin",
"atomic_units": 1e8,
"denominations": [],
"address_length": [96], # min/max size of addresses
"address_prefix": ['D'], # allowed prefixes of addresses
"min_withdrawal_fee": 1000000,
"web_wallet_url": None,
}
}

96
tipbot/command_manager.py Normal file
View File

@ -0,0 +1,96 @@
#!/bin/python
#
# Cryptonote tipbot - commands
# Copyright 2014 moneromooo
#
# The Cryptonote tipbot is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License as published
# by the Free Software Foundation; either version 2, or (at your option)
# any later version.
#
import tipbot.config as config
from tipbot.irc import *
commands = dict()
calltable=dict()
idles = []
def RunRegisteredCommand(nick,chan,ifyes,yesdata,ifno,nodata):
if nick not in calltable:
calltable[nick] = []
calltable[nick].append([chan,ifyes,yesdata,ifno,nodata])
if nick in registered_users:
RunNextCommand(nick,True)
else:
SendTo('nickserv', "ACC " + nick)
def IsAdmin(nick):
return nick in config.admins
def RunAdminCommand(nick,chan,ifyes,yesdata,ifno,nodata):
if not IsAdmin(nick):
log_warn('RunAdminCommand: nick %s is not admin, cannot call %s with %s' % (str(nick),str(ifyes),str(yesdata)))
SendTo(nick, "Access denied")
return
RunRegisteredCommand(nick,chan,ifyes,yesdata,ifno,nodata)
def RunNextCommand(nick,registered):
if registered:
registered_users.add(nick)
else:
registered_users.discard(nick)
if nick not in calltable:
log_error( 'Nothing in queue for %s' % nick)
return
try:
if registered:
calltable[nick][0][1](nick,calltable[nick][0][0],calltable[nick][0][2])
else:
calltable[nick][0][3](nick,calltable[nick][0][0],calltable[nick][0][4])
del calltable[nick][0]
except Exception, e:
log_error('RunNextCommand: Exception in action, continuing: %s' % str(e))
del calltable[nick][0]
def Commands(nick,chan,cmd):
if IsAdmin(nick):
all = True
else:
all = False
SendTo(nick, "Commands for %s:" % config.tipbot_name)
for command_name in commands:
c = commands[command_name]
if 'admin' in c and c['admin'] and not all:
continue
synopsis = c['name']
if 'parms' in c:
synopsis = synopsis + " " + c['parms']
SendTo(nick, "%s - %s" % (synopsis, c['help']))
def RegisterCommand(command):
commands[command['name']] = command
def RegisterIdleFunction(function):
idles.append(function)
def OnCommand(cmd,chan,who,check_admin,check_registered):
if cmd[0] in commands:
c = commands[cmd[0]]
if 'admin' in c and c['admin']:
check_admin(GetNick(who),chan,c['function'],cmd,SendTo,"You must be admin")
elif 'registered' in c and c['registered']:
check_registered(GetNick(who),chan,c['function'],cmd,SendTo,"You must be registered with Freenode")
else:
c['function'](GetNick(who),chan,cmd)
else:
SendTo(GetNick(who), "Invalid command, try !help")
def RunIdleFunctions(param):
for f in idles:
try:
f(param)
except Exception,e:
log_error("Exception running idle function %s: %s" % (str(f),str(e)))

35
tipbot/config.py Normal file
View File

@ -0,0 +1,35 @@
#!/bin/python
#
# Cryptonote tipbot - configuration
# Copyright 2014 moneromooo
# Inspired by "Simple Python IRC bot" by berend
#
# The Cryptonote tipbot is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License as published
# by the Free Software Foundation; either version 2, or (at your option)
# any later version.
#
tipbot_name = "monero-testnet-tipbot"
irc_network = 'irc.freenode.net'
irc_port = 6667
irc_send_delay = 0.4
redis_host="127.0.0.1"
redis_port=7777
daemon_host = 'testfull.monero.cc' # '127.0.0.1'
daemon_port = 28081 # 6060
wallet_host = '127.0.0.1'
wallet_port = 6061
wallet_update_time = 30 # seconds
withdrawal_fee=None # None defaults to the network default fee
min_withdraw_amount = None # None defaults to the withdrawal fee
disable_withdraw_on_error = True
admins = ["moneromooo", "moneromoo"]
# list of nicks to ignore for rains - bots, trolls, etc
no_rain_to_nicks = []

399
tipbot/irc.py Normal file
View File

@ -0,0 +1,399 @@
#!/bin/python
#
# Cryptonote tipbot - IRC routines
# Copyright 2014 moneromooo
# Inspired by "Simple Python IRC bot" by berend
#
# The Cryptonote tipbot is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License as published
# by the Free Software Foundation; either version 2, or (at your option)
# any later version.
#
import sys
import socket
import select
import time
import string
from tipbot.log import log_error, log_warn, log_info, log_log, log_IRCSEND, log_IRCRECV
irc_line_delay = 0
irc = None
irc_password = ""
irc_welcome_line = 'Welcome to the freenode Internet Relay Chat Network'
irc_homechan = '#txtptest000'
irc_timeout_seconds = 600
last_ping_time = time.time()
irc_network = None
irc_port = None
irc_name = None
userstable=dict()
registered_users=set()
def SendIRC(msg):
log_IRCSEND(msg)
irc.send(msg + '\r\n')
time.sleep(irc_line_delay)
def connect_to_irc(network,port,name,password,delay):
global irc
global irc_line_delay
global irc_network
global irc_port
global irc_line_delay
global irc_password
irc_network=network
irc_port=port
irc_name=name
irc_line_delay = delay
irc_password=password
log_info('Connecting to IRC at %s:%u' % (network, port))
try:
irc = socket.socket ( socket.AF_INET, socket.SOCK_STREAM )
irc.connect ( ( network, port ) )
except Exception, e:
log_error( 'Error initializing IRC: %s' % str(e))
exit()
log_IRCRECV(irc.recv ( 4096 ))
SendIRC ( 'PASS *********')
SendIRC ( 'NICK %s' % name)
SendIRC ( 'USER %s %s %s :%s' % (name, name, name, name))
return irc
def reconnect_to_irc():
connect_to_irc(irc_network,irc_port,irc_name,irc_password,irc_line_delay)
def Send(msg):
SendIRC ('PRIVMSG ' + irc_homechan + ' : ' + msg)
def SendTo(where,msg):
SendIRC ('PRIVMSG ' + where + ' : ' + msg)
def Join(chan):
SendIRC ( 'JOIN ' + chan)
def Part(chan):
SendIRC ( 'PART ' + chan)
def Who(chan):
userstable[chan] = dict()
SendIRC ( 'WHO ' + chan)
def GetHost(host): # Return Host
host = host.split('@')[1]
host = host.split(' ')[0]
return host
def GetChannel(data): # Return Channel
channel = data.split('#')[1]
channel = channel.split(':')[0]
channel = '#' + channel
channel = channel.strip(' \t\n\r')
return channel
def GetNick(data): # Return Nickname
nick = data.split('!')[0]
nick = nick.replace(':', ' ')
nick = nick.replace(' ', '')
nick = nick.strip(' \t\n\r')
return nick
def GetSendTo(nick,chan):
if chan[0] == '#':
return chan
return nick
def UpdateLastActiveTime(chan,nick):
if not chan in userstable:
log_error("UpdateLastActiveTime: %s spoke in %s, but %s not found in users table" % (nick, chan, chan))
userstable[chan] = dict()
if not nick in userstable[chan]:
log_error("UpdateLastActiveTime: %s spoke in %s, but was not found in that channel's users table" % (nick, chan))
userstable[chan][nick] = None
userstable[chan][nick] = time.time()
def GetTimeSinceActive(chan,nick):
if not chan in userstable:
log_error("GetTimeSinceActive: channel %s not found in users table" % chan)
return None
if not nick in userstable[chan]:
log_error("GetTimeSinceActive: %s not found in channel %s's users table" % (nick, chan))
return None
t = userstable[chan][nick]
if t == None:
return None
dt = time.time() - t
if dt < 0:
log_error("GetTimeSinceActive: %s active in %s in the future" % (nick, chan))
return None
return dt
def GetActiveNicks(chan,seconds):
nicks = []
if not chan in userstable:
return []
now = time.time()
for nick in userstable[chan]:
t = userstable[chan][nick]
if t == None:
continue
dt = now - t
if dt < 0:
log_error("GetActiveNicks: %s active in %s in the future" % (nick, chan))
continue
if dt < seconds:
nicks.append(nick)
return nicks
def GetUsersTable():
return userstable
#def Op(to_op, chan):
# SendIRC( 'MODE ' + chan + ' +o: ' + to_op)
#
#def DeOp(to_deop, chan):
# SendIRC( 'MODE ' + chan + ' -o: ' + to_deop)
#
#def Voice(to_v, chan):
# SendIRC( 'MODE ' + chan + ' +v: ' + to_v)
#
#def DeVoice(to_dv, chan):
# SendIRC( 'MODE ' + chan + ' -v: ' + to_dv)
buffered_data = ""
def GetIRCLine(s):
global buffered_data
idx = buffered_data.find("\n")
if idx == -1:
try:
(r,w,x)=select.select([s.fileno()],[],[],1)
if s.fileno() in r:
newdata=s.recv(4096,socket.MSG_DONTWAIT)
else:
newdata = None
if s.fileno() in x:
log_error('getline: IRC socket in exception set')
newdata = None
except Exception,e:
log_error('getline: Exception: %s' % str(e))
# Broken pipe when we get kicked for spam
if str(e).find("Broken pipe") != -1:
raise
newdata = None
if newdata == None:
return None
buffered_data+=newdata
idx = buffered_data.find("\n")
if idx == -1:
ret = buffered_data
buffered_data = ""
return ret
ret = buffered_data[0:idx+1]
buffered_data = buffered_data[idx+1:]
return ret
def IRCLoop(on_idle,on_identified,on_command):
global userstable
global registered_users
while True:
action = None
try:
data = GetIRCLine(irc)
except Exception,e:
log_warn('Exception fron GetIRCLine, we were probably disconnected, reconnecting in 5 seconds')
time.sleep(5)
last_ping_time = time.time()
reconnect_to_irc(irc_network,irc_port)
continue
# All that must be done even when nothing from IRC - data may be None here
on_idle()
if data == None:
if time.time() - last_ping_time > irc_timeout_seconds:
log_warn('%s seconds without PING, reconnecting in 5 seconds' % irc_timeout_seconds)
time.sleep(5)
last_ping_time = time.time()
reconnect_to_irc(irc_network,irc_port)
continue
data = data.strip("\r\n")
log_IRCRECV(data)
# consider any IRC data as a ping
last_ping_time = time.time()
if data.find ( irc_welcome_line ) != -1:
userstable = dict()
registered_users.clear()
SendTo("nickserv", "IDENTIFY %s" % irc_password)
Join(irc_homechan)
#ScanWho(None,[irc_homechan])
if data.find ( 'PING' ) == 0:
log_log('Got PING, replying PONG')
last_ping_time = time.time()
SendIRC ( 'PONG ' + data.split() [ 1 ])
continue
if data.find('ERROR :Closing Link:') == 0:
log_warn('We were kicked from IRC, reconnecting in 5 seconds')
time.sleep(5)
last_ping_time = time.time()
reconnect_to_irc(irc_network,irc_port)
continue
#--------------------------- Action check --------------------------------#
if data.find(':') == -1:
continue
try:
cparts = data.split(':')
if len(cparts) < 2:
continue
if len(cparts) >= 3:
text = cparts[2]
else:
text = ""
parts = cparts[1].split(' ')
who = parts[0]
action = parts[1]
chan = parts[2]
except Exception, e:
log_error('main parser: Exception, continuing: %s' % str(e))
continue
if action == None:
continue
#print 'text: ', text
#print 'who: ', who
#print 'action: ', action
#print 'chan: ', chan
# if data.find('#') != -1:
# action = data.split('#')[0]
# action = action.split(' ')[1]
# if data.find('NICK') != -1:
# if data.find('#') == -1:
# action = 'NICK'
#----------------------------- Actions -----------------------------------#
try:
if action == 'NOTICE':
if who == "NickServ!NickServ@services.":
#if text.find('Information on ') != -1:
# ns_nick = text.split(' ')[2].strip("\002")
# print 'NickServ says %s is registered' % ns_nick
# PerformNextAction(ns_nick, True)
#elif text.find(' is not registered') != -1:
# ns_nick = text.split(' ')[0].strip("\002")
# print 'NickServ says %s is not registered' % ns_nick
# PerformNextAction(ns_nick, False)
if text.find(' ACC ') != -1:
stext = text.split(' ')
ns_nick = stext[0]
ns_acc = stext[1]
ns_status = stext[2]
if ns_acc == "ACC":
if ns_status == "3":
log_info('NickServ says %s is identified' % ns_nick)
on_identified(ns_nick, True)
else:
log_info('NickServ says %s is not identified' % ns_nick)
on_identified(ns_nick, False)
else:
log_error('ACC line not as expected...')
elif action == '352':
try:
who_chan = parts[3]
who_chan_user = parts[7]
if not who_chan_user in userstable[who_chan]:
userstable[who_chan][who_chan_user] = None
log_log("New list of users in %s: %s" % (who_chan, str(userstable[who_chan].keys())))
except Exception,e:
log_error('Failed to parse "who" line: %s: %s' % (data, str(e)))
elif action == '353':
try:
who_chan = parts[4]
who_chan_users = cparts[2].split(" ")
for who_chan_user in who_chan_users:
if not who_chan_user in userstable[who_chan]:
if who_chan_user[0] == "@":
who_chan_user = who_chan_user[1:]
userstable[who_chan][who_chan_user] = None
log_log("New list of users in %s: %s" % (who_chan, str(userstable[who_chan].keys())))
except Exception,e:
log_error('Failed to parse "who" line: %s: %s' % (data, str(e)))
elif action == 'PRIVMSG':
UpdateLastActiveTime(chan,GetNick(who))
exidx = text.find('!')
if exidx != -1 and len(text)>exidx+1 and text[exidx+1] in string.ascii_letters:
cmd = text.split('!')[1]
cmd = cmd.split(' ')
cmd[0] = cmd[0].strip(' \t\n\r')
log_log('Found command: "%s" in channel "%s"' % (str(cmd), str(chan)))
#if cmd[0] == 'join':
# Join('#' + cmd[1])
#elif cmd[0] == 'part':
# Part('#' + cmd[1])
on_command(cmd,chan,who)
elif action == 'JOIN':
nick = GetNick(who)
log_info('%s joined the channel' % nick)
if not chan in userstable:
userstable[chan] = dict()
if nick in userstable[chan]:
log_warn('%s joined, but already in %s' % (nick, chan))
else:
userstable[chan][nick] = None
log_log("New list of users in %s: %s" % (chan, str(userstable[chan].keys())))
elif action == 'PART':
nick = GetNick(who)
log_info('%s left the channel' % nick)
if not nick in userstable[chan]:
log_warn('%s left, but was not in %s' % (nick, chan))
else:
del userstable[chan][nick]
log_log("New list of users in %s: %s" % (chan, str(userstable[chan].keys())))
elif action == 'QUIT':
nick = GetNick(who)
log_info('%s quit' % nick)
removed_list = ""
for chan in userstable:
log_log("Checking in %s" % chan)
if nick in userstable[chan]:
removed_list = removed_list + " " + chan
del userstable[chan][nick]
log_log("New list of users in %s: %s" % (chan, str(userstable[chan].keys())))
elif action == 'NICK':
nick = GetNick(who)
new_nick = text
log_info('%s renamed to %s' % (nick, new_nick))
for c in userstable:
log_log('checking %s' % c)
if nick in userstable[c]:
del userstable[c][nick]
if new_nick in userstable[c]:
log_warn('%s is the new name of %s, but was already in %s' % (new_nick, nick, c))
else:
userstable[c][new_nick] = None
log_log("New list of users in %s: %s" % (c, str(userstable[c].keys())))
except Exception,e:
log_error('Exception in top level action processing: %s' % str(e))

35
tipbot/log.py Normal file
View File

@ -0,0 +1,35 @@
#!/bin/python
#
# Cryptonote tipbot - logging
# Copyright 2014 moneromooo
#
# The Cryptonote tipbot is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License as published
# by the Free Software Foundation; either version 2, or (at your option)
# any later version.
#
import time
def log(stype,msg):
header = "%s\t%s\t" % (time.ctime(time.time()),stype)
print "%s%s" % (header, str(msg).replace("\n","\n"+header))
def log_error(msg):
log("ERROR",msg)
def log_warn(msg):
log("WARNING",msg)
def log_info(msg):
log("INFO",msg)
def log_log(msg):
log("LOG",msg)
def log_IRCRECV(msg):
log("IRCRECV",msg)
def log_IRCSEND(msg):
log("IRCSEND",msg)

View File

@ -0,0 +1 @@
__all__ = ['tipping', 'withdraw', 'payment']

92
tipbot/modules/payment.py Normal file
View File

@ -0,0 +1,92 @@
#!/bin/python
#
# Cryptonote tipbot - payment
# Copyright 2014 moneromooo
# Inspired by "Simple Python IRC bot" by berend
#
# The Cryptonote tipbot is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License as published
# by the Free Software Foundation; either version 2, or (at your option)
# any later version.
#
import redis
import time
import tipbot.config as config
from tipbot.log import log_error, log_warn, log_info, log_log
from tipbot.utils import *
from tipbot.redisdb import *
last_wallet_update_time = None
def UpdateCoin(param):
irc = param[0]
redisdb = param[1]
global last_wallet_update_time
if last_wallet_update_time == None:
last_wallet_update_time = 0
t=time.time()
dt = t - last_wallet_update_time
if dt < config.wallet_update_time:
return
try:
try:
scan_block_height = redis_get("scan_block_height")
scan_block_height = long(scan_block_height)
except Exception,e:
log_error('Failed to get scan_block_height: %s' % str(e))
last_wallet_update_time = time.time()
return
full_payment_ids = redis_hgetall("paymentid")
#print 'Got full payment ids: %s' % str(full_payment_ids)
payment_ids = []
for pid in full_payment_ids:
payment_ids.append(pid)
#print 'Got payment ids: %s' % str(payment_ids)
params = {
"payment_ids": payment_ids,
"min_block_height": scan_block_height
}
j = SendWalletJSONRPCCommand("get_bulk_payments",params)
#print 'Got j: %s' % str(j)
if "result" in j:
result = j["result"]
if "payments" in result:
payments = result["payments"]
log_info('UpdateCoin: Got %d payments' % len(payments))
for p in payments:
log_log('UpdateCoin: Looking at payment %s' % str(p))
bh = p["block_height"]
if bh > scan_block_height:
scan_block_height = bh
log_log('UpdateCoin: seen payments up to block %d' % scan_block_height)
try:
pipe = redis_pipeline()
pipe.set("scan_block_height", scan_block_height)
log_log('UpdateCoin: processing payments')
for p in payments:
payment_id=p["payment_id"]
tx_hash=p["tx_hash"]
amount=p["amount"]
try:
recipient = GetNickFromPaymendID(payment_id)
log_info('UpdateCoin: Found payment %s to %s for %s' % (tx_hash,recipient, AmountToString(amount)))
pipe.hincrby("balances",recipient,amount);
except Exception,e:
log_error('UpdateCoin: No nick found for payment id %s, tx hash %s, amount %s' % (payment_id, tx_hash, amount))
log_log('UpdateCoin: Executing received payments pipeline')
pipe.execute()
except Exception,e:
log_error('UpdateCoin: failed to set scan_block_height: %s' % str(e))
else:
log_log('UpdateCoin: No payments in get_bulk_payments reply')
else:
log_error('UpdateCoin: No results in get_bulk_payments reply')
except Exception,e:
log_error('UpdateCoin: Failed to get bulk payments: %s' % str(e))
last_wallet_update_time = time.time()
RegisterIdleFunction(UpdateCoin)

269
tipbot/modules/tipping.py Normal file
View File

@ -0,0 +1,269 @@
#!/bin/python
#
# Cryptonote tipbot - tipping commands
# Copyright 2014 moneromooo
#
# The Cryptonote tipbot is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License as published
# by the Free Software Foundation; either version 2, or (at your option)
# any later version.
#
import sys
import socket
import select
import random
import redis
import hashlib
import json
import httplib
import time
import string
import tipbot.config as config
from tipbot.log import log_error, log_warn, log_info, log_log
import tipbot.coinspecs as coinspecs
from tipbot.utils import *
from tipbot.irc import *
from tipbot.command_manager import *
from tipbot.redisdb import *
def Tip(nick,chan,cmd):
sendto=GetSendTo(nick,chan)
try:
who=cmd[1]
amount=float(cmd[2])
except Exception,e:
SendTo(sendto, "Usage: tip nick amount")
return
units=long(amount*coinspecs.atomic_units)
if units <= 0:
SendTo(sendto, "Invalid amount")
return
log_info("Tip: %s wants to tip %s %s" % (nick, who, AmountToString(units)))
try:
balance = redis_hget("balances",nick)
if balance == None:
balance = 0
balance=long(balance)
if units > balance:
SendTo(sendto, "You only have %s" % (AmountToString(balance)))
return
log_info('Tip: %s tipping %s %u units, with balance %u' % (nick, who, units, balance))
try:
p = redis_pipeline()
p.hincrby("balances",nick,-units);
p.hincrby("balances",who,units)
p.execute()
SendTo(sendto,"%s has tipped %s %s" % (nick, who, AmountToString(units)))
except Exception, e:
SendTo(sendto, "An error occured")
return
except Exception, e:
log_error('Tip: exception: %s' % str(e))
SendTo(sendto, "An error has occured")
def Rain(nick,chan,cmd):
userstable = GetUsersTable()
if chan[0] != '#':
SendTo(nick, "Raining can only be done in a channel")
return
try:
amount=float(cmd[1])
except Exception,e:
SendTo(chan, "Usage: rain amount [users]")
return
users = GetParam(cmd,2)
if users:
try:
users=long(users)
except Exception,e:
SendTo(chan, "Usage: rain amount [users]")
return
if amount <= 0:
SendTo(chan, "Usage: rain amount [users]")
return
if users != None and users <= 0:
SendTo(chan, "Usage: rain amount [users]")
return
units = long(amount * coinspecs.atomic_units)
try:
balance = redis_hget("balances",nick)
if balance == None:
balance = 0
balance=long(balance)
if units > balance:
SendTo(chan, "You only have %s" % (AmountToString(balance)))
return
log_log("userstable: %s" % str(userstable))
userlist = userstable[chan].keys()
userlist.remove(nick)
for n in config.no_rain_to_nicks:
userlist.remove(n)
if users == None or users > len(userlist):
users = len(userlist)
everyone = True
else:
everyone = False
if users == 0:
SendTo(chan, "Nobody eligible for rain")
return
if units < users:
SendTo(chan, "This would mean not even an atomic unit per nick")
return
log_info("%s wants to rain %s on %s users in %s" % (nick, AmountToString(units), users, chan))
log_log("users in %s: %s" % (chan, str(userlist)))
random.shuffle(userlist)
userlist = userlist[0:users]
log_log("selected users in %s: %s" % (chan, userlist))
user_units = long(units / users)
if everyone:
msg = "%s rained %s on everyone in the channel" % (nick, AmountToString(user_units))
else:
msg = "%s rained %s on:" % (nick, AmountToString(user_units))
pipe = redis_pipeline()
pipe.hincrby("balances",nick,-units)
for user in userlist:
pipe.hincrby("balances",user,user_units)
if not everyone:
msg = msg + " " + user
pipe.execute()
SendTo(chan, "%s" % msg)
except Exception,e:
log_error('Rain: exception: %s' % str(e))
SendTo(chan, "An error has occured")
return
def RainActive(nick,chan,cmd):
userstable = GetUsersTable()
amount=GetParam(cmd,1)
hours=GetParam(cmd,2)
minfrac=GetParam(cmd,3)
if chan[0] != '#':
SendTo(nick, "Raining can only be done in a channel")
return
try:
amount=float(amount)
if amount <= 0:
raise RuntimeError("")
except Exception,e:
SendTo(chan, "Invalid amount")
return
try:
hours=float(hours)
if hours <= 0:
raise RuntimeError("")
except Exception,e:
SendTo(chan, "Invalid hours")
return
if minfrac:
try:
minfrac=float(minfrac)
if minfrac < 0 or minfrac > 1:
raise RuntimeError("")
except Exception,e:
SendTo(chan, "minfrac must be a number between 0 and 1")
return
else:
minfrac = 0
units = long(amount * coinspecs.atomic_units)
try:
balance = redis_hget("balances",nick)
if balance == None:
balance = 0
balance=long(balance)
if units > balance:
SendTo(chan, "You only have %s" % (AmountToString(balance)))
return
now = time.time()
userlist = userstable[chan].keys()
userlist.remove(nick)
for n in config.no_rain_to_nicks:
userlist.remove(n)
weights=dict()
weight=0
for n in userlist:
t = userstable[chan][n]
if t == None:
continue
dt = now - t
if dt <= hours * 3600:
w = (1 * (hours * 3600 - dt) + minfrac * (1 - (hours * 3600 - dt))) / (hours * 3600)
weights[n] = w
weight += w
if len(weights) == 0:
SendTo(chan, "Nobody eligible for rain")
return
# if units < users:
# SendTo(chan, "This would mean not even an atomic unit per nick")
# return
pipe = redis_pipeline()
pipe.hincrby("balances",nick,-units)
rained_units = 0
nnicks = 0
minu=None
maxu=None
for n in weights:
user_units = long(units * weights[n] / weight)
if user_units <= 0:
continue
log_info("%s rained %s on %s (last active %f hours ago)" % (nick, AmountToString(user_units),n,GetTimeSinceActive(chan,n)/3600))
pipe.hincrby("balances",n,user_units)
rained_units += user_units
if not minu or user_units < minu:
minu = user_units
if not maxu or user_units > maxu:
maxu = user_units
nnicks = nnicks+1
if maxu == None:
SendTo(chan, "This would mean not even an atomic unit per nick")
return
pipe.execute()
log_info("%s rained %s - %s (total %s, acc %s) on the %d nicks active in the last %f hours" % (nick, AmountToString(minu), AmountToString(maxu), AmountToString(units), AmountToString(rained_units), nnicks, hours))
SendTo(chan, "%s rained %s - %s on the %d nicks active in the last %f hours" % (nick, AmountToString(minu), AmountToString(maxu), nnicks, hours))
except Exception,e:
log_error('Rain: exception: %s' % str(e))
SendTo(chan, "An error has occured")
return
RegisterCommand({
'name': 'tip',
'parms': '<nick> <amount>',
'function': Tip,
'registered': True,
'help': "tip another user"
})
RegisterCommand({
'name': 'rain',
'parms': '<amount> [<users>]',
'function': Rain,
'registered': True,
'help': "rain some coins on everyone (or just a few)"
})
RegisterCommand({
'name': 'rainactive',
'parms': '<amount> [<hours>]',
'function': RainActive,
'registered': True,
'help': "rain some coins on whoever was active recently"
})

167
tipbot/modules/withdraw.py Normal file
View File

@ -0,0 +1,167 @@
#!/bin/python
#
# Cryptonote tipbot - withdrawal commands
# Copyright 2014 moneromooo
#
# The Cryptonote tipbot is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License as published
# by the Free Software Foundation; either version 2, or (at your option)
# any later version.
#
import redis
import json
import string
from tipbot.log import log_error, log_warn, log_info, log_log
import tipbot.coinspecs as coinspecs
from tipbot.utils import *
from tipbot.irc import *
from tipbot.redisdb import *
from tipbot.command_manager import *
withdraw_disabled = False
def DisableWithdraw(nick,chan,cmd):
global withdraw_disabled
if nick:
log_warn('DisableWithdraw: disabled by %s' % nick)
else:
log_warn('DisableWithdraw: disabled')
withdraw_disabled = True
def EnableWithdraw(nick,chan,cmd):
global withdraw_disabled
log_info('EnableWithdraw: enabled by %s' % nick)
withdraw_disabled = False
def CheckDisableWithdraw():
if config.disable_withdraw_on_error:
DisableWithdraw(None,None,None)
def IsValidAddress(address):
if len(address) < coinspecs.address_length[0] or len(address) > coinspecs.address_length[1]:
return False
for prefix in coinspecs.address_prefix:
if address.startswith(prefix):
return True
return False
def Withdraw(nick,chan,cmd):
local_withdraw_fee = config.withdrawal_fee or coinspecs.min_withdrawal_fee
local_min_withdraw_amount = config.min_withdraw_amount or local_withdraw_fee
if local_min_withdraw_amount <= 0 or local_withdraw_fee <= 0 or local_min_withdraw_amount < local_withdraw_fee:
log_error('Withdraw: Inconsistent withdrawal settings')
SendTo(nick, "An error has occured")
return
try:
address=cmd[1]
except Exception,e:
SendTo(sendto, "Usage: withdraw address [amount]")
return
if not IsValidAddress(address):
SendTo(nick, "Invalid address")
return
amount = GetParam(cmd,2)
if amount:
try:
famount=float(amount)
if (famount < 0):
raise RuntimeError("")
amount = long(famount * coinspecs.atomic_units)
amount += local_withdraw_fee
except Exception,e:
SendTo(nick, "Invalid amount")
return
log_info("Withdraw: %s wants to withdraw %s to %s" % (nick, AmountToString(amount) if amount else "all", address))
if withdraw_disabled:
log_error('Withdraw: disabled')
SendTo(nick, "Sorry, withdrawal is disabled due to a wallet error which requires admin assistance")
return
try:
balance = redis_hget("balances",nick)
if balance == None:
balance = 0
balance=long(balance)
except Exception, e:
log_error('Withdraw: exception: %s' % str(e))
SendTo(nick, "An error has occured")
return
if amount:
if amount > balance:
log_info("Withdraw: %s trying to withdraw %s, but only has %s" % (nick,AmountToString(amount),AmountToString(balance)))
SendTo(nick, "You only have %s" % AmountToString(balance))
return
else:
amount = balance
if amount <= 0 or amount < local_min_withdraw_amount:
log_info("Withdraw: Minimum withdrawal balance: %s, %s cannot withdraw %s" % (AmountToString(config.min_withdraw_amount),nick,AmountToString(amount)))
SendTo(nick, "Minimum withdrawal balance: %s, cannot withdraw %s" % (AmountToString(config.min_withdraw_amount),AmountToString(amount)))
return
try:
fee = long(local_withdraw_fee)
topay = long(amount - fee)
log_info('Withdraw: Raw: fee: %s, to pay: %s' % (str(fee), str(topay)))
log_info('Withdraw: fee: %s, to pay: %s' % (AmountToString(fee), AmountToString(topay)))
params = {
'destinations': [{'address': address, 'amount': topay}],
'payment_id': GetPaymentID(nick),
'fee': fee,
'mixin': 0,
'unlock_time': 0,
}
j = SendWalletJSONRPCCommand("transfer",params)
except Exception,e:
log_error('Withdraw: Error in transfer: %s' % str(e))
CheckDisableWithdraw()
SendTo(nick,"An error has occured")
return
if not "result" in j:
log_error('Withdraw: No result in transfer reply')
CheckDisableWithdraw()
SendTo(nick,"An error has occured")
return
result = j["result"]
if not "tx_hash" in result:
log_error('Withdraw: No tx_hash in transfer reply')
CheckDisableWithdraw()
SendTo(nick,"An error has occured")
return
tx_hash = result["tx_hash"]
log_info('%s has withdrawn %s, tx hash %s' % (nick, amount, str(tx_hash)))
SendTo(nick, "Tx sent: %s" % tx_hash)
try:
redis_hincrby("balances",nick,-amount)
except Exception, e:
log_error('Withdraw: FAILED TO SUBTRACT BALANCE: exception: %s' % str(e))
CheckDisableWithdraw()
RegisterCommand({
'name': 'withdraw',
'parms': '<address> [<amount>]',
'function': Withdraw,
'registered': True,
'help': "withdraw part or all of your balance"
})
RegisterCommand({
'name': 'enable_withdraw',
'function': EnableWithdraw,
'admin': True,
'help': "Enable withdrawals"
})
RegisterCommand({
'name': 'disable_withdraw',
'function': DisableWithdraw,
'admin': True,
'help': "Disable withdrawals"
})

49
tipbot/redisdb.py Normal file
View File

@ -0,0 +1,49 @@
#!/bin/python
#
# Cryptonote tipbot
# Copyright 2014 moneromooo
# Inspired by "Simple Python IRC bot" by berend
#
# The Cryptonote tipbot is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License as published
# by the Free Software Foundation; either version 2, or (at your option)
# any later version.
#
import redis
from tipbot.log import log_error, log_warn, log_info, log_log
redisdb = None
def connect_to_redis(host,port):
log_info('Connecting to Redis at %s:%u' % (host, port))
try:
global redisdb
redisdb = redis.Redis(host=host,port=port)
return redisdb
except Exception, e:
log_error( 'Error initializing redis: %s' % str(e))
exit()
def redis_pipeline():
return redisdb.pipeline()
def redis_get(k):
return redisdb.get(k)
def redis_set(k,v):
return redisdb.set(k,v)
def redis_hget(t,k):
return redisdb.hget(t,k)
def redis_hgetall(t):
return redisdb.hgetall(t)
def redis_hset(t,k,v):
return redisdb.hset(t,k,v)
def redis_hincrby(t,k,v):
return redisdb.hincrby(t,k,v)

151
tipbot/utils.py Normal file
View File

@ -0,0 +1,151 @@
#!/bin/python
#
# Cryptonote tipbot - utility functions
# Copyright 2014 moneromooo
#
# The Cryptonote tipbot is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License as published
# by the Free Software Foundation; either version 2, or (at your option)
# any later version.
#
import redis
import hashlib
import json
import httplib
import tipbot.config as config
import tipbot.coinspecs as coinspecs
from tipbot.log import log_error, log_warn, log_info, log_log
from tipbot.irc import *
from tipbot.redisdb import *
from tipbot.command_manager import *
def GetPassword():
try:
f = open('tipbot-password.txt', 'r')
for p in f:
p = p.strip("\r\n")
f.close()
return p
except Exception,e:
log_error('could not fetch password: %s' % str(e))
raise
return "xxx"
def IsParamPresent(parms,idx):
return len(parms) > idx
def GetParam(parms,idx):
if IsParamPresent(parms,idx):
return parms[idx]
return None
def GetPaymentID(nick):
salt="2u3g55bkwrui32fi3g4bGR$j5g4ugnujb-"+coinspecs.name+"-";
p = hashlib.sha256(salt+nick).hexdigest();
try:
redis_hset("paymentid",p,nick)
except Exception,e:
log_error('GetPaymentID: failed to set payment ID for %s to redis: %s' % (nick,str(e)))
return p
def GetNickFromPaymendID(p):
nick = redis_hget("paymentid",p)
log_log('PaymendID %s => %s' % (p, str(nick)))
return nick
def AmountToString(amount):
if amount == None:
amount = 0
lamount=long(amount)
samount = None
if lamount == 0:
samount = "0 %s" % coinspecs.name
else:
for den in coinspecs.denominations:
if lamount < den[0]:
samount = "%.16g %s" % (float(lamount) / den[1], den[2])
break
if not samount:
samount = "%.16g %s" % (float(lamount) / coinspecs.atomic_units, coinspecs.name)
log_log("AmountToString: %s -> %s" % (str(amount),samount))
return samount
def SendJSONRPCCommand(host,port,method,params):
try:
http = httplib.HTTPConnection(host,port)
except Exception,e:
log_error('SendJSONRPCCommand: Error connecting to %s:%u: %s' % (host, port, str(e)))
raise
d = dict(id="0",jsonrpc="2.0",method=method,params=params)
try:
j = json.dumps(d).encode()
except Exception,e:
log_error('SendJSONRPCCommand: Failed to encode JSON: %s' % str(e))
http.close()
raise
log_log('SendJSONRPCCommand: Sending json as body: %s' % j)
headers = None
try:
http.request("POST","/json_rpc",body=j)
except Exception,e:
log_error('SendJSONRPCCommand: Failed to post request: %s' % str(e))
http.close()
raise
response = http.getresponse()
log_log('SendJSONRPCCommand: Received reply status: %s' % response.status)
if response.status != 200:
log_error('SendJSONRPCCommand: Error, not 200: %s' % str(response.status))
http.close()
raise RuntimeError("Error "+response.status)
s = response.read()
log_log('SendJSONRPCCommand: Received reply: %s' % str(s))
try:
j = json.loads(s)
except Exception,e:
log_error('SendJSONRPCCommand: Failed to decode JSON: %s' % str(e))
http.close()
raise
http.close()
return j
def SendHTMLCommand(host,port,method):
try:
http = httplib.HTTPConnection(host,port)
except Exception,e:
log_error('SendHTMLCommand: Error connecting to %s:%u: %s' % (host, port, str(e)))
raise
headers = None
try:
http.request("POST","/"+method)
except Exception,e:
log_error('SendHTMLCommand: Failed to post request: %s' % str(e))
http.close()
raise
response = http.getresponse()
log_log('SendHTMLCommand: Received reply status: %s' % response.status)
if response.status != 200:
log_error('SendHTMLCommand: Error, not 200: %s' % str(response.status))
http.close()
raise RuntimeError("Error "+response.status)
s = response.read()
log_log('SendHTMLCommand: Received reply: %s' % s)
try:
j = json.loads(s)
except Exception,e:
log_error('SendHTMLCommand: Failed to decode JSON: %s' % str(e))
http.close()
raise
http.close()
return j
def SendWalletJSONRPCCommand(method,params):
return SendJSONRPCCommand(config.wallet_host,config.wallet_port,method,params)
def SendDaemonJSONRPCCommand(method,params):
return SendJSONRPCCommand(config.daemon_host,config.daemon_port,method,params)
def SendDaemonHTMLCommand(method):
return SendHTMLCommand(config.daemon_host,config.daemon_port,method)

View File

@ -1,18 +0,0 @@
#!/bin/python
#
# Cryptonote tipbot - dashcoin setup
# Copyright 2014 moneromooo
#
# The Cryptonote tipbot is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License as published
# by the Free Software Foundation; either version 2, or (at your option)
# any later version.
#
coin_name="Dashcoin"
coin=1e8
coin_denominations = []
address_length = [96] # min/max size of addresses
address_prefix = ['D'] # allowed prefixes of addresses
min_withdrawal_fee = 0.01
web_wallet_url = None

View File

@ -1,18 +0,0 @@
#!/bin/python
#
# Cryptonote tipbot - ducknote setup
# Copyright 2014 moneromooo
#
# The Cryptonote tipbot is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License as published
# by the Free Software Foundation; either version 2, or (at your option)
# any later version.
#
coin_name="Darknote"
coin=1e8
coin_denominations = []
address_length = [95, 98] # min/max size of addresses
address_prefix = ['dd'] # allowed prefixes of addresses
min_withdrawal_fee = 0.1
web_wallet_url = None

View File

@ -1,18 +0,0 @@
#!/bin/python
#
# Cryptonote tipbot - monero setup
# Copyright 2014 moneromooo
#
# The Cryptonote tipbot is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License as published
# by the Free Software Foundation; either version 2, or (at your option)
# any later version.
#
coin_name="Monero"
coin=1e12
coin_denominations = [[1000000, 1, "piconero"], [1000000000, 1e6, "micronero"], [1000000000000, 1e9, "millinero"]]
address_length = [95, 95] # min/max size of addresses
address_prefix = ['4', '9'] # allowed prefixes of addresses
min_withdrawal_fee = 10000000000
web_wallet_url = "https://mymonero.com/" # None is there's none