an Irc object handles the connection to the irc server .. receiving, sending, connect and reconnect code.
Bases: fbf.lib.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.
do a fake ircevent.
we are connected.
handle 401 .. nick not available.
erroneous nick.
handle nick already taken.
we are banned.. exit the bot.
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.
log on to the network.
send names query.
send notice.
leave channel.
ping the irc server.
send privmsg to irc server.
send quit message.
save state data.
send text to irc server.
send raw text to the server.
shutdown the bot.
give voice.
send who query.
send whois query.
# fbf/socklib/irc/irc.py # # """ an Irc object handles the connection to the irc server .. receiving, sending, connect and reconnect code. """
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
from .ircevent import IrcEvent
import time import _thread import socket import threading import os import queue import random import logging import types import re import select
outlock = _thread.allocate_lock() outlocked = lockdec(outlock)
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("<b>", "\002") txt = txt.replace("</b>", "\002") txt = txt.replace("<i>", "\0032") txt = txt.replace("</i>", "\003") txt = txt.replace("<li>", "\0033*\003 ") txt = txt.replace("</li>", "") txt = txt.replace("<br><br>", " - ") txt = txt.replace("<br>", " [!] ") 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()