diff --git a/tipbot.py b/tipbot.py index e663b1a..e1d9e1d 100644 --- a/tipbot.py +++ b/tipbot.py @@ -25,8 +25,10 @@ import importlib import tipbot.coinspecs as coinspecs import tipbot.config as config from tipbot.log import log_error, log_warn, log_info, log_log +from tipbot.link import * +from tipbot.user import * +from tipbot.group import * from tipbot.utils import * -from tipbot.ircutils import * from tipbot.redisdb import * from tipbot.command_manager import * @@ -73,6 +75,9 @@ if not selected_coin: sys.path.append(os.path.join('tipbot','modules')) for modulename in modulenames: + if modulename in sys.modules: + log_error('A %s module already exists' % modulename) + exit(1) log_info('Importing %s module' % modulename) try: __import__(modulename) @@ -82,28 +87,28 @@ for modulename in modulenames: -def GetBalance(nick,chan,cmd): - sendto=GetSendTo(nick,chan) - log_log("GetBalance: checking %s" % nick) +def GetBalance(link,cmd): + nick=link.user.nick + log_log("GetBalance: checking %s (%s)" % (link.identity(),str(link))) try: - balance = redis_hget("balances",nick) + balance = redis_hget("balances",link.identity()) if balance == None: balance = 0 balance = long(balance) sbalance = AmountToString(balance) if balance < coinspecs.atomic_units: if balance == 0: - SendTo(sendto, "%s's balance is %s" % (nick, sbalance)) + link.send("%s's balance is %s" % (nick, sbalance)) else: - SendTo(sendto, "%s's balance is %s (%.16g %s)" % (nick, sbalance, float(balance) / coinspecs.atomic_units, coinspecs.name)) + link.send("%s's balance is %s (%.16g %s)" % (nick, sbalance, float(balance) / coinspecs.atomic_units, coinspecs.name)) else: - SendTo(sendto, "%s's balance is %s" % (nick, sbalance)) + link.send("%s's balance is %s" % (nick, sbalance)) except Exception, e: log_error('GetBalance: exception: %s' % str(e)) - SendTo(sendto, "An error has occured") + link.send("An error has occured") -def AddBalance(nick,chan,cmd): - sendto = GetSendTo(nick,chan) +def AddBalance(link,cmd): + nick=link.user.nick if GetParam(cmd,2): anick = GetParam(cmd,1) amount = GetParam(cmd,2) @@ -111,85 +116,91 @@ def AddBalance(nick,chan,cmd): anick = nick amount = GetParam(cmd,1) if not amount: - SendTo(sendto, 'usage: !addbalance [] ') + link.send('usage: !addbalance [] ') return try: units = long(float(amount)*coinspecs.atomic_units) except Exception,e: - log_error('AddBalance: invalid amount: %s' % str(e)) - SendTo(sendto, 'usage: !addbalance [] ') + log_error('AddBalance: error converting amount: %s' % str(e)) + link.send('usage: !addbalance [] ') return - log_info("AddBalance: Adding %s to %s's balance" % (AmountToString(units),anick)) + if anick.find(':') == -1: + network=link.network + log_info('No network found in %s, using %s from command originator' % (anick,network.name)) + aidentity=Link(network,User(network,anick)).identity() + else: + aidentity=anick + log_info("AddBalance: Adding %s to %s's balance" % (AmountToString(units),aidentity)) try: - balance = redis_hincrby("balances",anick,units) + balance = redis_hincrby("balances",aidentity,units) except Exception, e: log_error('AddBalance: exception: %s' % str(e)) - SendTo(sendto, "An error has occured") - SendTo(sendto,"%s's bvalance is now %s" % (anick,AmountToString(balance))) + link.send( "An error has occured") + link.send("%s's balance is now %s" % (aidentity,AmountToString(balance))) -def ScanWho(nick,chan,cmd): - Who(chan) +def ScanWho(link,cmd): + link.network.update_users_list(link.group.name if link.group else None) -def GetHeight(nick,chan,cmd): - log_info('GetHeight: %s wants to know block height' % nick) +def GetHeight(link,cmd): + log_info('GetHeight: %s wants to know block height' % str(link)) try: j = SendDaemonHTMLCommand("getheight") except Exception,e: log_error('GetHeight: error: %s' % str(e)) - SendTo(nick,"An error has occured") + link.send("An error has occured") return log_log('GetHeight: Got reply: %s' % str(j)) if not "height" in j: log_error('GetHeight: Cannot see height in here') - SendTo(nick, "Height not found") + link.send("Height not found") return height=j["height"] log_info('GetHeight: height is %s' % str(height)) - SendTo(nick, "Height: %s" % str(height)) + link.send("Height: %s" % str(height)) -def GetTipbotBalance(nick,chan,cmd): - log_info('%s wants to know the tipbot balance' % nick) +def GetTipbotBalance(link,cmd): + log_info('%s wants to know the tipbot balance' % str(link)) try: balance, unlocked_balance = RetrieveTipbotBalance() except Exception,e: - SendTo(nick,"An error has occured") + link.send("An error has occured") return pending = long(balance)-long(unlocked_balance) if pending == 0: log_info("GetTipbotBalance: Tipbot balance: %s" % AmountToString(balance)) - SendTo(nick,"Tipbot balance: %s" % AmountToString(balance)) + link.send("Tipbot balance: %s" % AmountToString(balance)) else: log_info("GetTipbotBalance: Tipbot balance: %s (%s pending)" % (AmountToString(unlocked_balance), AmountToString(pending))) - SendTo(nick,"Tipbot balance: %s (%s pending)" % (AmountToString(unlocked_balance), AmountToString(pending))) + link.send("Tipbot balance: %s (%s pending)" % (AmountToString(unlocked_balance), AmountToString(pending))) -def DumpUsers(nick,chan,cmd): - userstable = GetUsersTable() - log_info(str(userstable)) +def DumpUsers(link,cmd): + for network in networks: + network.dump_users() -def Help(nick,chan,cmd): +def Help(link,cmd): module = GetParam(cmd,1) if module: - RunModuleHelpFunction(module,nick,chan) + RunModuleHelpFunction(module,link) return - SendTo(nick, "See available commands with !commands or !commands ") - SendTo(nick, "Available modules: %s" % ", ".join(GetModuleNameList(IsAdmin(nick)))) - SendTo(nick, "Get help on a particular module with !help ") + link.send("See available commands with !commands or !commands ") + link.send("Available modules: %s" % ", ".join(GetModuleNameList(IsAdmin(link)))) + link.send("Get help on a particular module with !help ") if coinspecs.web_wallet_url: - SendTo(nick, "No %s address ? You can use %s" % (coinspecs.name, coinspecs.web_wallet_url)) + link.send("No %s address ? You can use %s" % (coinspecs.name, coinspecs.web_wallet_url)) -def Info(nick,chan,cmd): - SendTo(nick, "Info for %s:" % config.tipbot_name) - SendTo(nick, "Copyright 2014,2015 moneromooo - http://duckpool.mooo.com/tipbot/") - SendTo(nick, "Type !help, or !commands for a list of commands") - SendTo(nick, "NO WARRANTY, YOU MAY LOSE YOUR COINS") - SendTo(nick, "By sending your %s to %s, you are giving up their control" % (coinspecs.name, config.tipbot_name)) - SendTo(nick, "to whoever runs the tipbot. Any tip you make/receive using %s" % config.tipbot_name) - SendTo(nick, "is obviously not anonymous. %s's wallet may end up corrupt, or be" % config.tipbot_name) - SendTo(nick, "stolen, the server compromised, etc. While I hope this won't be the case,") - SendTo(nick, "I will not offer any warranty whatsoever for the use of %s or the" % config.tipbot_name) - SendTo(nick, "return of any %s. Use at your own risk." % coinspecs.name) - SendTo(nick, "That being said, I hope you enjoy using it :)") +def Info(link,cmd): + link.send("Info for %s:" % config.tipbot_name) + link.send("Copyright 2014,2015 moneromooo - http://duckpool.mooo.com/tipbot/") + link.send("Type !help, or !commands for a list of commands") + link.send("NO WARRANTY, YOU MAY LOSE YOUR COINS") + link.send("By sending your %s to %s, you are giving up their control" % (coinspecs.name, config.tipbot_name)) + link.send("to whoever runs the tipbot. Any tip you make/receive using %s" % config.tipbot_name) + link.send("is obviously not anonymous. %s's wallet may end up corrupt, or be" % config.tipbot_name) + link.send("stolen, the server compromised, etc. While I hope this won't be the case,") + link.send("I will not offer any warranty whatsoever for the use of %s or the" % config.tipbot_name) + link.send("return of any %s. Use at your own risk." % coinspecs.name) + link.send("That being said, I hope you enjoy using it :)") def InitScanBlockHeight(): try: @@ -201,56 +212,83 @@ def InitScanBlockHeight(): except Exception,e: log_error('Failed to initialize scan_block_height: %s' % str(e)) -def ShowActivity(nick,chan,cmd): - achan=cmd[1] - anick=cmd[2] - activity = GetTimeSinceActive(achan,anick) - if activity: - SendTo(nick,"%s was active in %s %f seconds ago" % (anick,achan,activity)) +def ShowActivity(link,cmd): + anick=GetParam(cmd,1) + achan=GetParam(cmd,2) + if not anick or not achan: + link.send('usage: !show_activity ') + return + if anick.find(':') == -1: + network = link.network else: - SendTo(nick,"%s was never active in %s" % (anick,achan)) + parts=anick.split(':') + network_name=parts[0] + anick=parts[1] + network = GetNetworkByName(network_name) + if network: + last_activity = network.get_last_active_time(anick,achan) + if last_activity: + link.send("%s was active in %s %f seconds ago" % (anick,achan,now-last_activity)) + else: + link.send("%s was never active in %s" % (anick,achan)) + else: + link.send("%s is not a valid network" % network) -def SendToNick(nick,chan,msg): - SendTo(nick,msg) +def SendToLink(link,msg): + link.send(msg) -def IsRegistered(nick,chan,cmd): - RunRegisteredCommand(nick,chan,SendToNick,"You are registered",SendToNick,"You are not registered") +def IsRegistered(link,cmd): + RunRegisteredCommand(link,SendToLink,"You are registered",SendToLink,"You are not registered") -def Reload(nick,chan,cmd): - sendto=GetSendTo(nick,chan) +def Reload(link,cmd): modulename=GetParam(cmd,1) if not modulename: - SendTo(sendto,"Usage: reload ") + link.send("Usage: reload ") return if modulename=="builtin": - SendTo(sendto,"Cannot reload builtin module") + link.send("Cannot reload builtin module") + return + if not modulename in sys.modules: + link.send("%s is not a dynamic module" % modulename) return log_info('Unloading %s module' % modulename) UnregisterModule(modulename) log_info('Reloading %s module' % modulename) try: reload(sys.modules[modulename]) - SendTo(sendto,'%s reloaded' % modulename) + link.send('%s reloaded' % modulename) except Exception,e: log_error('Failed to load module "%s": %s' % (modulename, str(e))) - SendTo(sendto,'An error occured') + link.send('An error occured') -def Disable(nick,chan,cmd): +def Disable(link,cmd): global disabled - sendto=GetSendTo(nick,chan) disabled = True - SendTo(sendto,'%s disabled, will require restart' % config.tipbot_name) + link.send('%s disabled, will require restart' % config.tipbot_name) def OnIdle(): if disabled: return RunIdleFunctions([irc,redisdb]) -def OnIdentified(nick, identified): +def Quit(link,cmd): + global networks + msg = "" + for w in cmd[1:]: + msg = msg + " " + w + for network in networks: + log_info('Quitting %s network' % network.name) + network.quit() + networks = [] + +def OnIdle(): + RunIdleFunctions() + +def OnIdentified(link, identified): if disabled: - log_info('Ignoring identified notification for %s while disabled' % str(nick)) + log_info('Ignoring identified notification for %s while disabled' % str(link.identity())) return - RunNextCommand(nick, identified) + RunNextCommand(link, identified) def RegisterCommands(): RegisterCommand({'module': 'builtin', 'name': 'help', 'parms': '[module]', 'function': Help, 'help': "Displays help about %s" % config.tipbot_name}) @@ -267,20 +305,43 @@ def RegisterCommands(): RegisterCommand({'module': 'builtin', 'name': 'show_activity', 'function': ShowActivity, 'admin': True, 'help': "Show time since a user was last active"}) RegisterCommand({'module': 'builtin', 'name': 'reload', 'function': Reload, 'admin': True, 'help': "Reload a module"}) RegisterCommand({'module': 'builtin', 'name': 'disable', 'function': Disable, 'admin': True, 'help': "Disable %s"%config.tipbot_name}) + RegisterCommand({'module': 'builtin', 'name': 'quit', 'function': Quit, 'admin': True, 'help': "Quit"}) -def OnCommandProxy(cmd,chan,who): +def OnCommandProxy(link,cmd): if disabled: - log_info('Ignoring command from %s while disabled: %s' % (str(who),str(cmd))) + log_info('Ignoring command from %s while disabled: %s' % (str(link.identity()),str(cmd))) return - OnCommand(cmd,chan,who,RunAdminCommand,RunRegisteredCommand) + link.batch_send_start() + try: + OnCommand(link,cmd,RunAdminCommand,RunRegisteredCommand) + except Exception,e: + log_error('Exception running command %s: %s' % (str(cmd),str(e))) + link.batch_send_done() +def MigrateBalances(): + balances=redis_hgetall('balances') + for balance in balances: + if balance.find(':') == -1: + redis_hset('balances','freenode:'+balance,balances[balance]) + redis_hdel('balances',balance) -redisdb = connect_to_redis(config.redis_host,config.redis_port) -irc = connect_to_irc(config.irc_network,config.irc_port,config.tipbot_name,GetPassword(),config.irc_send_delay) -InitScanBlockHeight() RegisterCommands() +redisdb = connect_to_redis(config.redis_host,config.redis_port) +MigrateBalances() +InitScanBlockHeight() + +# TODO: make this be created when the module is loaded +irc = sys.modules["freenode"].FreenodeNetwork() +irc.set_callbacks(OnCommandProxy,OnIdentified) +if irc.connect(config.irc_network,config.irc_port,config.tipbot_name,GetPassword(),config.irc_send_delay): + AddNetwork(irc) + +while len(networks)>0: + for network in networks: + network.update() + + OnIdle() -IRCLoop(OnIdle,OnIdentified,OnCommandProxy) log_info('shutting down redis') redisdb.shutdown diff --git a/tipbot/betutils.py b/tipbot/betutils.py index 6281ef6..880adc4 100644 --- a/tipbot/betutils.py +++ b/tipbot/betutils.py @@ -41,76 +41,82 @@ def IsBetAmountValid(amount,minbet,maxbet,potential_loss,max_loss,max_loss_ratio return True, None -def IsPlayerBalanceAtLeast(nick,units): +def IsPlayerBalanceAtLeast(link,units): try: - balance = redis_hget("balances",nick) + balance = redis_hget("balances",link.identity()) if balance == None: balance = 0 balance=long(balance) if units > balance: - log_error ('%s does not have enough balance' % nick) + log_error ('%s does not have enough balance' % link.user.nick) return False, "You only have %s" % (AmountToString(balance)) except Exception,e: log_error ('failed to query balance') return False, "Failed to query balance" return True, None -def SetServerSeed(nick,game,seed): +def SetServerSeed(link,game,seed): + identity=link.identity() try: - redis_hset('%s:serverseed' % game,nick,seed) - log_info('%s\'s %s server seed set' % (nick, game)) + redis_hset('%s:serverseed' % game,identity,seed) + log_info('%s\'s %s serverseed set' % (identity, game)) except Exception,e: - log_error('Failed to set %s server seed for %s: %s' % (game, nick, str(e))) + log_error('Failed to set %s server seed for %s: %s' % (game, identity, str(e))) raise -def GetServerSeed(nick,game): - EnsureServerSeed(nick,game) +def GetServerSeed(link,game): + EnsureServerSeed(link,game) + identity=link.identity() try: - return redis_hget('%s:serverseed' % game,nick) + return redis_hget('%s:serverseed' % game,identity) except Exception,e: - log_error('Failed to get %s server seed for %s: %s' % (game, nick, str(e))) + log_error('Failed to get %s server seed for %s: %s' % (game, identity, str(e))) raise -def GenerateServerSeed(nick,game): +def GenerateServerSeed(link,game): + identity=link.identity() try: salt="kfn3kjg4nkngvekjvn3u4vgb" + ":" + game - s=salt+":"+nick+":"+str(time.time())+":"+str(random.randint(0,1000000)) + s=salt+":"+identity+":"+str(time.time())+":"+str(random.randint(0,1000000)) seed=hashlib.sha256(s).hexdigest() - SetServerSeed(nick,game,seed) + SetServerSeed(link,game,seed) except Exception,e: - log_error('Failed to generate %s server seed for %s: %s' % (game,nick,str(e))) + log_error('Failed to generate %s server seed for %s: %s' % (game,identity,str(e))) raise -def EnsureServerSeed(nick,game): - if not redis_hexists('%s:serverseed' % game,nick): - GenerateServerSeed(nick,game) +def EnsureServerSeed(link,game): + if not redis_hexists('%s:serverseed' % game,link.identity()): + GenerateServerSeed(link,game) -def SetPlayerSeed(nick,game,seed): +def SetPlayerSeed(link,game,seed): + identity=link.identity() try: - redis_hset('%s:playerseed' % game,nick,seed) - log_info('%s\'s %s playerseed set to %s' % (nick, game, seed)) + redis_hset('%s:playerseed' % game,identity,seed) + log_info('%s\'s %s playerseed set to %s' % (identity, game, seed)) except Exception,e: - log_error('Failed to set %s player seed for %s: %s' % (game, nick, str(e))) + log_error('Failed to set %s player seed for %s: %s' % (game, identity, str(e))) raise -def GetPlayerSeed(nick,game): +def GetPlayerSeed(link,game): + identity=link.identity() try: - if not redis_hexists('%s:playerseed' % game,nick): + if not redis_hexists('%s:playerseed' % game,identity): return "" - return redis_hget('%s:playerseed' % game,nick) + return redis_hget('%s:playerseed' % game,identity) except Exception,e: - log_error('Failed to get %s player seed for %s: %s' % (game, nick, str(e))) + log_error('Failed to get %s player seed for %s: %s' % (game, identity, str(e))) raise -def GetServerSeedHash(nick,game): - seed = GetServerSeed(nick,game) +def GetServerSeedHash(link,game): + seed = GetServerSeed(link,game) return hashlib.sha256(seed).hexdigest() -def RecordGameResult(nick,chan,game,win,lose,units): +def RecordGameResult(link,game,win,lose,units): + identity=link.identity() try: p = redis_pipeline() - tname="%s:stats:"%game+nick - rtname="%s:stats:reset:"%game+nick + tname="%s:stats:"%game+identity + rtname="%s:stats:reset:"%game+identity alltname="%s:stats:"%game p.hincrby(tname,"bets",1) p.hincrby(rtname,"bets",1) @@ -119,7 +125,7 @@ def RecordGameResult(nick,chan,game,win,lose,units): p.hincrby(rtname,"wagered",units) p.hincrby(alltname,"wagered",units) if win: - p.hincrby("balances",nick,units) + p.hincrby("balances",identity,units) p.hincrby(tname,"won",units) p.hincrby(rtname,"won",units) p.hincrby(alltname,"won",units) @@ -127,7 +133,7 @@ def RecordGameResult(nick,chan,game,win,lose,units): p.hincrby(rtname,"nwon",1) p.hincrby(alltname,"nwon",1) if lose: - p.hincrby("balances",nick,-units) + p.hincrby("balances",identity,-units) p.hincrby(tname,"lost",units) p.hincrby(rtname,"lost",units) p.hincrby(alltname,"lost",units) @@ -139,8 +145,9 @@ def RecordGameResult(nick,chan,game,win,lose,units): log_error('RecordGameResult: exception updating redis: %s' % str(e)) raise -def ShowGameStats(sendto,snick,title,game): - tname="%s:stats:"%game+snick +def ShowGameStats(link,sidentity,title,game): + identity=IdentityFromString(link,sidentity) + tname="%s:stats:"%game+sidentity try: bets=redis_hget(tname,"bets") wagered=redis_hget(tname,"wagered") @@ -150,10 +157,10 @@ def ShowGameStats(sendto,snick,title,game): nlost=redis_hget(tname,"nlost") except Exception,e: log_error('Failed to retrieve %s stats for %s: %s' % (game, title, str(e))) - SendTo(sendto,"An error occured") + link.send("An error occured") return if not bets: - SendTo(sendto,"No %s stats available for %s" % (game,title)) + link.send("No %s stats available for %s" % (game,title)) return bets = long(bets) @@ -164,7 +171,7 @@ def ShowGameStats(sendto,snick,title,game): nlost = long(nlost or 0) if bets==0: - SenTo(sendto,"No %s stats available for %s" % (game,title)) + link.send("No %s stats available for %s" % (game,title)) return swagered = AmountToString(wagered) @@ -175,12 +182,13 @@ def ShowGameStats(sendto,snick,title,game): sov = "+" + AmountToString(won-lost) else: sov = "-" + AmountToString(lost-won) - SendTo(sendto,"%s: %d games %d won, %d lost, %s wagered (average %s per game), %s won, %s lost, overall %s" % (title, bets, nwon, nlost, swagered, savg, swon, slost, sov)) + link.send("%s: %d games %d won, %d lost, %s wagered (average %s per game), %s won, %s lost, overall %s" % (title, bets, nwon, nlost, swagered, savg, swon, slost, sov)) -def ResetGameStats(sendto,snick,game): +def ResetGameStats(link,sidentity,game): + identity=IdentityFromString(link,sidentity) try: p = redis_pipeline() - tname="%s:stats:reset:"%game+snick + tname="%s:stats:reset:"%game+sidentity bets=p.hset(tname,"bets",0) wagered=p.hset(tname,"wagered",0) won=p.hset(tname,"won",0) @@ -188,31 +196,31 @@ def ResetGameStats(sendto,snick,game): nwon=p.hset(tname,"nwon",0) nlost=p.hset(tname,"nlost",0) p.execute() - SendTo(sendto,"%s stats reset for %s" % (game,snick)) + link.send("%s stats reset for %s" % (game,sidentity)) except Exception,e: - log_error('Error resetting %s stats for %s: %s' % (game,snick,str(e))) + log_error('Error resetting %s stats for %s: %s' % (game,sidentity,str(e))) raise def RetrieveHouseBalance(): balance, unlocked_balance = RetrieveTipbotBalance() - nicks = redis_hgetall("balances") - for nick in nicks: - nb = redis_hget("balances", nick) - unlocked_balance = unlocked_balance - long(nb) - log_log('RetrieveHouseBalance: subtracting %s from %s to give %s' % (AmountToString(nb), nick, AmountToString(unlocked_balance))) + identities = redis_hgetall("balances") + for identity in identities: + ib = redis_hget("balances", identity) + unlocked_balance = unlocked_balance - long(ib) + log_log('RetrieveHouseBalance: subtracting %s from %s to give %s' % (AmountToString(ib), identity, AmountToString(unlocked_balance))) rbal=redis_get('reserve_balance') if rbal: unlocked_balance = unlocked_balance - long(rbal) + log_log('RetrieveHouseBalance: subtracting %s reserve balance to give %s' % (AmountToString(rbal), AmountToString(unlocked_balance))) if unlocked_balance < 0: raise RuntimeError('Negative house balance') return return unlocked_balance -def ReserveBalance(nick,chan,cmd): - sendto=GetSendTo(nick,chan) +def ReserveBalance(link,cmd): rbal=GetParam(cmd,1) if rbal: try: @@ -222,36 +230,36 @@ def ReserveBalance(nick,chan,cmd): rbal = long(rbal * coinspecs.atomic_units) except Exception,e: log_error('SetReserveBalance: invalid balance: %s' % str(e)) - SendTo(sendto,"Invalid balance") + link.send("Invalid balance") return try: current_rbal=long(redis_get('reserve_balance') or 0) except Exception,e: log_error('Failed to get current reserve balance: %s' % str(e)) - SendTo(sendto,"Failed to get current reserve balance") + link.send("Failed to get current reserve balance") return if rbal == None: - SendTo(sendto,"Reserve balance is %s" % AmountToString(current_rbal)) + link.send("Reserve balance is %s" % AmountToString(current_rbal)) return try: house_balance = RetrieveHouseBalance() except Exception,e: log_error('Failed to get house balance: %s' % str(e)) - SendTo(sendto,"Failed to get house balance") + link.send("Failed to get house balance") return if rbal > house_balance + current_rbal: log_error('Cannot set reserve balance higher than max house balance') - SendTo(sendto,'Cannot set reserve balance higher than max house balance') + link.send('Cannot set reserve balance higher than max house balance') return try: redis_set('reserve_balance',rbal) except Exception,e: log_error('Failed to set reserve balance: %s' % str(e)) - SendTo(sendto,"Failed to set reserve balance") + link.send("Failed to set reserve balance") return - SendTo(sendto,"Reserve balance set") + link.send("Reserve balance set") RegisterCommand({ 'module': 'betting', diff --git a/tipbot/command_manager.py b/tipbot/command_manager.py index d768581..1ed9f20 100644 --- a/tipbot/command_manager.py +++ b/tipbot/command_manager.py @@ -11,7 +11,6 @@ import tipbot.config as config from tipbot.utils import * -from tipbot.ircutils import * modules = dict() commands = dict() @@ -19,48 +18,46 @@ calltable=dict() idles = [] cleanup = dict() -def SendToProxy(nick,chan,msg): - SendTo(GetSendTo(nick,chan),msg) +def SendToProxy(link,msg): + link.send(msg) -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) +def RunRegisteredCommand(link,ifyes,yesdata,ifno,nodata): + if link.identity() not in calltable: + calltable[link.identity()] = [] + calltable[link.identity()].append([link,ifyes,yesdata,ifno,nodata]) + if link.network.is_identified(link): + RunNextCommand(link,True) else: - SendTo('nickserv', "ACC " + nick) + link.network.identify(link) -def IsAdmin(nick): - return nick in config.admins +def IsAdmin(link): + return link.identity() 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") +def RunAdminCommand(link,ifyes,yesdata,ifno,nodata): + if not IsAdmin(link): + log_warn('RunAdminCommand: %s is not admin, cannot call %s with %s' % (str(link.identity()),str(ifyes),str(yesdata))) + link.send("Access denied") return - RunRegisteredCommand(nick,chan,ifyes,yesdata,ifno,nodata) + RunRegisteredCommand(link,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) +def RunNextCommand(link,registered): + identity = link.identity() + if identity not in calltable: + log_error('Nothing in queue for %s' % identity) return try: + link=calltable[identity][0][0] if registered: - calltable[nick][0][1](nick,calltable[nick][0][0],calltable[nick][0][2]) + calltable[identity][0][1](link,calltable[identity][0][2]) else: - calltable[nick][0][3](nick,calltable[nick][0][0],calltable[nick][0][4]) - del calltable[nick][0] + calltable[identity][0][3](link,calltable[identity][0][4]) + del calltable[identity][0] except Exception, e: log_error('RunNextCommand: Exception in action, continuing: %s' % str(e)) - del calltable[nick][0] + del calltable[identity][0] -def Commands(nick,chan,cmd): - if IsAdmin(nick): +def Commands(link,cmd): + if IsAdmin(link): all = True else: all = False @@ -68,9 +65,9 @@ def Commands(nick,chan,cmd): module_name = GetParam(cmd,1) if module_name: - SendTo(nick, "Commands for %s's %s module:" % (config.tipbot_name,module_name)) + link.send_private("Commands for %s's %s module:" % (config.tipbot_name,module_name)) else: - SendTo(nick, "Commands for %s (use !commands for help about the module's commands):" % config.tipbot_name) + link.send_private("Commands for %s (use !commands for help about the module's commands):" % config.tipbot_name) msgs = dict() for command_name in commands: @@ -84,7 +81,7 @@ def Commands(nick,chan,cmd): synopsis = c['name'] if 'parms' in c: synopsis = synopsis + " " + c['parms'] - SendTo(nick, "%s - %s" % (synopsis, c['help'])) + link.send_private("%s - %s" % (synopsis, c['help'])) else: if module in msgs: msgs[module] = msgs[module] +(", ") @@ -94,7 +91,7 @@ def Commands(nick,chan,cmd): if not module_name: for msg in msgs: - SendTo(nick, "%s" % msgs[msg]) + link.send_private("%s" % msgs[msg]) def RegisterModule(module): if module['name'] in modules: @@ -131,7 +128,7 @@ def RegisterIdleFunction(module,function): def RegisterCleanupFunction(module,function): cleanup.append((module,function)) -def OnCommand(cmd,chan,who,check_admin,check_registered): +def OnCommand(link,cmd,check_admin,check_registered): cmdparts = cmd[0].split(':') log_log('cmdparts: %s' % str(cmdparts)) if len(cmdparts) == 2: @@ -141,7 +138,7 @@ def OnCommand(cmd,chan,who,check_admin,check_registered): modulename = None cmdname = cmdparts[0] else: - SendTo(GetNick(who), "Invalid command, try !help") + link.send("Invalid command, try !help") return log_log('modulename: %s, cmdname: %s' % (str(modulename),str(cmdname))) if cmdname in commands: @@ -153,7 +150,7 @@ def OnCommand(cmd,chan,who,check_admin,check_registered): if msg != "": msg = msg + ", " msg = msg + c['module'] + ":" + cmd[0] - SendTo(GetNick(who), "Ambiguous command, try one of: %s" % msg) + link.send("Ambiguous command, try one of: %s" % msg) return c = None for command in commands[cmdname]: @@ -161,34 +158,34 @@ def OnCommand(cmd,chan,who,check_admin,check_registered): c = command break if not c: - SendTo(GetNick(who), "Invalid command, try !help") + link.send("Invalid command, try !help") return else: c = commands[cmdname][0] if 'admin' in c and c['admin']: - check_admin(GetNick(who),chan,c['function'],cmd,SendToProxy,"You must be admin") + check_admin(link,c['function'],cmd,SendToProxy,"You must be admin") elif 'registered' in c and c['registered']: - check_registered(GetNick(who),chan,c['function'],cmd,SendToProxy,"You must be registered with Freenode") + check_registered(link,c['function'],cmd,SendToProxy,"You must be registered with Freenode") else: - c['function'](GetNick(who),chan,cmd) + c['function'](link,cmd) else: - SendTo(GetNick(who), "Invalid command, try !help") + link.send("Invalid command, try !help") -def RunIdleFunctions(param): +def RunIdleFunctions(param=None): for f in idles: try: f[1](param) except Exception,e: log_error("Exception running idle function %s from module %s: %s" % (str(f[1]),str(f[2]),str(e))) -def RunModuleHelpFunction(module,nick,chan): +def RunModuleHelpFunction(module,link): if module in modules: try: - modules[module]['help'](nick,chan) + modules[module]['help'](link) except Exception,e: log_error("Exception running help function %s from module %s: %s" % (str(modules[module]['help']),str(module),str(e))) else: - SendTo(nick,'No help found for module %s' % module) + link.send_private('No help found for module %s' % module) def UnregisterModule(module): global commands diff --git a/tipbot/config.py b/tipbot/config.py index 2e72294..de6cc41 100644 --- a/tipbot/config.py +++ b/tipbot/config.py @@ -35,7 +35,7 @@ min_withdraw_amount = None # None defaults to the withdrawal fee withdrawal_mixin=0 disable_withdraw_on_error = True -admins = ["moneromooo", "moneromoo"] +admins = ["freenode:moneromooo", "freenode:moneromoo"] # list of nicks to ignore for rains - bots, trolls, etc no_rain_to_nicks = [] diff --git a/tipbot/group.py b/tipbot/group.py new file mode 100644 index 0000000..ca78358 --- /dev/null +++ b/tipbot/group.py @@ -0,0 +1,19 @@ +#!/bin/python +# +# Cryptonote tipbot - group +# Copyright 2015 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. +# + +class Group: + def __init__(self,network,name): + self.network=network + self.name=name + + def send(self,msg): + self.network.send_group(self,msg) + diff --git a/tipbot/ircutils.py b/tipbot/ircutils.py deleted file mode 100644 index fffda0f..0000000 --- a/tipbot/ircutils.py +++ /dev/null @@ -1,515 +0,0 @@ -#!/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 ssl -import select -import time -import string -import base64 -import re -import tipbot.config as config -from tipbot.log import log, log_error, log_warn, log_info, log_log - -irc_line_delay = 0 -irc = None -sslirc = None -irc_password = "" -irc_min_send_delay = 0.05 # seconds -irc_max_send_delay = 1.1 # seconds - -last_ping_time = time.time() -last_send_time = 0 -current_send_delay = irc_min_send_delay -irc_network = None -irc_port = None -irc_name = None -irc_quitting = False - -userstable=dict() -registered_users=set() - -def log_IRCRECV(msg): - log("IRCRECV",msg) - -def log_IRCSEND(msg): - log("IRCSEND",msg) - -def SendIRC(msg): - global last_send_time, current_send_delay - - t = time.time() - dt = t - last_send_time - if dt < current_send_delay: - time.sleep (current_send_delay - dt) - current_send_delay = current_send_delay * 1.5 - if current_send_delay > irc_max_send_delay: - current_send_delay = irc_max_send_delay - else: - while dt > current_send_delay * 1.5: - dt = dt - current_send_delay - current_send_delay = current_send_delay / 1.5 - if current_send_delay < irc_min_send_delay: - current_send_delay = irc_min_send_delay - break - - log_IRCSEND(msg) - irc_send(msg + '\r\n') - last_send_time = time.time() - -def irc_recv(size,flags=None): - if config.irc_use_ssl: - return sslirc.read(size) - else: - return irc.recv(size,flags) - -def irc_send(data): - if config.irc_use_ssl: - return sslirc.write(data) - else: - return irc.send(data) - -def connect_to_irc(network,port,name,password,delay): - global irc - global sslirc - global irc_line_delay - global irc_network - global irc_port - global irc_line_delay - global irc_name - 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 ) - if config.irc_use_ssl: - try: - raise RuntimeError('') - irc_ssl_context = ssl.create_default_context() - sslirc = irc_ssl_context.wrap_socket(irc, network) - sslirc.connect ( ( network, port ) ) - except Exception,e: - log_warn('Failed to create SSL context, using fallback code') - irc.connect ( ( network, port ) ) - sslirc = socket.ssl(irc) - except Exception, e: - log_error( 'Error initializing IRC: %s' % str(e)) - exit() - log_IRCRECV(irc_recv(4096)) - if config.irc_use_sasl: - SendIRC('CAP REQ :sasl') - else: - 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 SendTo(where,msg): - SendIRC ('PRIVMSG ' + where + ' : ' + msg) - -def Join(chan): - SendIRC ( 'JOIN ' + chan) - -def Part(chan): - SendIRC ( 'PART ' + chan) - -def Quit(msg): - global irc_quitting - irc_quitting = True - SendIRC ( 'QUIT%s' % msg) - -def Who(chan): - userstable[chan] = dict() - SendIRC ( 'WHO ' + chan) - -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 chan[0] != '#': - return - 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 IsAcceptableCommandPrefix(s): - s=s.strip() - log_log('checking whether %s is an acceptable command prefix' % s) - if s=="": - return True - if re.match("%s[\t ]*[:,]?$"%config.tipbot_name, s): - return True - return False - -#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(): - global buffered_data - idx = buffered_data.find("\n") - if idx == -1: - try: - (r,w,x)=select.select([irc.fileno()],[],[],1) - if irc.fileno() in r: - newdata=irc_recv(4096,socket.MSG_DONTWAIT) - else: - newdata = None - if irc.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 - global last_ping_time - - while True: - action = None - try: - data = GetIRCLine() - except Exception,e: - log_warn('Exception from GetIRCLine, we were probably disconnected, reconnecting in 5 seconds') - time.sleep(5) - last_ping_time = time.time() - reconnect_to_irc() - 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 > config.irc_timeout_seconds: - log_warn('%s seconds without PING, reconnecting in 5 seconds' % config.irc_timeout_seconds) - time.sleep(5) - last_ping_time = time.time() - reconnect_to_irc() - continue - - data = data.strip("\r\n") - log_IRCRECV(data) - - # consider any IRC data as a ping - last_ping_time = time.time() - - if data.find ( config.irc_welcome_line ) != -1: - userstable = dict() - registered_users.clear() - if not config.irc_use_sasl: - SendTo("nickserv", "IDENTIFY %s" % irc_password) - for chan in config.irc_channels: - Join(chan) - #ScanWho(None,[chan]) - - if data.find ( 'PING' ) == 0: - log_log('Got PING, replying PONG') - last_ping_time = time.time() - SendIRC ( 'PONG ' + data.split() [ 1 ]) - continue - - if data.startswith('AUTHENTICATE +'): - if config.irc_use_sasl: - authstring = config.irc_sasl_name + chr(0) + config.irc_sasl_name + chr(0) + irc_password - SendIRC('AUTHENTICATE %s' % base64.b64encode(authstring)) - else: - log_warn('Got AUTHENTICATE while not using SASL') - - if data.find('ERROR :Closing Link:') == 0: - if irc_quitting: - log_info('IRC stopped, bye') - break - log_warn('We were kicked from IRC, reconnecting in 5 seconds') - time.sleep(5) - last_ping_time = time.time() - reconnect_to_irc() - continue - - #--------------------------- Action check --------------------------------# - if data.find(':') == -1: - continue - - try: - cparts = data.lstrip(':').split(' :') - if len(cparts) == 0: - continue - #if len(cparts) >= 9: - # idx_colon = data.find(':',1) - # idx_space = data.find(' ') - # if idx_space and idx_colon < idx_space and re.search("@([0-9a-fA-F]+:){7}[0-9a-fA-F]+", data): - # log_info('Found IPv6 address in non-text, restructuring') - # idx = data.rfind(':') - # cparts = [ cparts[0], "".join(cparts[1:]) ] - if len(cparts) >= 2: - text = cparts[1] - else: - text = "" - parts = cparts[0].split(' ') - who = parts[0] - action = parts[1] - if len(parts) >= 3: - chan = parts[2] - else: - chan = None - except Exception, e: - log_error('main parser: Exception, continuing: %s' % str(e)) - continue - - if action == None: - continue - - #print 'cparts: ', str(cparts) - #print 'parts: ', str(parts) - #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 == 'CAP': - if parts[2] == '*' and parts[3] == 'ACK': - log_info('CAP ACK received from server') - SendIRC('AUTHENTICATE PLAIN') - elif parts[2] == '*' and parts[3] == 'NAK': - log_info('CAP NAK received from server') - log_error('Failed to negotiate SASL') - exit() - else: - log_warn('Unknown CAP line received from server: %s' % data) - if action == 'NOTICE': - if text.find ('throttled due to flooding') >= 0: - log_warn('Flood protection kicked in, outgoing messages lost') - 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 == '903': - log_info('SASL authentication success') - SendIRC('CAP END') - elif action in ['902', '904', '905', '906']: - log_error('SASL authentication failed (%s)' % action) - - 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 "352" line: %s: %s' % (data, str(e))) - - elif action == '353': - try: - who_chan = parts[4] - who_chan_users = cparts[1].split(" ") - log_info('who_chan: %s' % str(who_chan)) - log_info('who_chan_users: %s' % str(who_chan_users)) - 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 "353" line: %s: %s' % (data, str(e))) - - elif action == 'PRIVMSG': - UpdateLastActiveTime(chan,GetNick(who)) - # resplit to avoid splitting text that contains ':' - text = data.split(':',2)[2] - text=text.strip() - exidx = text.find('!') - if exidx != -1 and len(text)>exidx+1 and text[exidx+1] in string.ascii_letters and IsAcceptableCommandPrefix(text[:exidx]): - cmd = text.split('!')[1] - cmd = cmd.split(' ') - cmd[0] = cmd[0].strip(' \t\n\r') - - log_log('Found command from %s: "%s" in channel "%s"' % (who, 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 == 'KICK': - nick = parts[3] - log_info('%s was kicked' % 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 = cparts[len(cparts)-1] - 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)) - diff --git a/tipbot/link.py b/tipbot/link.py new file mode 100644 index 0000000..de0f8f4 --- /dev/null +++ b/tipbot/link.py @@ -0,0 +1,62 @@ +#!/bin/python +# +# Cryptonote tipbot - link +# Copyright 2015 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. +# + +from tipbot.log import log_error, log_warn, log_info, log_log + +class Link: + def __init__(self,network,user,group=None,data=None): + self.network=network + self.user=user + self.group=group + self.data=data + self.batch_message = None + self.batch_message_private = None + + def __repr__(self): + return '' % (str(self.network),str(self.user),str(self.group),str(self.data)) + + def identity(self): + return self.network.name+":"+self.user.nick + + def send(self,msg): + if self.batch_message != None: + self.batch_message.append(msg) + else: + return self._send(msg) + + def send_private(self,msg): + if self.batch_message_private != None: + self.batch_message_private.append(msg) + else: + return self._send_private(msg) + + def _send(self,msg): + if self.group: + self.network.send_group(self.group,msg,self.data) + else: + self.network.send_user(self.user,msg,self.data) + + def _send_private(self,msg): + self.network.send_user(self.user,msg,self.data) + + def batch_send_start(self): + self.batch_message = [] + self.batch_message_private = [] + + def batch_send_done(self): + if self.batch_message != None: + if len(self.batch_message)>0: + self._send("\n".join(self.batch_message)) + self.batch_message = None + if self.batch_message_private != None: + if len(self.batch_message_private)>0: + self._send_private("\n".join(self.batch_message_private)) + self.batch_message_private = None diff --git a/tipbot/modules/dice.py b/tipbot/modules/dice.py index a622e53..6179f32 100644 --- a/tipbot/modules/dice.py +++ b/tipbot/modules/dice.py @@ -20,95 +20,93 @@ 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.ircutils import * from tipbot.command_manager import * from tipbot.redisdb import * from tipbot.betutils import * -def GetHouseBalance(nick,chan,cmd): - sendto=GetSendTo(nick,chan) +def GetHouseBalance(link,cmd): try: balance = RetrieveHouseBalance() except Exception,e: log_error('Failed to retrieve house balance: %s' % str(e)) - SendTo(sendto, 'An error occured') + link.send('An error occured') return - SendTo(sendto, 'House balance: %s' % AmountToString(balance)) + link.send('House balance: %s' % AmountToString(balance)) -def Roll(nick): +def Roll(link): + identity=link.identity() try: - if redis_hexists('dice:rolls',nick): - rolls = redis_hget('dice:rolls',nick) + if redis_hexists('dice:rolls',identity): + rolls = redis_hget('dice:rolls',identity) rolls = long(rolls) + 1 else: rolls = 1 except Exception,e: - log_error('Failed to prepare roll for %s: %s' % (nick, str(e))) + log_error('Failed to prepare roll for %s: %s' % (identity, str(e))) raise try: - s = GetServerSeed(nick,'dice') + ":" + GetPlayerSeed(nick,'dice') + ":" + str(rolls) + s = GetServerSeed(link,'dice') + ":" + GetPlayerSeed(link,'dice') + ":" + str(rolls) sh = hashlib.sha256(s).hexdigest() roll = float(long(sh[0:8],base=16))/0x100000000 return rolls, roll except Exception,e: - log_error('Failed to roll for %s: %s' % (nick,str(e))) + log_error('Failed to roll for %s: %s' % (identity,str(e))) raise -def Dice(nick,chan,cmd): - sendto=GetSendTo(nick,chan) - +def Dice(link,cmd): + identity=link.identity() try: amount=float(cmd[1]) units=long(amount*coinspecs.atomic_units) multiplier = float(cmd[2]) overunder=GetParam(cmd,3) except Exception,e: - SendTo(sendto, "Usage: dice amount multiplier [over|under]") + link.send("Usage: dice amount multiplier [over|under]") return if multiplier < 0.1 or multiplier > 10: - SendTo(sendto, "Invalid multiplier: should be between 0.1 and 10") + link.send("Invalid multiplier: should be between 0.1 and 10") return if overunder == "over": under=False elif overunder == "under" or not overunder: under=True else: - SendTo(sendto, "Usage: dice amount multiplier [over|under]") + link.send("Usage: dice amount multiplier [over|under]") return - log_info("Dice: %s wants to bet %s at x%f, %s target" % (nick, AmountToString(units), multiplier, "under" if under else "over")) + log_info("Dice: %s wants to bet %s at x%f, %s target" % (identity, AmountToString(units), multiplier, "under" if under else "over")) potential_loss = amount * multiplier valid,reason = IsBetAmountValid(amount,config.dice_min_bet,config.dice_max_bet,potential_loss,config.dice_max_loss,config.dice_max_loss_ratio) if not valid: - log_info("Dice: %s's bet refused: %s" % (nick, reason)) - SendTo(sendto, "%s: %s" % (nick, reason)) + log_info("Dice: %s's bet refused: %s" % (identity, reason)) + link.send("%s: %s" % (link.user.nick, reason)) return try: - balance = redis_hget("balances",nick) + balance = redis_hget("balances",identity) if balance == None: balance = 0 balance=long(balance) if units > balance: - log_error ('%s does not have enough balance' % nick) - SendTo(sendto, "You only have %s" % (AmountToString(balance))) + log_error ('%s does not have enough balance' % identity) + link.send("You only have %s" % (AmountToString(balance))) return except Exception,e: log_error ('failed to query balance') - SendTo(sendto, "Failed to query balance") + link.send("Failed to query balance") return try: - rolls, roll = Roll(nick) + rolls, roll = Roll(link) except: - SendTo(sendto,"An error occured") + link.send("An error occured") return target = (1 - config.dice_edge) / (1+multiplier) if not under: target = 1 - target - log_info("Dice: %s's #%d roll: %.16g, target %s %.16g" % (nick, rolls, roll, "under" if under else "over", target)) + log_info("Dice: %s's #%d roll: %.16g, target %s %.16g" % (identity, rolls, roll, "under" if under else "over", target)) lose_units = units win_units = long(units * multiplier) @@ -118,129 +116,140 @@ def Dice(nick,chan,cmd): else: win = roll >= target if win: - msg = "%s wins %s on roll #%d! %.16g %s %.16g" % (nick, AmountToString(win_units), rolls, roll, "<=" if under else ">=", target) + msg = "%s wins %s on roll #%d! %.16g %s %.16g" % (link.user.nick, AmountToString(win_units), rolls, roll, "<=" if under else ">=", target) else: - msg = "%s loses %s on roll #%d. %.16g %s %.16g" % (nick, AmountToString(lose_units), rolls, roll, ">" if under else "<", target) + msg = "%s loses %s on roll #%d. %.16g %s %.16g" % (link.user.nick, AmountToString(lose_units), rolls, roll, ">" if under else "<", target) try: - RecordGameResult(nick,chan,"dice",win,not win,win_units if win else lose_units) + RecordGameResult(link,"dice",win,not win,win_units if win else lose_units) except: return - redis_hset("dice:rolls",nick,rolls) + redis_hset("dice:rolls",identity,rolls) - SendTo(sendto, "%s" % msg) + link.send("%s" % msg) -def ShowDiceStats(sendto,snick,title): - return ShowGameStats(sendto,snick,title,"dice") +def ShowDiceStats(link,sidentity,title): + return ShowGameStats(link,sidentity,title,"dice") -def GetDiceStats(nick,chan,cmd): - sendto=GetSendTo(nick,chan) - snick = GetParam(cmd,1) - if snick and snick != nick: - if not IsAdmin(nick): - log_error('%s is not admin, cannot see dice stats for %s' % (nick, snick)) - SendTo(sendto,'Access denied') +def GetDiceStats(link,cmd): + identity=link.identity() + sidentity = GetParam(cmd,1) + if sidentity: + sidentity=IdentityFromString(link,sidentity) + if sidentity and sidentity != identity: + if not IsAdmin(link): + log_error('%s is not admin, cannot see dice stats for %s' % (identity, sidentity)) + link.send('Access denied') return else: - snick=nick - ShowDiceStats(sendto,snick,snick) - ShowDiceStats(sendto,"reset:"+snick,'%s since reset' % snick) - ShowDiceStats(sendto,'','overall') + sidentity=identity + ShowDiceStats(link,sidentity,sidentity) + ShowDiceStats(link,"reset:"+sidentity,'%s since reset' % sidentity) + ShowDiceStats(link,'','overall') -def ResetDiceStats(nick,chan,cmd): - sendto=GetSendTo(nick,chan) - snick = GetParam(cmd,1) - if snick and snick != nick: - if not IsAdmin(nick): - log_error('%s is not admin, cannot see dice stats for %s' % (nick, snick)) - SendTo(sendto,'Access denied') +def ResetDiceStats(link,cmd): + identity=link.identity() + sidentity = GetParam(cmd,1) + if sidentity: + sidentity=IdentityFromString(link,sidentity) + if sidentity and sidentity != identity: + if not IsAdmin(link): + log_error('%s is not admin, cannot see dice stats for %s' % (identity, sidentity)) + link.send('Access denied') return else: - snick=nick + sidentity=identity try: - ResetGameStats(sendto,snick,"dice") + ResetGameStats(link,sidentity,"dice") except Exception,e: - SendTo(sendto,"An error occured") + link.send("An error occured") -def PlayerSeed(nick,chan,cmd): - sendto=GetSendTo(nick,chan) +def PlayerSeed(link,cmd): + identity=link.identity() fair_string = GetParam(cmd,1) if not fair_string: - SendTo(nick, "Usage: !playerseed ") + link.send("Usage: !playerseed ") return try: - SetPlayerSeed(nick,'dice',fair_string) + SetPlayerSeed(link,'dice',fair_string) except Exception,e: - log_error('Failed to save player seed for %s: %s' % (nick, str(e))) - SendTo(sendto, 'An error occured') + log_error('Failed to save player seed for %s: %s' % (identity, str(e))) + link.send('An error occured') + try: + ps = GetPlayerSeed(link,'dice') + except Exception,e: + log_error('Failed to retrieve newly set player seed for %s: %s' % (identity, str(e))) + link.send('An error occured') + return + link.send('Your new player seed is: %s' % ps) -def FairCheck(nick,chan,cmd): - sendto=GetSendTo(nick,chan) +def FairCheck(link,cmd): + identity=link.identity() try: - seed = GetServerSeed(nick,'dice') + seed = GetServerSeed(link,'dice') except Exception,e: - log_error('Failed to get server seed for %s: %s' % (nick,str(e))) - SendTo(seed,'An error has occured') + log_error('Failed to get server seed for %s: %s' % (identity,str(e))) + link.send('An error has occured') return try: - GenerateServerSeed(nick,'dice') + GenerateServerSeed(link,'dice') except Exception,e: - log_error('Failed to generate server seed for %s: %s' % (nick,str(e))) - SendTo(seed,'An error has occured') + log_error('Failed to generate server seed for %s: %s' % (identity,str(e))) + link.send('An error has occured') return - SendTo(sendto, 'Your server seed was %s - it has now been reset; see !fair for details' % str(seed)) + link.send('Your server seed was %s - it has now been reset; see !fair for details' % str(seed)) -def Seeds(nick,chan,cmd): - sendto=GetSendTo(nick,chan) +def Seeds(link,cmd): + identity=link.identity() try: - sh = GetServerSeedHash(nick,'dice') - ps = GetPlayerSeed(nick,'dice') + sh = GetServerSeedHash(link,'dice') + ps = GetPlayerSeed(link,'dice') except Exception,e: - log_error('Failed to get server seed for %s: %s' % (nick,str(e))) - SendTo(seed,'An error has occured') + log_error('Failed to get server seed for %s: %s' % (identity,str(e))) + link.send('An error has occured') return - SendTo(sendto, 'Your server seed hash is %s' % str(sh)) + link.send('Your server seed hash is %s' % str(sh)) if ps == "": - SendTo(sendto, 'Your have not set a player seed') + link.send('Your have not set a player seed') else: - SendTo(sendto, 'Your player seed hash is %s' % str(ps)) + link.send('Your player seed hash is %s' % str(ps)) -def Fair(nick,chan,cmd): - SendTo(nick,"%s's dice betting is provably fair" % config.tipbot_name) - SendTo(nick,"Your rolls are determined by three pieces of information:") - SendTo(nick," - your server seed. You can see its hash with !seeds") - SendTo(nick," - your player seed. Empty by default, you can set it with !playerseed") - SendTo(nick," - the roll number, displayed with each bet you make") - SendTo(nick,"To verify past rolls were fair, use !faircheck") - SendTo(nick,"You will be given your server seed, and a new one will be generated") - SendTo(nick,"for future rolls. Then follow these steps:") - SendTo(nick,"Calculate the SHA-256 sum of serverseed:playerseed:rollnumber") - SendTo(nick,"Take the first 8 characters of this sum to make an hexadecimal") - SendTo(nick,"number, and divide it by 0x100000000. You will end up with a number") - SendTo(nick,"between 0 and 1 which was your roll for that particular bet") - SendTo(nick,"See !faircode for Python code implementing this check") +def Fair(link,cmd): + link.send("%s's dice betting is provably fair" % config.tipbot_name) + link.send("Your rolls are determined by three pieces of information:") + link.send(" - your server seed. You can see its hash with !seeds") + link.send(" - your player seed. Empty by default, you can set it with !playerseed") + link.send(" - the roll number, displayed with each bet you make") + link.send("To verify past rolls were fair, use !faircheck") + link.send("You will be given your server seed, and a new one will be generated") + link.send("for future rolls. Then follow these steps:") + link.send("Calculate the SHA-256 sum of serverseed:playerseed:rollnumber") + link.send("Take the first 8 characters of this sum to make an hexadecimal") + link.send("number, and divide it by 0x100000000. You will end up with a number") + link.send("between 0 and 1 which was your roll for that particular bet") + link.send("See !faircode for Python code implementing this check") -def FairCode(nick,chan,cmd): - SendTo(nick,"This Python 2 code takes the seeds and roll number and outputs the roll") - SendTo(nick,"for the corresponding game. Run it with three arguments: server seed,") - SendTo(nick,"player seed (use '' if you did not set any), and roll number.") +def FairCode(link,cmd): + link.send("This Python 2 code takes the seeds and roll number and outputs the roll") + link.send("for the corresponding game. Run it with three arguments: server seed,") + link.send("player seed (use '' if you did not set any), and roll number.") - SendTo(nick,"import sys,hashlib,random") - SendTo(nick,"try:") - SendTo(nick," s=hashlib.sha256(sys.argv[1]+':'+sys.argv[2]+':'+sys.argv[3]).hexdigest()") - SendTo(nick," roll = float(long(s[0:8],base=16))/0x100000000") - SendTo(nick," print '%.16g' % roll") - SendTo(nick,"except:") - SendTo(nick," print 'need serverseed, playerseed, and roll number'") + link.send("import sys,hashlib,random") + link.send("try:") + link.send(" s=hashlib.sha256(sys.argv[1]+':'+sys.argv[2]+':'+sys.argv[3]).hexdigest()") + link.send(" roll = float(long(s[0:8],base=16))/0x100000000") + link.send(" print '%.16g' % roll") + link.send("except:") + link.send(" print 'need serverseed, playerseed, and roll number'") -def DiceHelp(nick,chan): - SendTo(nick,"The dice module is a provably fair %s dice betting game" % coinspecs.name) - SendTo(nick,"Basic usage: !dice [over|under]") - SendTo(nick,"The goal is to get a roll under (or over, at your option) a target that depends") - SendTo(nick,"on your chosen profit multiplier (1 for even money)") - SendTo(nick,"See !fair and !faircode for a description of the provable fairness of the game") - SendTo(nick,"See !faircheck to get the server seed to check past rolls were fair") +def DiceHelp(link): + link.send("The dice module is a provably fair %s dice betting game" % coinspecs.name) + link.send("Basic usage: !dice [over|under]") + link.send("The goal is to get a roll under (or over, at your option) a target that depends") + link.send("on your chosen profit multiplier (1 for even money)") + link.send("See !fair and !faircode for a description of the provable fairness of the game") + link.send("See !faircheck to get the server seed to check past rolls were fair") diff --git a/tipbot/modules/freenode.py b/tipbot/modules/freenode.py new file mode 100644 index 0000000..6fd908b --- /dev/null +++ b/tipbot/modules/freenode.py @@ -0,0 +1,54 @@ +#!/bin/python +# +# Cryptonote tipbot - Freenode +# Copyright 2015 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 string +import tipbot.config as config +from tipbot.log import log_error, log_warn, log_info, log_log +from tipbot.utils import * +from tipbot.command_manager import * +from irc import * + +class FreenodeNetwork(IRCNetwork): + def __init__(self): + IRCNetwork.__init__(self,"freenode") + + def login(self): + self.send_to("nickserv", "IDENTIFY %s" % self.password) + + def identify(self,link): + nick = link.user.nick + log_info('Asking nickserv whether %s is identified' % nick) + self.send_to('nickserv', "ACC " + nick) + + def on_notice(self,who,text): + if who == "NickServ!NickServ@services.": + if text.find(' ACC ') != -1: + stext = text.split(' ') + ns_nick = stext[0] + ns_acc = stext[1] + ns_status = stext[2] + if ns_acc == "ACC": + ns_link=Link(self,User(self,ns_nick),None) + if ns_status == "3": + log_info('NickServ says %s is identified' % ns_nick) + self.registered_users.add(ns_link.identity()) + if self.on_identified: + self.on_identified(ns_link,True) + else: + log_info('NickServ says %s is not identified' % ns_nick) + self.registered_users.discard(ns_link.identity()) + if self.on_identified: + self.on_identified(ns_link,False) + else: + log_error('ACC line not as expected...') + return True + diff --git a/tipbot/modules/irc.py b/tipbot/modules/irc.py index 268d32b..e55ec92 100644 --- a/tipbot/modules/irc.py +++ b/tipbot/modules/irc.py @@ -1,7 +1,7 @@ #!/bin/python # # Cryptonote tipbot - IRC commands -# Copyright 2014 moneromooo +# Copyright 2015 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 @@ -10,40 +10,498 @@ # import sys +import socket +import ssl +import select +import time import string +import base64 +import re import tipbot.config as config -from tipbot.log import log_error, log_warn, log_info, log_log +from tipbot.log import log, log_error, log_warn, log_info, log_log from tipbot.utils import * -from tipbot.ircutils import * +from tipbot.network import * from tipbot.command_manager import * -def JoinChannel(nick,chan,cmd): - sendto=GetSendTo(nick,chan) +irc_min_send_delay = 0.05 # seconds +irc_max_send_delay = 1.1 # seconds + +def GetNick(data): # Return Nickname + nick = data.split('!')[0] + nick = nick.replace(':', ' ') + nick = nick.replace(' ', '') + nick = nick.strip(' \t\n\r') + return nick + +class IRCNetwork(Network): + def __init__(self,name): + Network.__init__(self,name) + self.userstable = dict() + self.registered_users = set() + self.last_send_time=0 + self.last_ping_time=0 + self.current_send_delay = irc_min_send_delay + self.quitting = False + self.buffered_data = "" + + def connect(self,host,port,login,password,delay): + return self._connect(host,port,login,password,delay) + + def disconnect(self): + self._irc_sendmsg ('QUIT') + if self.sslirc: + self.sslirc.close() + self.sslirc = None + self.irc.close() + self.irc = None + + def send_to(self,where,msg): + for line in msg.split("\n"): + line=line.strip('\r') + if len(line)>0: + self._irc_sendmsg('PRIVMSG '+where+' :'+line) + + def send_group(self,group,msg,data=None): + self.send_to(group.name,msg) + + def send_user(self,user,msg,data=None): + self.send_to(user.nick,msg) + + def is_identified(self,link): + return link.identity() in self.registered_users + + def join(self,chan): + self._irc_sendmsg('JOIN '+chan) + + def part(self,chan): + self._irc_sendmsg('PART '+chan) + + def quit(self,msg=None): + self.quitting = True + if msg: + self._irc_sendmsg('QUIT%s '%msg) + else: + self._irc_sendmsg('QUIT') + + def dump_users(self): + log_info('users on %s: %s' % (self.name,str(self.userstable))) + + def update_users_list(self,chan): + if chan: + self._irc_sendmsg('WHO '+chan) + + def update_last_active_time(self,chan,nick): + if chan[0] != '#': + return + if not chan in self.userstable: + log_error("IRCNetwork:update_last_active_time: %s spoke in %s, but %s not found in users table" % (nick, chan, chan)) + self.userstable[chan] = dict() + if not nick in self.userstable[chan]: + log_error("IRCNetwork:update_last_active_time: %s spoke in %s, but was not found in that channel's users table" % (nick, chan)) + self.userstable[chan][nick] = None + self.userstable[chan][nick] = time.time() + + def get_last_active_time(self,nick,chan): + if not chan in self.userstable: + log_error("IRCNetwork:get_last_active_time: channel %s not found in users table" % chan) + return None + if not nick in self.userstable[chan]: + log_error("IRCNetwork:get_last_active_time: %s not found in channel %s's users table" % (nick, chan)) + return None + return self.userstable[chan][nick] + + def get_active_users(self,seconds,chan): + nicks = [] + if not chan in self.userstable: + return [] + now = time.time() + for nick in self.userstable[chan]: + t = self.userstable[chan][nick] + if t == None: + continue + dt = now - t + if dt < 0: + log_error("IRCNetwork:get_active_users: %s active in %s in the future" % (nick, chan)) + continue + if dt < seconds: + nicks.append(Link(self,User(self,nick),Group(self,chan))) + return nicks + + def get_users(self,chan): + nicks = [] + if not chan in self.userstable: + return [] + for nick in self.userstable[chan]: + nicks.append(Link(self,User(self,nick),Group(self,chan))) + return nicks + + def is_acceptable_command_prefix(self,s): + s=s.strip() + log_log('checking whether %s is an acceptable command prefix' % s) + if s=="": + return True + if re.match("%s[\t ]*[:,]?$"%config.tipbot_name, s): + return True + return False + + def update(self): + try: + data=self._getline() + except Exception,e: + log_warn('Exception from IRCNetwork:_getline, we were probably disconnected, reconnecting in %s seconds' % config.irc_timeout_seconds) + time.sleep(5) + self.last_ping_time = time.time() + self._reconnect() + return True + if data == None: + if time.time() - self.last_ping_time > config.irc_timeout_seconds: + log_warn('%s seconds without PING, reconnecting in 5 seconds' % config.irc_timeout_seconds) + time.sleep(5) + self.last_ping_time = time.time() + self._reconnect() + return True + + data = data.strip("\r\n") + self._log_IRCRECV(data) + + # consider any IRC data as a ping + self.last_ping_time = time.time() + + if data.find ( config.irc_welcome_line ) != -1: + self.userstable = dict() + self.registered_users.clear() + if not config.irc_use_sasl: + self.login() + for chan in config.irc_channels: + self.join(chan) + #ScanWho(None,[chan]) + + if data.find ( 'PING' ) == 0: + log_log('Got PING, replying PONG') + self.last_ping_time = time.time() + self._irc_sendmsg ( 'PONG ' + data.split() [ 1 ]) + return True + + if data.startswith('AUTHENTICATE +'): + if config.irc_use_sasl: + authstring = config.irc_sasl_name + chr(0) + config.irc_sasl_name + chr(0) + self.password + self._irc_sendmsg('AUTHENTICATE %s' % base64.b64encode(authstring)) + else: + log_warn('Got AUTHENTICATE while not using SASL') + + if data.find('ERROR :Closing Link:') == 0: + if self.quitting: + log_info('IRC stopped, bye') + return False + log_warn('We were kicked from IRC, reconnecting in 5 seconds') + time.sleep(5) + self.last_ping_time = time.time() + self._reconnect() + return True + + if data.find(':') == -1: + return True + + try: + cparts = data.lstrip(':').split(' :') + if len(cparts) == 0: + log_warn('No separator found, ignoring line') + return True + #if len(cparts) >= 9: + # idx_colon = data.find(':',1) + # idx_space = data.find(' ') + # if idx_space and idx_colon < idx_space and re.search("@([0-9a-fA-F]+:){7}[0-9a-fA-F]+", data): + # log_info('Found IPv6 address in non-text, restructuring') + # idx = data.rfind(':') + # cparts = [ cparts[0], "".join(cparts[1:]) ] + if len(cparts) >= 2: + text = cparts[1] + else: + text = "" + parts = cparts[0].split(' ') + who = parts[0] + action = parts[1] + if len(parts) >= 3: + chan = parts[2] + else: + chan = None + except Exception, e: + log_error('main parser: Exception, ignoring line: %s' % str(e)) + return True + + if action == None: + return True + + #print 'cparts: ', str(cparts) + #print 'parts: ', str(parts) + #print 'text: ', text + #print 'who: ', who + #print 'action: ', action + #print 'chan: ', chan + + try: + if action == 'CAP': + if parts[2] == '*' and parts[3] == 'ACK': + log_info('CAP ACK received from server') + self._irc_sendmsg('AUTHENTICATE PLAIN') + elif parts[2] == '*' and parts[3] == 'NAK': + log_info('CAP NAK received from server') + log_error('Failed to negotiate SASL') + exit() + else: + log_warn('Unknown CAP line received from server: %s' % data) + if action == 'NOTICE': + if text.find ('throttled due to flooding') >= 0: + log_warn('Flood protection kicked in, outgoing messages lost') + ret = self.on_notice(who,text) + if ret: + return ret + + elif action == '903': + log_info('SASL authentication success') + self._irc_sendmsg('CAP END') + elif action in ['902', '904', '905', '906']: + log_error('SASL authentication failed (%s)' % action) + + elif action == '352': + try: + who_chan = parts[3] + who_chan_user = parts[7] + if not who_chan_user in self.userstable[who_chan]: + self.userstable[who_chan][who_chan_user] = None + log_log("New list of users in %s: %s" % (who_chan, str(self.userstable[who_chan].keys()))) + except Exception,e: + log_error('Failed to parse "352" line: %s: %s' % (data, str(e))) + + elif action == '353': + try: + who_chan = parts[4] + who_chan_users = cparts[1].split(" ") + log_info('who_chan: %s' % str(who_chan)) + log_info('who_chan_users: %s' % str(who_chan_users)) + for who_chan_user in who_chan_users: + if not who_chan_user in self.userstable[who_chan]: + if who_chan_user[0] == "@": + who_chan_user = who_chan_user[1:] + self.userstable[who_chan][who_chan_user] = None + log_log("New list of users in %s: %s" % (who_chan, str(self.userstable[who_chan].keys()))) + except Exception,e: + log_error('Failed to parse "353" line: %s: %s' % (data, str(e))) + + elif action == 'PRIVMSG': + self.update_last_active_time(chan,GetNick(who)) + # resplit to avoid splitting text that contains ':' + text = data.split(':',2)[2] + exidx = text.find('!') + if exidx != -1 and len(text)>exidx+1 and text[exidx+1] in string.ascii_letters and self.is_acceptable_command_prefix(text[:exidx]): + cmd = text.split('!')[1] + cmd = cmd.split(' ') + cmd[0] = cmd[0].strip(' \t\n\r') + + log_log('Found command from %s: "%s" in channel "%s"' % (who, str(cmd), str(chan))) + if self.on_command: + self.on_command(Link(self,User(self,GetNick(who)),Group(self,chan) if chan[0]=='#' else None),cmd) + return True + + elif action == 'JOIN': + nick = GetNick(who) + log_info('%s joined the channel' % nick) + if not chan in self.userstable: + self.userstable[chan] = dict() + if nick in self.userstable[chan]: + log_warn('%s joined, but already in %s' % (nick, chan)) + else: + self.userstable[chan][nick] = None + log_log("New list of users in %s: %s" % (chan, str(self.userstable[chan].keys()))) + + elif action == 'PART': + nick = GetNick(who) + log_info('%s left the channel' % nick) + if not nick in self.userstable[chan]: + log_warn('%s left, but was not in %s' % (nick, chan)) + else: + del self.userstable[chan][nick] + log_log("New list of users in %s: %s" % (chan, str(self.userstable[chan].keys()))) + + elif action == 'QUIT': + nick = GetNick(who) + log_info('%s quit' % nick) + removed_list = "" + for chan in self.userstable: + log_log("Checking in %s" % chan) + if nick in self.userstable[chan]: + removed_list = removed_list + " " + chan + del self.userstable[chan][nick] + log_log("New list of users in %s: %s" % (chan, str(self.userstable[chan].keys()))) + + elif action == 'KICK': + nick = parts[3] + log_info('%s was kicked' % nick) + removed_list = "" + for chan in self.userstable: + log_log("Checking in %s" % chan) + if nick in self.userstable[chan]: + removed_list = removed_list + " " + chan + del self.userstable[chan][nick] + log_log("New list of users in %s: %s" % (chan, str(self.userstable[chan].keys()))) + + elif action == 'NICK': + nick = GetNick(who) + new_nick = cparts[len(cparts)-1] + log_info('%s renamed to %s' % (nick, new_nick)) + for c in self.userstable: + log_log('checking %s' % c) + if nick in self.userstable[c]: + del self.userstable[c][nick] + if new_nick in self.userstable[c]: + log_warn('%s is the new name of %s, but was already in %s' % (new_nick, nick, c)) + else: + self.userstable[c][new_nick] = None + log_log("New list of users in %s: %s" % (c, str(self.userstable[c].keys()))) + + except Exception,e: + log_error('Exception in top level action processing: %s' % str(e)) + + return True + + def _log_IRCRECV(self,msg): + log("IRCRECV",msg) + + def _log_IRCSEND(self,msg): + log("IRCSEND",msg) + + def _irc_recv(self,size,flags=None): + if config.irc_use_ssl: + return self.sslirc.read(size) + else: + return self.irc.recv(size,flags) + + def _irc_send(self,data): + if config.irc_use_ssl: + return self.sslirc.write(data) + else: + return self.irc.send(data) + + def _irc_sendmsg(self,msg): + t = time.time() + dt = t - self.last_send_time + if dt < self.current_send_delay: + time.sleep (self.current_send_delay - dt) + self.current_send_delay = self.current_send_delay * 1.5 + if self.current_send_delay > irc_max_send_delay: + self.current_send_delay = irc_max_send_delay + else: + while dt > self.current_send_delay * 1.5: + dt = dt - self.current_send_delay + self.current_send_delay = self.current_send_delay / 1.5 + if self.current_send_delay < irc_min_send_delay: + self.current_send_delay = irc_min_send_delay + break + + self._log_IRCSEND(msg) + self._irc_send(msg + '\r\n') + self.last_send_time = time.time() + + def _getline(self): + idx = self.buffered_data.find("\n") + if idx == -1: + try: + (r,w,x)=select.select([self.irc.fileno()],[],[],1) + if self.irc.fileno() in r: + newdata=self._irc_recv(4096,socket.MSG_DONTWAIT) + else: + newdata = None + if self.irc.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 + self.buffered_data+=newdata + idx = self.buffered_data.find("\n") + if idx == -1: + ret = self.buffered_data + self.buffered_data = "" + return ret + ret = self.buffered_data[0:idx+1] + self.buffered_data = self.buffered_data[idx+1:] + return ret + + def _connect(self,host,port,login,password,delay): + self.host=host + self.port=port + self.login=login + self.password=password + self.line_delay=delay + + log_info('Connecting to IRC at %s:%u' % (host, port)) + self.last_send_time=0 + self.last_ping_time = time.time() + self.quitting = False + self.buffered_data = "" + self.userstable=dict() + self.registered_users=set() + try: + self.irc = socket.socket ( socket.AF_INET, socket.SOCK_STREAM ) + if config.irc_use_ssl: + try: + raise RuntimeError('') + self.irc_ssl_context = ssl.create_default_context() + self.sslirc = self.irc_ssl_context.wrap_socket(self.irc, host) + self.sslirc.connect ( ( host, port ) ) + except Exception,e: + log_warn('Failed to create SSL context, using fallback code: %s' % str(e)) + self.irc.connect ( ( host, port ) ) + self.sslirc = socket.ssl(self.irc) + except Exception, e: + log_error( 'Error initializing IRC: %s' % str(e)) + return False + self._log_IRCRECV(self._irc_recv(4096)) + if config.irc_use_sasl: + self._irc_sendmsg('CAP REQ :sasl') + else: + self._irc_sendmsg ( 'PASS *********') + self._irc_sendmsg ( 'NICK %s' % login) + self._irc_sendmsg ( 'USER %s %s %s :%s' % (login, login, login, login)) + return True + + def _reconnect(self): + return self._connect(self.host,self.port,self.login,self.password,self.line_delay) + + + +def JoinChannel(link,cmd): jchan = GetParam(cmd,1) if not jchan: - SendTo(sendto,'Usage: join ') - rerurn - if jchan[0] != '#': - SendTo(sendto,'Channel name must start with #') + link.send('Usage: join ') return - Join(jchan) + if jchan[0] != '#': + link.send('Channel name must start with #') + return + network=GetNetworkByType(IRCNetwork) + if not network: + link.send('No IRC network found') + return + network.join(jchan) -def PartChannel(nick,chan,cmd): - sendto=GetSendTo(nick,chan) +def PartChannel(link,cmd): pchan = GetParam(cmd,1) if pchan: if pchan[0] != '#': - SendTo(sendto,'Channel name must start with #') + link.send('Channel name must start with #') return else: pchan = chan - Part(pchan) - -def QuitIRC(nick,chan,cmd): - msg = "" - for w in cmd[1:]: - msg = msg + " " + w - Quit(msg) + network=GetNetworkByType(IRCNetwork) + if not network: + link.send('No IRC network found') + return + network.part(pchan) RegisterCommand({ 'module': __name__, @@ -61,10 +519,3 @@ RegisterCommand({ 'admin': True, 'help': "Makes %s part from a channel" % (config.tipbot_name) }) -RegisterCommand({ - 'module': __name__, - 'name': 'quit', - 'function': QuitIRC, - 'admin': True, - 'help': "Makes %s quit IRC" % (config.tipbot_name) -}) diff --git a/tipbot/modules/payment.py b/tipbot/modules/payment.py index a300af5..69d26ee 100644 --- a/tipbot/modules/payment.py +++ b/tipbot/modules/payment.py @@ -35,10 +35,7 @@ def GetTipbotAddress(): log_error("GetTipbotAddress: Error retrieving %s's address: %s" % (config.tipbot_name, str(e))) return "ERROR" -def UpdateCoin(param): - irc = param[0] - redisdb = param[1] - +def UpdateCoin(data): global last_wallet_update_time if last_wallet_update_time == None: last_wallet_update_time = 0 @@ -87,11 +84,13 @@ def UpdateCoin(param): tx_hash=p["tx_hash"] amount=p["amount"] try: - recipient = GetNickFromPaymentID(payment_id) + recipient = GetIdentityFromPaymentID(payment_id) + if not recipient: + raise RuntimeError('Payment ID %s not found' % 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_error('UpdateCoin: No identity found for payment id %s, tx hash %s, amount %s: %s' % (payment_id, tx_hash, amount, str(e))) log_log('UpdateCoin: Executing received payments pipeline') pipe.execute() except Exception,e: @@ -104,13 +103,13 @@ def UpdateCoin(param): log_error('UpdateCoin: Failed to get bulk payments: %s' % str(e)) last_wallet_update_time = time.time() -def Deposit(nick,chan,cmd): - Help(nick,chan) +def Deposit(link,cmd): + Help(link) -def Help(nick,chan): - SendTo(nick, "You can send %s to your account:" % coinspecs.name); - SendTo(nick, " Address: %s" % GetTipbotAddress()) - SendTo(nick, " Payment ID: %s" % GetPaymentID(nick)) +def Help(link): + link.send("You can send %s to your account:" % coinspecs.name); + link.send(" Address: %s" % GetTipbotAddress()) + link.send(" Payment ID: %s" % GetPaymentID(link)) RegisterModule({ 'name': __name__, diff --git a/tipbot/modules/tipping.py b/tipbot/modules/tipping.py index f6a1cef..ad93699 100644 --- a/tipbot/modules/tipping.py +++ b/tipbot/modules/tipping.py @@ -23,184 +23,183 @@ 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.ircutils import * from tipbot.command_manager import * from tipbot.redisdb import * pending_confirmations=dict() -def PerformTip(nick,chan,who,units): - sendto=GetSendTo(nick,chan) +def PerformTip(link,whoid,units): + identity=link.identity() try: - balance = redis_hget("balances",nick) + balance = redis_hget("balances",identity) if balance == None: balance = 0 balance=long(balance) if units > balance: - SendTo(sendto, "You only have %s" % (AmountToString(balance))) + link.send("You only have %s" % (AmountToString(balance))) return - log_info('Tip: %s tipping %s %u units, with balance %u' % (nick, who, units, balance)) + log_info('Tip: %s tipping %s %u units, with balance %u' % (identity, whoid, units, balance)) try: p = redis_pipeline() p.incrby("tips_total_count",1); p.incrby("tips_total_amount",units); - p.hincrby("tips_count",nick,1); - p.hincrby("tips_amount",nick,units); - p.hincrby("balances",nick,-units); - p.hincrby("balances",who,units) + p.hincrby("tips_count",identity,1); + p.hincrby("tips_amount",identity,units); + p.hincrby("balances",identity,-units); + p.hincrby("balances",whoid,units) p.execute() - SendTo(sendto,"%s has tipped %s %s" % (nick, who, AmountToString(units))) + link.send("%s has tipped %s %s" % (NickFromIdentity(identity), NickFromIdentity(whoid), AmountToString(units))) except Exception, e: log_error("Tip: Error updating redis: %s" % str(e)) - SendTo(sendto, "An error occured") + link.send("An error occured") return except Exception, e: log_error('Tip: exception: %s' % str(e)) - SendTo(sendto, "An error has occured") + link.send("An error has occured") -def Tip(nick,chan,cmd): - userstable = GetUsersTable() - - sendto=GetSendTo(nick,chan) +def Tip(link,cmd): + identity=link.identity() try: who=cmd[1] amount=float(cmd[2]) except Exception,e: - SendTo(sendto, "Usage: tip nick amount") + link.send("Usage: tip nick amount") return units=long(amount*coinspecs.atomic_units) if units <= 0: - SendTo(sendto, "Invalid amount") + link.send("Invalid amount") return - log_info("Tip: %s wants to tip %s %s" % (nick, who, AmountToString(units))) - if chan in userstable: - log_info('getting keys') - userlist = userstable[chan].keys() - log_info('testingwho') - if not who in userlist: - SendTo(sendto,"%s is not in %s: if you really intend to tip %s, type !confirmtip before tipping again" % (who, chan, who)) - pending_confirmations[nick]={'who': who, 'units': units} + whoid = IdentityFromString(link,who) + + log_info("Tip: %s wants to tip %s %s" % (identity, whoid, AmountToString(units))) + if link.group: + userlist=[user.identity() for user in link.network.get_users(link.group.name)] + log_log('users: %s' % str(userlist)) + if not whoid in userlist: + link.send("%s is not in %s: if you really intend to tip %s, type !confirmtip before tipping again" % (who, link.group.name, who)) + pending_confirmations[identity]={'who': whoid, 'units': units} return - log_info('delk') - pending_confirmations.pop(nick,None) - PerformTip(nick,chan,who,units) + pending_confirmations.pop(identity,None) + PerformTip(link,whoid,units) -def ConfirmTip(nick,chan,cmd): - sendto=GetSendTo(nick,chan) - if not nick in pending_confirmations: - SendTo(sendto,"%s has no tip waiting confirmation" % nick) +def ConfirmTip(link,cmd): + identity=link.identity() + if not identity in pending_confirmations: + link.send("%s has no tip waiting confirmation" % NickFromIdentity(identity)) return - who=pending_confirmations[nick]['who'] - units=pending_confirmations[nick]['units'] - pending_confirmations.pop(nick,None) - PerformTip(nick,chan,who,units) + whoid=pending_confirmations[identity]['who'] + units=pending_confirmations[identity]['units'] + pending_confirmations.pop(identity,None) + PerformTip(link,whoid,units) -def Rain(nick,chan,cmd): - userstable = GetUsersTable() +def Rain(link,cmd): + identity=link.identity() - if chan[0] != '#': - SendTo(nick, "Raining can only be done in a channel") + group=link.group + if not group: + link.send("Raining can only be done in a group") return try: amount=float(cmd[1]) except Exception,e: - SendTo(chan, "Usage: rain amount [users]") + link.send("Usage: rain amount [users]") return users = GetParam(cmd,2) if users: try: users=long(users) except Exception,e: - SendTo(chan, "Usage: rain amount [users]") + link.send("Usage: rain amount [users]") return if amount <= 0: - SendTo(chan, "Usage: rain amount [users]") + link.send("Usage: rain amount [users]") return if users != None and users <= 0: - SendTo(chan, "Usage: rain amount [users]") + link.send("Usage: rain amount [users]") return units = long(amount * coinspecs.atomic_units) try: - balance = redis_hget("balances",nick) + balance = redis_hget("balances",identity) if balance == None: balance = 0 balance=long(balance) if units > balance: - SendTo(chan, "You only have %s" % (AmountToString(balance))) + link.send("You only have %s" % (AmountToString(balance))) return - log_log("userstable: %s" % str(userstable)) - userlist = userstable[chan].keys() - userlist.remove(nick) + userlist=[user.identity() for user in link.network.get_users(group.name)] + log_log("users in %s: %s" % (group.name,str(userlist))) + userlist.remove(identity) for n in config.no_rain_to_nicks: - userlist.remove(n) + userlist.remove(IdentityFromString(link,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") + link.send("Nobody eligible for rain") return if units < users: - SendTo(chan, "This would mean not even an atomic unit per nick") + link.send("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))) + log_info("%s wants to rain %s on %s users in %s" % (identity, AmountToString(units), users, group.name)) + log_log("eligible users in %s: %s" % (group.name, str(userlist))) random.shuffle(userlist) userlist = userlist[0:users] - log_log("selected users in %s: %s" % (chan, userlist)) + log_log("selected users in %s: %s" % (group.name, userlist)) user_units = long(units / users) enumerate_users = False if everyone: - msg = "%s rained %s on everyone in the channel" % (nick, AmountToString(user_units)) + msg = "%s rained %s on everyone in the channel" % (link.user.nick, AmountToString(user_units)) elif len(userlist) > 16: - msg = "%s rained %s on %d nicks" % (nick, AmountToString(user_units), len(userlist)) + msg = "%s rained %s on %d nicks" % (link.user.nick, AmountToString(user_units), len(userlist)) else: - msg = "%s rained %s on:" % (nick, AmountToString(user_units)) + msg = "%s rained %s on:" % (link.user.nick, AmountToString(user_units)) enumerate_users = True pipe = redis_pipeline() - pipe.hincrby("balances",nick,-units) - pipe.incrby("rain_total_count",1); - pipe.incrby("rain_total_amount",units); - pipe.hincrby("rain_count",nick,1); - pipe.hincrby("rain_amount",nick,units); + pipe.hincrby("balances",identity,-units) + pipe.incrby("rain_total_count",1) + pipe.incrby("rain_total_amount",units) + pipe.hincrby("rain_count",identity,1) + pipe.hincrby("rain_amount",identity,units) for user in userlist: pipe.hincrby("balances",user,user_units) if enumerate_users: - msg = msg + " " + user + msg = msg + " " + NickFromIdentity(user) pipe.execute() - SendTo(chan, "%s" % msg) + link.send("%s" % msg) except Exception,e: log_error('Rain: exception: %s' % str(e)) - SendTo(chan, "An error has occured") + link.send("An error has occured") return -def RainActive(nick,chan,cmd): - userstable = GetUsersTable() +def RainActive(link,cmd): + identity=link.identity() 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") + group=link.group + if not group: + link.send("Raining can only be done in a channel") return if not amount or not hours: - SendTo(chan, "usage: !rainactive []") + link.send("usage: !rainactive []") return try: amount=float(amount) if amount <= 0: raise RuntimeError("") except Exception,e: - SendTo(chan, "Invalid amount") + link.send("Invalid amount") return try: hours=float(hours) @@ -208,7 +207,7 @@ def RainActive(nick,chan,cmd): raise RuntimeError("") seconds = hours * 3600 except Exception,e: - SendTo(chan, "Invalid hours") + link.send("Invalid hours") return if minfrac: try: @@ -216,7 +215,7 @@ def RainActive(nick,chan,cmd): if minfrac < 0 or minfrac > 1: raise RuntimeError("") except Exception,e: - SendTo(chan, "minfrac must be a number between 0 and 1") + link.send("minfrac must be a number between 0 and 1") return else: minfrac = 0 @@ -224,23 +223,26 @@ def RainActive(nick,chan,cmd): units = long(amount * coinspecs.atomic_units) try: - balance = redis_hget("balances",nick) + balance = redis_hget("balances",identity) if balance == None: balance = 0 balance=long(balance) if units > balance: - SendTo(chan, "You only have %s" % (AmountToString(balance))) + link.send("You only have %s" % (AmountToString(balance))) return now = time.time() - userlist = userstable[chan].keys() - userlist.remove(nick) + userlist = [user.identity() for user in link.network.get_active_users(seconds,group.name)] + log_log('userlist: %s' % str(userlist)) + userlist.remove(link.identity()) for n in config.no_rain_to_nicks: - userlist.remove(n) + userlist.remove(IdentityFromString(link,n)) weights=dict() weight=0 + log_log('userlist to loop: %s' % str(userlist)) for n in userlist: - t = userstable[chan][n] + log_log('user to check: %s' % NickFromIdentity(n)) + t = link.network.get_last_active_time(NickFromIdentity(n),group.name) if t == None: continue dt = now - t @@ -250,15 +252,15 @@ def RainActive(nick,chan,cmd): weight += w if len(weights) == 0: - SendTo(chan, "Nobody eligible for rain") + link.send("Nobody eligible for rain") return pipe = redis_pipeline() - pipe.hincrby("balances",nick,-units) + pipe.hincrby("balances",identity,-units) pipe.incrby("arain_total_count",1); pipe.incrby("arain_total_amount",units); - pipe.hincrby("arain_count",nick,1); - pipe.hincrby("arain_amount",nick,units); + pipe.hincrby("arain_count",identity,1); + pipe.hincrby("arain_amount",identity,units); rained_units = 0 nnicks = 0 minu=None @@ -267,7 +269,8 @@ def RainActive(nick,chan,cmd): 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)) + act = now - link.network.get_last_active_time(NickFromIdentity(n),link.group.name) + log_info("%s rained %s on %s (last active %f hours ago)" % (identity, AmountToString(user_units),n,act/3600)) pipe.hincrby("balances",n,user_units) rained_units += user_units if not minu or user_units < minu: @@ -277,23 +280,23 @@ def RainActive(nick,chan,cmd): nnicks = nnicks+1 if maxu == None: - SendTo(chan, "This would mean not even an atomic unit per nick") + link.send("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 %s" % (nick, AmountToString(minu), AmountToString(maxu), AmountToString(units), AmountToString(rained_units), nnicks, TimeToString(seconds))) - SendTo(chan, "%s rained %s - %s on the %d nicks active in the last %s" % (nick, AmountToString(minu), AmountToString(maxu), nnicks, TimeToString(seconds))) + log_info("%s rained %s - %s (total %s, acc %s) on the %d nicks active in the last %f hours" % (identity, AmountToString(minu), AmountToString(maxu), AmountToString(units), AmountToString(rained_units), nnicks, TimeToString(seconds))) + link.send("%s rained %s - %s on the %d nicks active in the last %f hours" % (identity, AmountToString(minu), AmountToString(maxu), nnicks, TimeToString(seconds))) except Exception,e: log_error('Rain: exception: %s' % str(e)) - SendTo(chan, "An error has occured") + link.send("An error has occured") return -def Help(nick,chan): - SendTo(nick,'You can tip other people, or rain %s on them' % coinspecs.name) - SendTo(nick,'!tip tips a single person, while !rain shares equally between people in the channel') - SendTo(nick,'!rainactive tips all within the last N hours, with more recently active people') - SendTo(nick,'getting a larger share.') +def Help(link): + link.send('You can tip other people, or rain %s on them' % coinspecs.name) + link.send('!tip tips a single person, while !rain shares equally between people in the channel') + link.send('!rainactive tips all within the last N hours, with more recently active people') + link.send('getting a larger share.') RegisterModule({ diff --git a/tipbot/modules/withdraw.py b/tipbot/modules/withdraw.py index 6f34280..314b408 100644 --- a/tipbot/modules/withdraw.py +++ b/tipbot/modules/withdraw.py @@ -16,46 +16,47 @@ from tipbot.log import log_error, log_warn, log_info, log_log import tipbot.coinspecs as coinspecs import tipbot.config as config from tipbot.utils import * -from tipbot.ircutils import * from tipbot.redisdb import * from tipbot.command_manager import * withdraw_disabled = False -def DisableWithdraw(nick,chan,cmd): +def DisableWithdraw(link,cmd): global withdraw_disabled - if nick: - log_warn('DisableWithdraw: disabled by %s' % nick) + if link: + log_warn('DisableWithdraw: disabled by %s' % link.identity()) else: log_warn('DisableWithdraw: disabled') withdraw_disabled = True -def EnableWithdraw(nick,chan,cmd): +def EnableWithdraw(link,cmd): global withdraw_disabled - log_info('EnableWithdraw: enabled by %s' % nick) + log_info('EnableWithdraw: enabled by %s' % link.identity()) withdraw_disabled = False def CheckDisableWithdraw(): if config.disable_withdraw_on_error: DisableWithdraw(None,None,None) -def Withdraw(nick,chan,cmd): +def Withdraw(link,cmd): + identity=link.identity() + 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") + link.send("An error has occured") return try: address=cmd[1] except Exception,e: - SendTo(nick, "Usage: withdraw address [amount]") + link.send("Usage: withdraw address [amount]") return if not IsValidAddress(address): - SendTo(nick, "Invalid address") + link.send("Invalid address") return amount = GetParam(cmd,2) if amount: @@ -66,37 +67,37 @@ def Withdraw(nick,chan,cmd): amount = long(famount * coinspecs.atomic_units) amount += local_withdraw_fee except Exception,e: - SendTo(nick, "Invalid amount") + link.send("Invalid amount") return - log_info("Withdraw: %s wants to withdraw %s to %s" % (nick, AmountToString(amount) if amount else "all", address)) + log_info("Withdraw: %s wants to withdraw %s to %s" % (identity, 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") + link.send("Sorry, withdrawal is disabled due to a wallet error which requires admin assistance") return try: - balance = redis_hget("balances",nick) + balance = redis_hget("balances",identity) if balance == None: balance = 0 balance=long(balance) except Exception, e: log_error('Withdraw: exception: %s' % str(e)) - SendTo(nick, "An error has occured") + link.send("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)) + log_info("Withdraw: %s trying to withdraw %s, but only has %s" % (identity,AmountToString(amount),AmountToString(balance))) + link.send("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))) + link.send("Minimum withdrawal balance: %s, cannot withdraw %s" % (AmountToString(config.min_withdraw_amount),AmountToString(amount))) return try: fee = long(local_withdraw_fee) @@ -105,7 +106,7 @@ def Withdraw(nick,chan,cmd): log_info('Withdraw: fee: %s, to pay: %s' % (AmountToString(fee), AmountToString(topay))) params = { 'destinations': [{'address': address, 'amount': topay}], - 'payment_id': GetPaymentID(nick), + 'payment_id': GetPaymentID(link), 'fee': fee, 'mixin': config.withdrawal_mixin, 'unlock_time': 0, @@ -114,34 +115,34 @@ def Withdraw(nick,chan,cmd): except Exception,e: log_error('Withdraw: Error in transfer: %s' % str(e)) CheckDisableWithdraw() - SendTo(nick,"An error has occured") + link.send("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") + link.send("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") + link.send("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) + log_info('%s has withdrawn %s, tx hash %s' % (identity, amount, str(tx_hash))) + link.send( "Tx sent: %s" % tx_hash) try: - redis_hincrby("balances",nick,-amount) + redis_hincrby("balances",identity,-amount) except Exception, e: log_error('Withdraw: FAILED TO SUBTRACT BALANCE: exception: %s' % str(e)) CheckDisableWithdraw() -def Help(nick,chan): +def Help(link): fee = config.withdrawal_fee or coinspecs.min_withdrawal_fee min_amount = config.min_withdraw_amount or fee - SendTo(nick, "Minimum withdrawal: %s" % AmountToString(min_amount)) - SendTo(nick, "Withdrawal fee: %s" % AmountToString(fee)) + link.send("Minimum withdrawal: %s" % AmountToString(min_amount)) + link.send("Withdrawal fee: %s" % AmountToString(fee)) diff --git a/tipbot/network.py b/tipbot/network.py new file mode 100644 index 0000000..92a19f8 --- /dev/null +++ b/tipbot/network.py @@ -0,0 +1,58 @@ +#!/bin/python +# +# Cryptonote tipbot - network +# Copyright 2015 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. +# + +from link import Link +from user import User +from group import Group + +class Network: + def __init__(self,name): + self.name=name + + def connect(self): + pass + + def disconnect(self): + pass + + def send_group(self,group,msg,data=None): + pass + + def send_user(self,user,msg,data=None): + pass + + def identify(self,link): + pass + + def dump_users(self): + pass + + def set_callbacks(self,on_command,on_identified): + self.on_command=on_command + self.on_identified=on_identified + + def get_last_active_time(user_name,group_name=None): + return None + + def get_active_users(seconds,group_name=None): + return [] + + def get_users(group_name=None): + return [] + + def update_users_list(self,group_name=None): + pass + + def update(self): + return True + + def quit(self,msg=None): + pass diff --git a/tipbot/user.py b/tipbot/user.py new file mode 100644 index 0000000..9f1881f --- /dev/null +++ b/tipbot/user.py @@ -0,0 +1,24 @@ +#!/bin/python +# +# Cryptonote tipbot - user +# Copyright 2015 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. +# + +class User: + def __init__(self,network,nick): + self.network=network + self.nick=nick + + def check_registered(self): + pass + + def is_registered(self): + if not self.registered: + self.check_registered() + return self.registered + diff --git a/tipbot/utils.py b/tipbot/utils.py index 1a54e34..0321af0 100644 --- a/tipbot/utils.py +++ b/tipbot/utils.py @@ -16,9 +16,9 @@ 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.ircutils import * from tipbot.redisdb import * +networks=[] def GetPassword(): try: @@ -40,19 +40,26 @@ def GetParam(parms,idx): return parms[idx] return None -def GetPaymentID(nick): +def GetPaymentID(link): salt="2u3g55bkwrui32fi3g4bGR$j5g4ugnujb-"+coinspecs.name+"-"; - p = hashlib.sha256(salt+nick).hexdigest(); + p = hashlib.sha256(salt+link.identity()).hexdigest(); try: - redis_hset("paymentid",p,nick) + redis_hset("paymentid",p,link.identity()) except Exception,e: - log_error('GetPaymentID: failed to set payment ID for %s to redis: %s' % (nick,str(e))) + log_error('GetPaymentID: failed to set payment ID for %s to redis: %s' % (link.identity(),str(e))) return p -def GetNickFromPaymentID(p): - nick = redis_hget("paymentid",p) - log_log('PaymentID %s => %s' % (p, str(nick))) - return nick +def GetIdentityFromPaymentID(p): + if not redis_hexists("paymentid",p): + log_log('PaymentID %s not found' % p) + return None + identity = redis_hget("paymentid",p) + log_log('PaymentID %s => %s' % (p, str(identity))) + # HACK - grandfathering pre-network payment IDs + if identity.index(':') == -1: + log_warn('Pre-network payment ID found, assuming freenode') + identity = "freenode:"+identity + return identity def IsValidAddress(address): if len(address) < coinspecs.address_length[0] or len(address) > coinspecs.address_length[1]: @@ -205,3 +212,31 @@ def RetrieveTipbotBalance(): return return balance, unlocked_balance +def IdentityFromString(link,s): + if s.find(':') == -1: + network = link.network.name + nick=s + else: + parts=s.split(':') + network=parts[0] + nick=parts[1] + return network+':'+nick + +def NickFromIdentity(identity): + return identity.split(':')[1] + +def AddNetwork(network): + networks.append(network) + +def GetNetworkByName(name): + for network in networks: + if network.name==name: + return network + return None + +def GetNetworkByType(type): + for network in networks: + if isinstance(network,type): + return network + return None +