#!/bin/python # # Cryptonote tipbot - IRC commands # 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 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 from tipbot.utils import * from tipbot.network import * from tipbot.command_manager import * irc_min_send_delay = 0.05 # seconds irc_max_send_delay = 1.2 # seconds def GetNick(data): # Return Nickname nick = data.split('!')[0] nick = nick.replace(':', ' ') nick = nick.replace(' ', '') nick = nick.strip(' \t\n\r') return nick.lower() 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): try: cfg=config.network_config[self.name] host=cfg['host'] port=cfg['port'] login=cfg['login'] password=GetPassword(self.name) delay=cfg['delay'] self.use_ssl=cfg['ssl'] self.use_sasl=cfg['sasl'] self.welcome_line=cfg['welcome_line'] self.timeout_seconds=cfg['timeout_seconds'] self.channels=cfg['channels'] if self.use_sasl: self.sasl_name=cfg['sasl_name'] except Exception,e: log_error('Configuration not found for %s: %s' % (self.name, str(e))) return False 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 canonicalize(self,nick): return nick.lower() 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' % self.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 > self.timeout_seconds: log_warn('%s seconds without PING, reconnecting in 5 seconds' % self.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 ( self.welcome_line ) != -1: self.userstable = dict() self.registered_users.clear() if not self.use_sasl: self.login() for chan in self.channels: self.join(chan) #ScanWho(None,[chan]) if data.find ( 'PING' ) == 0: self.last_ping_time = time.time() self._irc_sendmsg ( 'PONG ' + data.split() [ 1 ]) return True if data.startswith('AUTHENTICATE +'): if self.use_sasl: authstring = self.sasl_name + chr(0) + self.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] in ["@","+"]: 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(' :',1)[1] 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].lower() 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 self.use_ssl: return self.sslirc.read(size) else: return self.irc.recv(size,flags) def _irc_send(self,data): if self.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 self.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 self.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: link.send('Usage: join ') return 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(link,cmd): pchan = GetParam(cmd,1) if pchan: if pchan[0] != '#': link.send('Channel name must start with #') return else: pchan = chan network=GetNetworkByType(IRCNetwork) if not network: link.send('No IRC network found') return network.part(pchan) RegisterCommand({ 'module': __name__, 'name': 'join', 'parms': '', 'function': JoinChannel, 'admin': True, 'help': "Makes %s join a channel" % (config.tipbot_name) }) RegisterCommand({ 'module': __name__, 'name': 'part', 'parms': '', 'function': PartChannel, 'admin': True, 'help': "Makes %s part from a channel" % (config.tipbot_name) }) RegisterNetwork("irc",IRCNetwork)