an Irc object handles the connection to the irc server .. receiving, sending, connect and reconnect code.
Bases: exceptions.Exception
already connected exception
Bases: exceptions.Exception
bot is already connecting exception
Bases: gozerbot.botbase.BotBase
the irc class, provides interface to irc related stuff.
do action.
broadcast txt to all joined channels.
close the connection.
connect to server/port using nick .. connect can timeout so catch exception .. reconnect if enabled.
send ctcp privmsg.
send ctcp notice.
de-op user.
change nick .. optionally set original nick and/or save to config.
give ops.
exit the bot.
do a fake ircevent.
we are connected.
handle 401 .. nick not available.
handle 401 .. nick not available.
erroneous nick.
handle nick already taken.
handle 700 .. encoding request of the server.
handle client to client request .. version and ping.
show error.
handle ircevent .. dispatch to ‘handle_command’ method.
handle notice event .. check for version request.
send pong response.
set pongcheck on received pong.
check if msg is ctcp or not .. return 1 on handling.
join channel with optional password.
send names query.
send notice.
output the first 375 chars .. put the rest into cache.
first output .. then call saymonitor.
do output to irc server .. rate limit to 3 sec.
return sizes of output queues.
leave channel.
ping the irc server.
send privmsg to irc server.
put output onto one of the output queues.
send quit message.
reconnect to the irc server.
save state data.
say what to printto.
send text to irc server.
send raw text to the server.
shutdown the bot.
give voice.
send who query.
send whois query.
# gozerbot/irc.py # # """ an Irc object handles the connection to the irc server .. receiving, sending, connect and reconnect code. """ __status__ = "seen"
from gozerbot.utils.log import rlog from gozerbot.utils.exception import handle_exception from gozerbot.utils.generic import getrandomnick, toenc, fromenc, strippedtxt from gozerbot.utils.generic import fix_format, splittxt, waitforqueue, uniqlist from gozerbot.utils.locking import lockdec from gozerbot.wait import Wait from gozerbot.config import config from monitor import saymonitor from gozerbot.less import Less from gozerbot.ignore import shouldignore from gozerbot.persist.pdod import Pdod from gozerbot.datadir import datadir from gozerbot.botbase import BotBase from gozerbot.threads.thr import start_new_thread, threaded from gozerbot.periodical import periodical from gozerbot.morphs import inputmorphs, outputmorphs from ircevent import Ircevent
import time import thread import socket import threading import os import Queue import random from errno import EAGAIN, EBADF, EPIPE
outlock = thread.allocate_lock() outlocked = lockdec(outlock)
class AlreadyConnected(Exception): """ already connected exception """ pass class AlreadyConnecting(Exception): """ bot is already connecting exception """ pass
class Irc(BotBase): """ the irc class, provides interface to irc related stuff. """ def __init__(self, name, cfg={}): BotBase.__init__(self, name, cfg) self.type = 'irc' self.wait = Wait() self.outputlock = thread.allocate_lock() self.fsock = None self.oldsock = None self.sock = None if not self.cfg.has_key('nolimiter'): self.nolimiter = 0 else: self.nolimiter = self.cfg['nolimiter'] self.reconnectcount = 0 self.pongcheck = 0 self.nickchanged = 0 self.noauto433 = 0 if not self.state.has_key('alternick'): self.state['alternick'] = self.cfg['alternick'] if not self.state.has_key('no-op'): self.state['no-op'] = [] self.nrevents = 0 self.gcevents = 0 self.outqueues = [Queue.Queue() for i in range(10)] self.tickqueue = Queue.Queue() self.nicks401 = [] self.stopreadloop = False self.stopoutloop = False self.port = self.cfg['port'] or 6667 rlog(10, self.name, "port is %s" % self.port) self.connectlock = thread.allocate_lock() self.encoding = 'utf-8' def __del__(self): self.exit() def _raw(self, txt): """ send raw text to the server. """ if not txt: return rlog(2, self.name + '.sending', txt) try: self.lastoutput = time.time() itxt = toenc(outputmorphs.do(txt), self.encoding) if self.cfg.has_key('ssl') and self.cfg['ssl']: self.sock.write(itxt + '\n') else: self.sock.send(itxt[:500] + '\n') except Exception, ex: try: (errno, errstr) = ex if errno != EPIPE and errno != EBADF: raise else: time.sleep(0.5) except: rlog(10, self.name, "ERROR: can't send %s" % str(ex)) self.reconnect() def _connect(self): """ connect to server/port using nick. """ if self.connecting: rlog(10, self.name, 'already connecting') raise AlreadyConnecting() if self.connected: rlog(10, self.name, 'already connected') raise AlreadyConnected() self.stopped = 0 self.connecting = True self.connectok.clear() self.connectlock.acquire() if self.ipv6: rlog(10, self.name, 'creating ipv6 socket') self.oldsock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM) self.ipv6 = 1 else: rlog(10, self.name, 'creating ipv4 socket') self.oldsock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) assert(self.oldsock) server = self.server elite = self.cfg['bindhost'] or config['bindhost'] if elite: try: self.oldsock.bind((elite, 0)) except socket.gaierror: rlog(10, self.name, "can't bind to %s" % elite) if not server: try: socket.inet_pton(socket.AF_INET6, self.server) except socket.error: pass else: server = self.server if not server: try: socket.inet_pton(socket.AF_INET, self.server) except socket.error: pass else: server = self.server if not server: ips = [] try: for item in socket.getaddrinfo(self.server, None): if item[0] in [socket.AF_INET, socket.AF_INET6] and item[1] == socket.SOCK_STREAM: ip = item[4][0] if ip not in ips: ips.append(ip) except socket.error: pass else: server = random.choice(ips) rlog(10, self.name, 'connecting to %s (%s)' % (server, self.server)) self.oldsock.settimeout(15) self.oldsock.connect((server, int(self.port))) rlog(10, self.name, 'connection ok') time.sleep(1) self.connected = True self.fsock = self.oldsock.makefile("r") self.oldsock.setblocking(self.blocking) self.fsock._sock.setblocking(self.blocking) if self.blocking: socktimeout = self.cfg['socktimeout'] if not socktimeout: socktimeout = 301.0 else: socktimeout = float(socktimeout) self.oldsock.settimeout(socktimeout) self.fsock._sock.settimeout(socktimeout) if self.cfg.has_key('ssl') and self.cfg['ssl']: rlog(10, self.name, 'ssl enabled') try: import ssl rlog(10, self.name, 'using ssl.wrap_socket()') self.sock = ssl.wrap_socket(self.oldsock) except Exception, ex: rlog(10, self.name, 'using socket.ssl()') self.sock = socket.ssl(self.oldsock) else: self.sock = self.oldsock try: self.outputlock.release() except thread.error: pass start_new_thread(self._readloop, ()) start_new_thread(self._outloop, ()) self._logon() self.nickchanged = 0 self.reconnectcount = 0 saymonitor.start() return 1 def _readloop(self): """ loop on the socketfile. """ self.stopreadloop = 0 self.stopped = 0 doreconnect = 0 timeout = 1 rlog(5, self.name, 'starting readloop') prevtxt = "" while not self.stopped and not self.stopreadloop: try: time.sleep(0.01) if self.cfg.has_key('ssl') and self.cfg['ssl']: intxt = inputmorphs.do(self.sock.read()).split('\n') else: intxt = inputmorphs.do(self.fsock.readline()).split('\n') if self.stopreadloop or self.stopped: doreconnect = 0 ; break if not intxt or not intxt[0]: doreconnect = 1 ; break if prevtxt: intxt[0] = prevtxt + intxt[0] prevtxt = "" if intxt[-1] != '': prevtxt = intxt[-1] intxt = intxt[:-1] for r in intxt: r = r.rstrip() rr = fromenc(r, self.encoding) if not rr: continue res = strippedtxt(rr) res = rr rlog(2, self.name, res) try: ievent = Ircevent().parse(self, res) except Exception, ex: handle_exception() ; continue if ievent: self.handle_ievent(ievent) timeout = 1 except UnicodeError: handle_exception() ; continue except socket.timeout: if self.stopped: break timeout += 1 if timeout > 2: doreconnect = 1 rlog(10, self.name, 'no pong received') break rlog(1, self.name, "socket timeout") pingsend = self.ping() if not pingsend: doreconnect = 1 ; break continue except socket.sslerror, ex: if self.stopped or self.stopreadloop: break if not 'timed out' in str(ex): handle_exception() doreconnect = 1 break timeout += 1 if timeout > 2: doreconnect = 1 rlog(10, self.name, 'no pong received') break rlog(1, self.name, "socket timeout") pingsend = self.ping() if not pingsend: doreconnect = 1 ; break continue except IOError, ex: if self.blocking and 'temporarily' in str(ex): time.sleep(0.5) ; continue handle_exception() doreconnect = 1 break except Exception, ex: if self.stopped or self.stopreadloop: break err = ex try: (errno, msg) = ex except: errno = -1 msg = err if errno == EAGAIN: time.sleep(0.5) ; continue rlog(10, self.name, "error in readloop: %s" % msg) doreconnect = 1 break rlog(5, self.name, 'readloop stopped') self.connectok.clear() self.connected = False if doreconnect: time.sleep(2) ; self.reconnect() def _getqueue(self): """ get one of the outqueues. """ go = self.tickqueue.get() for index in range(len(self.outqueues)): if not self.outqueues[index].empty(): return self.outqueues[index] def putonqueue(self, nr, *args): """ put output onto one of the output queues. """ self.outqueues[nr].put_nowait(*args) self.tickqueue.put_nowait('go') def _outloop(self): """ output loop. """ rlog(5, self.name, 'starting output loop') self.stopoutloop = 0 while not self.stopped and not self.stopoutloop: queue = self._getqueue() if queue: rlog(5, self.name, "outputsizes: %s" % self.outputsizes()) try: res = queue.get_nowait() except Queue.Empty: continue if not res: continue try: (printto, what, who, how, fromm, speed) = res except ValueError: self.send(res) ; continue if not self.stopped and not self.stopoutloop and printto not in self.nicks401: self.out(printto, what, who, how, fromm, speed) else: time.sleep(0.1) rlog(5, self.name, 'stopping output loop') def _logon(self): """ log on to the network. """ if self.password: rlog(10, self.name ,'sending password') self._raw("PASS %s" % self.password) rlog(10, self.name, 'registering with %s using nick %s' % (self.server, self.nick)) rlog(10, self.name, 'this may take a while') username = self.nick or self.cfg['username'] realname = self.cfg['realname'] or username time.sleep(1) self._raw("NICK %s" % self.nick) time.sleep(1) self._raw("USER %s localhost localhost :%s" % (username, realname)) self.connectok.wait() def _onconnect(self): """ overload this to run after connect. """ pass def _resume(self, data, reto=None): """ resume to server/port using nick. """ try: if data['ssl']: self.connectwithjoin() ; return 1 except KeyError: pass try: fd = int(data['fd']) except (TypeError, ValueError): fd = None self.connecting = False # we're already connected self.nick = data['nick'] self.orignick = self.nick self.server = str(data['server']) self.port = int(data['port']) self.password = data['password'] self.ipv6 = data['ipv6'] self.ssl = data['ssl'] if self.ipv6: if fd: rlog(1, self.name, 'resuming ipv6 socket') self.sock = socket.fromfd(fd , socket.AF_INET6, socket.SOCK_STREAM) else: rlog(10, self.name, 'creating ipv6 socket') self.sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM) self.ipv6 = 1 else: if fd: rlog(1, self.name, 'resuming ipv4 socket') self.sock = socket.fromfd(fd, socket.AF_INET, socket.SOCK_STREAM) else: rlog(10, self.name, 'creating ipv4 socket') self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) rlog(10, self.name, 'resuming to ' + self.server) self.sock.settimeout(30) rlog(10, self.name, 'connection ok') self.stopped = 0 self.fsock = self.sock.makefile("r") self.sock.setblocking(self.blocking) if self.blocking: socktimeout = self.cfg['socktimeout'] if not socktimeout: socktimeout = 301.0 else: socktimeout = float(socktimeout) self.sock.settimeout(socktimeout) rlog(0, self.name, 'resuming readloop') start_new_thread(self._readloop, ()) start_new_thread(self._outloop, ()) self.reconnectcount = 0 self.nickchanged = 0 self.connecting = False self._raw('PING :RESUME %s' % str(time.time())) self.connectok.set() self.connected = True self.reconnectcount = 0 if reto: self.say(reto, 'rebooting done') saymonitor.start() return 1 def _resumedata(self): """ return data used for resume. """ try: fd = self.sock.fileno() except AttributeError, ex: fd = None ; self.exit() return {self.name: { 'nick': self.nick, 'server': self.server, 'port': self.port, 'password': self.password, 'ipv6': self.ipv6, 'ssl': self.ssl, 'fd': fd }} def outputsizes(self): """ return sizes of output queues. """ result = [] for q in self.outqueues: result.append(q.qsize()) return result def broadcast(self, txt): """ broadcast txt to all joined channels. """ for i in self.state['joinedchannels']: self.say(i, txt, speed=-1) def save(self): """ save state data. """ self.state.save() def connect(self, reconnect=True): """ connect to server/port using nick .. connect can timeout so catch exception .. reconnect if enabled. """ res = 0 try: res = self._connect() if res: self.connectok.wait() self._onconnect() self.connected = True rlog(10, self.name, 'logged on !') self.connecting = False except AlreadyConnecting: return 0 except AlreadyConnected: return 0 except Exception, ex: self.connectlock.release() if self.stopped: return 0 rlog(10, self.name, 'connecting error: %s' % str(ex)) if reconnect: self.reconnect() ; return raise self.connectlock.release() return res def shutdown(self): """ shutdown the bot. """ rlog(10, self.name, 'shutdown') self.stopoutputloop = 1 self.stopped = 1 time.sleep(1) self.tickqueue.put_nowait('go') self.close() self.connecting = False self.connected = False self.connectok.clear() def close(self): """ close the connection. """ try: if self.cfg.has_key('ssl') and self.cfg['ssl']: self.oldsock.shutdown(2) else: self.sock.shutdown(2) except: pass try: if self.cfg.has_key('ssl') and self.cfg['ssl']: self.oldsock.close() else: self.sock.close() self.fsock.close() except: pass def exit(self): """ exit the bot. """ self.stopreadloop = 1 self.stopped = 1 self.connected = 0 self.shutdown() def reconnect(self): """ reconnect to the irc server. """ try: if self.stopped: return 0 if self.reconnectcount > 0: reconsleep = self.reconnectcount*15 rlog(10, self.name, 'sleeping %s seconds for reconnect' % reconsleep) time.sleep(reconsleep) if self.stopped: rlog(10, self.name, 'stopped.. not reconnecting') ; return 1 if self.connected: rlog(10, self.name, 'already connected .. not reconnecting') ; return 1 self.reconnectcount += 1 self.exit() rlog(10, self.name, 'reconnecting') result = self.connect() return result except Exception, ex: handle_exception() def handle_pong(self, ievent): """ set pongcheck on received pong. """ rlog(1, self.name, 'received server pong') self.pongcheck = 1 def sendraw(self, txt): """ send raw text to the server. """ if self.stopped: return rlog(2, self.name + '.sending', txt) self._raw(txt) def fakein(self, txt): """ do a fake ircevent. """ if not txt: return rlog(10, self.name + '.fakein', txt) self.handle_ievent(Ircevent().parse(self, txt)) def say(self, printto, what, who=None, how='msg', fromm=None, speed=0, groupchat=False): """ say what to printto. """ if not printto or not what or printto in self.nicks401: return if not 'socket' in repr(printto): if who: what = "%s: %s" % (who, what) if speed > 9: speed = 9 self.putonqueue(9-speed, (printto, what, who, how, fromm, speed)) return try: printto.send(what + '\n') time.sleep(0.001) except Exception, ex : if "Broken pipe" in str(ex) or "Bad file descriptor" in str(ex): return handle_exception() def out(self, printto, what, who=None, how='msg', fromm=None, speed=5): """ output the first 375 chars .. put the rest into cache. """ try: what = toenc(what.rstrip()) except Exception, ex: rlog(10, self.name, "can't output: %s" % str(ex)) ; return if not what: return txtlist = splittxt(what) size = 0 self.output(printto, txtlist[0], how, who, fromm) result = "" if len(txtlist) > 2: if not fromm: self.less.add(printto, txtlist[1:]) else: self.less.add(fromm, txtlist[1:]) size = len(txtlist) - 2 result = txtlist[1:2][0] if size: result += " (+%s)" % size else: if len(txtlist) == 2: result = txtlist[1] if result: self.output(printto, result, how, who, fromm) def output(self, printto, what, how='msg' , who=None, fromm=None): """ first output .. then call saymonitor. """ self.outputnolog(printto, what, how, who, fromm) saymonitor.put(self.name, printto, what, who, how, fromm) def outputnolog(self, printto, what, how, who=None, fromm=None): """ do output to irc server .. rate limit to 3 sec. """ if fromm and shouldignore(fromm): return try: what = fix_format(what) if what: if how == 'msg': self.privmsg(printto, what) elif how == 'notice': self.notice(printto, what) elif how == 'ctcp': self.ctcp(printto, what) except Exception, ex: handle_exception() def donick(self, nick, setorig=0, save=0, whois=0): """ change nick .. optionally set original nick and/or save to config. """ if not nick: return self.noauto433 = 1 queue = Queue.Queue() nick = nick[:16] self.wait.register('NICK', self.nick[:16], queue, 12) self._raw('NICK %s\n' % nick) result = waitforqueue(queue, 5) self.noauto433 = 0 if not result: return 0 self.nick = nick if whois: self.whois(nick) if setorig: self.orignick = nick if save: self.state['nick'] = nick self.state.save() self.cfg.set('nick', nick) self.cfg.save() return 1 def join(self, channel, password=None): """ join channel with optional password. """ if not channel: return if password: self._raw('JOIN %s %s' % (channel, password)) try: self.channels[channel]['key'] = password self.channels.save() except KeyError: pass else: self._raw('JOIN %s' % channel) def part(self, channel): """ leave channel. """ if not channel: return self._raw('PART %s' % channel) try: self.state['joinedchannels'].remove(channel) self.state.save() except (KeyError, ValueError): pass def who(self, who): """ send who query. """ if not who: return self.putonqueue(6, 'WHO %s' % who.strip()) def names(self, channel): """ send names query. """ if not channel: return self.putonqueue(6, 'NAMES %s' % channel) def whois(self, who): """ send whois query. """ if not who: return self.putonqueue(6, 'WHOIS %s' % who) def privmsg(self, printto, what): """ send privmsg to irc server. """ if not printto or not what: return self.send('PRIVMSG %s :%s' % (printto, what)) def send(self, txt): """ send text to irc server. """ if not txt: return if self.stopped: return try: self.outputlock.acquire() txt = toenc(strippedtxt(txt)) txt = txt.rstrip() self._raw(txt) now = time.time() timetosleep = 5 - (now - self.lastoutput) if timetosleep > 0 and not self.nolimiter: rlog(0, self.name, 'flood protect') ; time.sleep(timetosleep) try: self.outputlock.release() except: pass self.lastoutput = time.time() except Exception, ex: try: self.outputlock.release() except: pass if not self.blocking and 'broken pipe' in str(ex).lower(): time.sleep(0.5) else: rlog(11, self.name, 'send error: %s' % str(ex)) self.reconnect() return def voice(self, channel, who): """ give voice. """ if not channel or not who: return self.putonqueue(9, 'MODE %s +v %s' % (channel, who)) def doop(self, channel, who): """ give ops. """ if not channel or not who: return self._raw('MODE %s +o %s' % (channel, who)) def delop(self, channel, who): """ de-op user. """ if not channel or not who: return self._raw('MODE %s -o %s' % (channel, who)) def quit(self, reason='http://gozerbot.org'): """ send quit message. """ rlog(10, self.name, 'sending quit') try: self._raw('QUIT :%s' % reason) except IOError: pass def notice(self, printto, what): """ send notice. """ if not printto or not what: return self.send('NOTICE %s :%s' % (printto, what)) def ctcp(self, printto, what): """ send ctcp privmsg. """ if not printto or not what: return self.send("PRIVMSG %s :\001%s\001" % (printto, what)) def ctcpreply(self, printto, what): """ send ctcp notice. """ if not printto or not what: return self.putonqueue(2, "NOTICE %s :\001%s\001" % (printto, what)) def action(self, printto, what): """ do action. """ if not printto or not what: return self.putonqueue(9, "PRIVMSG %s :\001ACTION %s\001" % (printto, what)) def handle_ievent(self, ievent): """ handle ircevent .. dispatch to 'handle_command' method. """ try: if ievent.cmnd == 'JOIN' or ievent.msg: if ievent.nick in self.nicks401: self.nicks401.remove(ievent.nick) rlog(10, self.name, '%s joined .. unignoring' % ievent.nick) method = getattr(self,'handle_' + ievent.cmnd.lower()) if method: try: method(ievent) except: handle_exception() except AttributeError: pass try: self.wait.check(ievent) except: handle_exception() def handle_432(self, ievent): """ erroneous nick. """ self.handle_433(ievent) def handle_433(self, ievent): """ handle nick already taken. """ if self.noauto433: return nick = ievent.arguments[1] alternick = self.state['alternick'] if alternick and not self.nickchanged: rlog(10, self.name, 'using alternick %s' % alternick) self.donick(alternick) self.nickchanged = 1 return randomnick = getrandomnick() self._raw("NICK %s" % randomnick) self.nick = randomnick rlog(100, self.name, 'ALERT: nick %s already in use/unavailable .. using randomnick %s' % (nick, randomnick)) self.nickchanged = 1 def handle_ping(self, ievent): """ send pong response. """ if not ievent.txt: return self._raw('PONG :%s' % ievent.txt) def handle_001(self, ievent): """ we are connected. """ self.connectok.set() self.connected = True periodical.addjob(15, 1, self.whois, self, self.nick) def handle_privmsg(self, ievent): """ check if msg is ctcp or not .. return 1 on handling. """ if ievent.txt and ievent.txt[0] == '\001': self.handle_ctcp(ievent) return 1 def handle_notice(self, ievent): """ handle notice event .. check for version request. """ if ievent.txt and ievent.txt.find('VERSION') != -1: self.say(ievent.nick, self.cfg['version'], None, 'notice') return 1 def handle_ctcp(self, ievent): """ handle client to client request .. version and ping. """ if ievent.txt.find('VERSION') != -1: self.ctcpreply(ievent.nick, 'VERSION %s' % self.cfg['version']) if ievent.txt.find('PING') != -1: try: pingtime = ievent.txt.split()[1] pingtime2 = ievent.txt.split()[2] if pingtime: self.ctcpreply(ievent.nick, 'PING ' + pingtime + ' ' + pingtime2) except IndexError: pass def handle_error(self, ievent): """ show error. """ if ievent.txt.startswith('Closing'): rlog(10, self.name, ievent.txt) else: rlog(10, self.name + '.ERROR', "%s - %s" % (ievent.arguments, ievent.txt)) def ping(self): """ ping the irc server. """ rlog(1, self.name, 'sending ping') try: self.putonqueue(1, 'PING :%s' % self.server) return 1 except Exception, ex: rlog(10, self.name, "can't send ping: %s" % str(ex)) return 0 def handle_401(self, ievent): """ handle 401 .. nick not available. """ try: nick = ievent.arguments[1] if nick not in self.nicks401: rlog(10, self.name, '401 on %s .. ignoring' % nick) self.nicks401.append(nick) except: pass handle_403 = handle_401 def handle_700(self, ievent): """ handle 700 .. encoding request of the server. """ try: self.encoding = ievent.arguments[1] rlog(10, self.name, '700 encoding now is %s' % self.encoding) except: pass