.. _fbf.drivers.irc.irc: irc ~~~ .. automodule:: fbf.drivers.irc.irc :show-inheritance: :members: :undoc-members: CODE ---- :: # fbf/socklib/irc/irc.py # # """ an Irc object handles the connection to the irc server .. receiving, sending, connect and reconnect code. """ .. _fbf.drivers.irc.irc_fbf_imports: fbf imports -------------- :: from fbf.utils.exception import handle_exception from fbf.utils.generic import toenc, fromenc from fbf.utils.generic import getrandomnick, strippedtxt from fbf.utils.generic import fix_format, splittxt, uniqlist from fbf.utils.locking import lockdec, lock_object, release_object from fbf.lib.botbase import BotBase from fbf.lib.threads import start_new_thread, threaded from fbf.utils.pdod import Pdod from fbf.lib.channelbase import ChannelBase from fbf.lib.morphs import inputmorphs, outputmorphs from fbf.lib.exit import globalshutdown from fbf.lib.config import Config, getmainconfig .. _fbf.drivers.irc.irc_fbf.irc_imports: fbf.irc imports ------------------ :: from .ircevent import IrcEvent .. _fbf.drivers.irc.irc_basic_imports: basic imports ---------------- :: import time import _thread import socket import threading import os import queue import random import logging import types import re import select .. _fbf.drivers.irc.irc_locks_: locks -------- :: outlock = _thread.allocate_lock() outlocked = lockdec(outlock) .. _fbf.drivers.irc.irc_exceptions_: exceptions ------------- :: class Irc(BotBase): """ the irc class, provides interface to irc related stuff. """ def __init__(self, cfg=None, users=None, plugs=None, *args, **kwargs): BotBase.__init__(self, cfg, users, plugs, *args, **kwargs) BotBase.setstate(self) self.type = 'irc' self.fsock = None self.oldsock = None self.sock = None self.reconnectcount = 0 self.pongcheck = 0 self.nickchanged = False self.noauto433 = False if self.state: if 'alternick' not in self.state: self.state['alternick'] = self.cfg['alternick'] if 'no-op' not in self.state: self.state['no-op'] = [] self.nicks401 = [] self.cfg.port = self.cfg.port or 6667 self.connecttime = 0 self.encoding = 'utf-8' self.blocking = 1 self.lastoutput = 0 self.splitted = [] if not self.cfg.server: self.cfg.server = self.cfg.host or "localhost" assert self.cfg.port assert self.cfg.server def _raw(self, txt): """ send raw text to the server. """ if not txt or self.stopped or not self.sock: logging.debug("%s - bot is stopped .. not sending." % self.cfg.name) return 0 try: self.lastoutput = time.time() itxt = bytes(txt + "\n", self.encoding or "utf-8") if not self.sock: logging.debug("%s - socket disappeared - not sending." % self.cfg.name) ; return if not txt.startswith("PONG"): logging.warn("> %s (%s)" % (itxt, self.cfg.name)) else: logging.info("> %s (%s)" % (itxt, self.cfg.name)) if 'ssl' in self.cfg and self.cfg['ssl']: self.sock.write(itxt) else: self.sock.send(itxt[:502]) except Exception as ex: handle_exception() ; logging.error("%s - can't send: %s" % (self.cfg.name, str(ex))) def _connect(self): """ connect to server/port using nick. """ self.stopped = False self.connecting = True self.connectok.clear() if self.cfg.ipv6: self.oldsock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM) else: self.oldsock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) assert self.oldsock assert self.cfg.server assert self.cfg.port server = self.bind() logging.warn('connecting to %s - %s - %s (%s)' % (server, self.cfg.server, self.cfg.port, self.cfg.name)) self.oldsock.settimeout(30) self.oldsock.connect((server, int(str(self.cfg.port)))) self.blocking = 1 self.oldsock.setblocking(self.blocking) logging.warn('connected! (%s)' % self.cfg.name) self.connected = True self.fsock = self.oldsock.makefile("r") if self.blocking: socktimeout = self.cfg['socktimeout'] if not socktimeout: socktimeout = 301.0 else: socktimeout = float(socktimeout) self.oldsock.settimeout(socktimeout) if 'ssl' in self.cfg and self.cfg['ssl']: logging.warn('ssl enabled (%s)' % self.cfg.name) self.sock = socket.ssl(self.oldsock) else: self.sock = self.oldsock try: self.outputlock.release() except _thread.error: pass self.connecttime = time.time() return True def bind(self): server = self.cfg.server elite = self.cfg['bindhost'] or getmainconfig()['bindhost'] if elite: logging.warn("trying to bind to %s" % elite) try: self.oldsock.bind((elite, 0)) except socket.gaierror: logging.debug("%s - can't bind to %s" % (self.cfg.name, elite)) if not server: try: socket.inet_pton(socket.AF_INET6, self.cfg.server) except socket.error: pass else: server = self.cfg.server if not server: try: socket.inet_pton(socket.AF_INET, self.cfg.server) except socket.error: pass else: server = self.cfg.server if not server: ips = [] try: for item in socket.getaddrinfo(self.cfg.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) return server def _readloop(self): """ loop on the socketfile. """ self.stopreadloop = False self.stopped = False doreconnect = True timeout = 1 logging.debug('%s - starting readloop' % self.cfg.name) prevtxt = "" while not self.stopped and not self.stopreadloop and self.sock and self.fsock: try: time.sleep(0.01) if 'ssl' in self.cfg 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 intxt == ["",]: logging.error("remote disconnected") doreconnect = 1 break if prevtxt: intxt[0] = prevtxt + intxt[0] prevtxt = "" if intxt[-1] != '': prevtxt = intxt[-1] intxt = intxt[:-1] for r in intxt: if not r: continue try: r = strippedtxt(r.rstrip(), ["\001", "\002", "\003"]) rr = str(fromenc(r.rstrip(), self.encoding)) except UnicodeDecodeError: logging.warn("decode error - ignoring (%s)" % self.cfg.name) continue if not rr: continue res = rr try: ievent = IrcEvent().parse(self, res) except Exception as ex: handle_exception() continue try: if int(ievent.cmnd) > 400: logging.error("< %s - %s" % (res, self.cfg.name)) elif int(ievent.cmnd) >= 300: logging.info("< %s - %s" % (res, self.cfg.name)) except ValueError: if not res.startswith("PING") and not res.startswith("NOTICE"): logging.warn("< %s - %s" % (res, self.cfg.name)) else: logging.info("< %s - %s" % (res, self.cfg.name)) if ievent: self.handle_ievent(ievent) timeout = 1 except UnicodeError: handle_exception() continue except socket.timeout as ex: logging.warn("socket timeout (%s)" % self.cfg.name) self.error = str(ex) if self.stopped or self.stopreadloop: break timeout += 1 if timeout > 2: doreconnect = 1 logging.warn('no pong received (%s)' % self.cfg.name) break pingsend = self.ping() if not pingsend: doreconnect = 1 break continue except socket.sslerror as ex: self.error = str(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 logging.warn('no pong received (%s)' % self.cfg.name) break logging.warn("socket timeout (%s)" % self.cfg.name) pingsend = self.ping() if not pingsend: doreconnect = 1 break continue except IOError as ex: self.error = str(ex) if self.blocking and 'temporarily' in str(ex): logging.warn("iorror: %s (%s)" % (self.error, self.cfg.name)) time.sleep(1) continue if not self.stopped: logging.error('connecting error: %s (%s)' % (str(ex), self.cfg.name)) handle_exception() doreconnect = 1 break except socket.error as ex: self.error = str(ex) if self.blocking and 'temporarily' in str(ex): logging.warn("ioerror: %s (%s)" % (self.error, self.cfg.name)) time.sleep(0.5) continue if not self.stopped: logging.error('connecting error: %s (%s)' % (str(ex), self.cfg.name)) doreconnect = 1 break except Exception as ex: self.error = str(ex) if self.stopped or self.stopreadloop: break logging.error("%s - error in readloop: %s" % (self.cfg.name, str(ex))) doreconnect = 1 break logging.debug('%s - readloop stopped - %s' % (self.cfg.name, self.error)) self.connectok.clear() self.connected = False if doreconnect and not self.stopped: time.sleep(2) self.reconnect() def logon(self): """ log on to the network. """ time.sleep(2) if self.cfg.password: logging.debug('%s - sending password' % self.cfg.name) self._raw("PASS %s" % self.cfg.password) logging.warn('registering with %s using nick %s (%s)' % (self.cfg.server, self.cfg.nick, self.cfg.name)) logging.warn('%s - this may take a while' % self.cfg.name) username = self.cfg.username or "fbfbot" realname = self.cfg.realname or "fbfbot" time.sleep(1) self._raw("NICK %s" % self.cfg.nick) time.sleep(1) self._raw("USER %s localhost %s :%s" % (username, self.cfg.server, realname)) def _onconnect(self): """ overload this to run after connect. """ on = self.cfg.onconnect logging.debug("onconnect is %s" % on) if on: time.sleep(2) ; self._raw(on) m = self.cfg.servermodes if m: time.sleep(2) logging.debug("sending servermodes %s" % m) self._raw("MODE %s %s" % (self.cfg.nick, m)) def _resume(self, data, botname, reto=None): """ resume to server/port using nick. """ try: if data['ssl']: self.exit() time.sleep(3) self.start() return 1 except KeyError: pass self.stopped = False try: fd = int(data['fd']) except (KeyError, TypeError, ValueError): fd = None logging.error("%s - can't determine file descriptor" % self.cfg.name) print(data.tojson()) return 0 logging.warn("resume - file descriptor is %s (%s)" % (fd, data.name)) # create socket if self.cfg.ipv6: self.oldsock = socket.fromfd(fd , socket.AF_INET6, socket.SOCK_STREAM) else: self.oldsock = socket.fromfd(fd, socket.AF_INET, socket.SOCK_STREAM) assert self.oldsock self.oldsock.settimeout(30) self.fsock = self.oldsock.makefile("r") self.oldsock.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.sock = self.oldsock self.nickchanged = 0 self.connecting = False time.sleep(2) self._raw('PING :RESUME %s' % str(time.time())) self.dostart(self.cfg.name, self.type) self.connectok.set() self.connected = True self.reconnectcount = 0 if reto: self.say(reto, 'rebooting done') logging.warn("rebooting done (%s)" % self.cfg.name) return True def outnocb(self, printto, what, how='msg', *args, **kwargs): what = fix_format(what) what = self.normalize(what) if 'socket' in repr(printto) and self.sock: printto.send(bytearray(what + "\n", self.encoding or "utf-8")) return True if not printto: self._raw(what) elif how == 'notice': self.notice(printto, what) elif how == 'ctcp': self.ctcp(printto, what) else: self.privmsg(printto, what) def broadcast(self, txt): """ broadcast txt to all joined channels. """ for i in self.state['joinedchannels']: self.say(i, txt, speed=1) def normalize(self, what): txt = strippedtxt(what, ["\001", "\002", "\003"]) txt = txt.replace("", "\002") txt = txt.replace("", "\002") txt = txt.replace("", "\0032") txt = txt.replace("", "\003") txt = txt.replace("
  • ", "\0033*\003 ") txt = txt.replace("
  • ", "") txt = txt.replace("

    ", " - ") txt = txt.replace("
    ", " [!] ") txt = txt.replace("<b>", "\002") txt = txt.replace("</b>", "\002") txt = txt.replace("<i>", "\003") txt = txt.replace("</i>", "") txt = txt.replace("<h2>", "\0033") txt = txt.replace("</h2>", "\003") txt = txt.replace("<h3>", "\0034") txt = txt.replace("</h3>", "\003") txt = txt.replace("<li>", "\0034") txt = txt.replace("</li>", "\003") return txt def save(self): """ save state data. """ if self.state: self.state.save() def connect(self, reconnect=True): """ connect to server/port using nick .. connect can timeout so catch exception .. reconnect if enabled. """ res = self._connect() logging.info("%s - starting logon" % self.cfg.name) self.logon() time.sleep(1) self.nickchanged = 0 self.reconnectcount = 0 self._onconnect() self.connected = True self.connecting = False return res def shutdown(self): """ shutdown the bot. """ logging.warn('shutdown (%s)' % self.cfg.name) self.stopoutputloop = 1 #self.close() self.connecting = False self.connected = False self.connectok.clear() def close(self): """ close the connection. """ try: if 'ssl' in self.cfg and self.cfg['ssl']: self.oldsock.shutdown(2) else: self.sock.shutdown(2) except: pass try: if 'ssl' in self.cfg and self.cfg['ssl']: self.oldsock.close() else: self.sock.close() self.fsock.close() except: pass def handle_pong(self, ievent): """ set pongcheck on received pong. """ logging.debug('%s - received server pong' % self.cfg.name) self.pongcheck = 1 def sendraw(self, txt): """ send raw text to the server. """ if self.stopped: return logging.debug('%s - sending %s' % (self.cfg.name, txt)) self._raw(txt) def fakein(self, txt): """ do a fake ircevent. """ if not txt: return logging.debug('%s - fakein - %s' % (self.cfg.name, txt)) self.handle_ievent(IrcEvent().parse(self, txt)) def donick(self, nick, setorig=False, save=False, whois=False): """ change nick .. optionally set original nick and/or save to config. """ if not nick: return self.noauto433 = True nick = nick[:16] self._raw('NICK %s\n' % nick) self.noauto433 = False def join(self, channel, password=None): """ join channel with optional password. """ if not channel: return if password: self._raw('JOIN %s %s' % (channel, password)) else: self._raw('JOIN %s' % channel) if self.state: if channel not in self.state.data.joinedchannels: self.state.data.joinedchannels.append(channel) self.state.save() def part(self, channel): """ leave channel. """ if not channel: return self._raw('PART %s' % channel) try: self.state.data['joinedchannels'].remove(channel) self.state.save() except (KeyError, ValueError) as ex: logging.error("error removing %s from joinedchannels: %s" % (channel, str(ex))) if self.cfg.channels and channel in self.cfg.channels: self.cfg.channels.remove(channel) ; self.cfg.save() def who(self, who): """ send who query. """ if not who: return self.putonqueue(4, None, 'WHO %s' % who.strip()) def names(self, channel): """ send names query. """ if not channel: return self.putonqueue(4, None, 'NAMES %s' % channel) def whois(self, who): """ send whois query. """ if not who: return self.putonqueue(4, None, '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)) #@outlocked def send(self, txt): """ send text to irc server. """ if not txt: return if self.stopped: return try: #lock_object(self) now = time.time() txt = txt.rstrip() self._raw(txt) if self.cfg.sleepsec: timetosleep = self.cfg.sleepsec - (now - self.lastoutput) else: timetosleep = 4 - (now - self.lastoutput) if timetosleep > 0 and not self.cfg.nolimiter and not (time.time() - self.connecttime < 5): logging.debug('%s - flood protect' % self.cfg.name) time.sleep(timetosleep) except Exception as ex: logging.error('%s - send error: %s' % (self.cfg.name, str(ex))) handle_exception() #finally: release_object(self) def voice(self, channel, who): """ give voice. """ if not channel or not who: return self.putonqueue(9, None, '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://github.com/feedbackflow/fbfbot & http:///docs/fbfbot'): """ send quit message. """ logging.warn('sending quit - %s (%s)' % (reason, self.cfg.name)) self._raw('QUIT :%s' % reason) def notice(self, printto, what): """ send notice. """ if not printto or not what: return self.putonqueue(3, None, 'NOTICE %s :%s' % (printto, what)) def ctcp(self, printto, what): """ send ctcp privmsg. """ if not printto or not what: return self.putonqueue(3, None, "PRIVMSG %s :\001%s\001" % (printto, what)) def ctcpreply(self, printto, what): """ send ctcp notice. """ if not printto or not what: return self.putonqueue(3, None, "NOTICE %s :\001%s\001" % (printto, what)) def action(self, printto, what, event=None, *args, **kwargs): """ do action. """ if not printto or not what: return self.putonqueue(9, None, "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) logging.debug('%s - %s joined .. unignoring' % (self.cfg.name, ievent.nick)) ievent.bind(self) method = getattr(self,'handle_' + ievent.cmnd.lower()) if method: try: method(ievent) except: handle_exception() except AttributeError: pass 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: logging.debug('%s - using alternick %s' % (self.cfg.name, alternick)) self.donick(alternick) self.nickchanged = 1 return randomnick = getrandomnick() self._raw("NICK %s" % randomnick) self.cfg.wantnick = self.cfg.nick self.cfg.nick = randomnick logging.warn('ALERT: nick %s already in use/unavailable .. using randomnick %s (%s)' % (nick, randomnick, randomnick)) self.nickchanged = 1 def handle_ping(self, ievent): """ send pong response. """ if not ievent.txt: logging.debug("no txt set") ; return self._raw('PONG :%s' % ievent.txt) def handle_001(self, ievent): """ we are connected. """ time.sleep(1) self._onconnect() self.connectok.set() self.connected = True self.whois(self.cfg.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: from fbf.version import getversion self.say(ievent.nick, getversion(), None, 'notice') return 1 def handle_ctcp(self, ievent): """ handle client to client request .. version and ping. """ if ievent.txt.find('VERSION') != -1: from fbf.version import getversion self.ctcpreply(ievent.nick, 'VERSION %s' % getversion()) if ievent.txt.find('PING') != -1: try: pingtime = ievent.txt.split()[1] pingtifbf = ievent.txt.split()[2] if pingtime: self.ctcpreply(ievent.nick, 'PING ' + pingtime + ' ' + pingtifbf) except IndexError: pass def handle_error(self, ievent): """ show error. """ txt = ievent.txt if 'Closing' in txt: if "banned" in txt.lower() or "-lined" in txt.lower(): logging.error("WE ARE BANNED !! - %s - %s" % (self.cfg.server, txt)) self.exit() else: logging.error("%s - %s" % (self.cfg.name, txt)) else: logging.error("%s - %s - %s" % (self.cfg.name.upper(), ", ".join(ievent.arguments[1:]), txt)) def ping(self): """ ping the irc server. """ logging.debug('%s - sending ping' % self.cfg.name) try: self._raw('PING :%s' % self.cfg.server) return 1 except Exception as ex: logging.warn("can't send ping: %s (%s)" % (str(ex), self.cfg.name)) return 0 def handle_401(self, ievent): """ handle 401 .. nick not available. """ pass def handle_700(self, ievent): """ handle 700 .. encoding request of the server. """ try: self.encoding = ievent.arguments[1] logging.warn('700 encoding now is %s (%s)' % (self.encoding, self.cfg.name)) except: pass def handle_465(self, ievent): """ we are banned.. exit the bot. """ self.exit()