.. _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("