Source code for meds.bots.irc

# meds/bots/irc.py
#
#

""" class to implement an Internet Relay Chat (IRC) bot. """

from meds.utils.trace import get_exception
from meds.utils.misc import split_txt
from meds.errors import EDISCONNECT
from meds.utils.name import sname
from meds.storage import Storage
from meds.object import Object
from meds.utils.join import j
from meds.event import Event
from meds.bots import Bot

from meds import __version__

from meds.core import cfg, cmnds, kernel

import logging
import _thread
import select
import random
import socket
import queue
import time
import ssl
import re
import os

[docs]class IRC(Bot): cc = "!" default = "" def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._buffer = [] self._error = "" self._handlers.register("004", self.connected) self._handlers.register("005", self.h005) self._handlers.register("ERROR", self.errored) self._handlers.register("366", self.h366) self._handlers.register("433", self.h433) self._handlers.register("513", self.h513) self._handlers.register("PING", self.pinged) self._handlers.register("PONG", self.ponged) self._handlers.register("QUIT", self.quited) self._handlers.register("INVITE", self.invited) self._handlers.register("PRIVMSG", self.privmsged) self._handlers.register("NOTICE", self.noticed) self._handlers.register("JOIN", self.joined) self._last = time.time() self._lastline = "" self._lock = _thread.allocate_lock() self._nrconnect = 0 self._outqueue = queue.Queue() self._sock = None self._status = "start" self.channels = [] self.encoding = "utf-8" self.started = False self.launch(self.push) if self._cfg.channel: self.channels.append(self._cfg.channel)
[docs] def announce(self, txt): for channel in self.channels: self._outqueue.put((channel, txt))
[docs] def close(self): if 'ssl' in self and self['ssl']: self.oldsock.shutdown(1) ; self.oldsock.close() else: self._sock.shutdown(1) ; self._sock.close() self.fsock.close()
[docs] def connect(self): if cfg.resume: self.resume() else: while 1: self._nrconnect += 1 try: self._connect() self.logon() except (EDISCONNECT, BrokenPipeError, ConnectionResetError) as ex: logging.error("disconnect %s %s" % (self._cfg.server, str(ex))) time.sleep(self._nrconnect * 10.0) break
[docs] def dispatch(self, event): if event.command in self._handlers: for func in self._handlers[event.command]: func(event)
[docs] def event(self): if not self._buffer: self.some() line = self._buffer.pop(0) return self.parsing(line.rstrip())
[docs] def join(self, channel, password=""): if password: self.out('JOIN %s %s' % (channel, password)) else: self.out('JOIN %s' % channel)
[docs] def joinall(self): self.join(self._cfg.channel) for channel in self.channels: self.join(channel)
[docs] def logon(self, *args): self.out("NICK %s" % self._cfg.nick or "meds") self.out("USER %s localhost %s :%s" % (self._cfg.username, self._cfg.server, self._cfg.realname))
[docs] def out(self, txt): if not txt.endswith("\r\n"): txt += "\r\n" txt = txt[:512] txt = bytes(txt, "utf-8") logging.info("> %s" % txt) self._last = time.time() try: self._sock.send(txt) except AttributeError: self._sock.write(txt)
[docs] def output(self, channel, line): for txt in split_txt(line, 450): self.privmsg(channel, txt) if time.time() - self._last < 3.5: time.sleep(3.5)
[docs] def parsing(self, txt): rawstr = str(txt) obj = Event() obj.server = self._cfg.server obj.cc = self.cc obj.btype = sname(self) obj.arguments = [] arguments = rawstr.split() obj.origin = arguments[0] if obj.origin.startswith(":"): obj.origin = obj.origin[1:] if len(arguments) > 1: obj.command = arguments[1] if len(arguments) > 2: txtlist = [] adding = False for arg in arguments[2:]: if arg.startswith(":"): adding = True ; txtlist.append(arg[1:]) ; continue if adding: txtlist.append(arg) else: obj.arguments.append(arg) obj.txt = " ".join(txtlist) else: obj.command = obj.origin ; obj.origin = self._cfg.server try: obj.nick, obj.userhost = obj.origin.split("!") except: pass if obj.arguments: obj.target = obj.arguments[-1] else: obj.target = "" if obj.target.startswith("#"): obj.channel = obj.target if not obj.txt and len(arguments) == 1: obj.txt = arguments[1] if not obj.txt: obj.txt = rawstr.split(":")[-1] return obj
[docs] def part(self, channel): self.out('PART %s' % channel) if channel in self.channels: self.channels.remove(channel) self.save()
[docs] def prompt(self): pass
[docs] def push(self): while self._status: args = self._outqueue.get() if not args or not self._status: break try: self.output(*args) except (EDISCONNECT, BrokenPipeError, ConnectionResetError) as ex: logging.error("disconnect %s %s" % (self._cfg.server, str(ex))) self._nrconnect += 1 time.sleep(self._nrconnect * 3.0)
[docs] def register_fd(self, fd): self._poll.register(fd)
[docs] def resume(self): resume = Object().load(j(cfg.workdir, "resume")) for bot in resume.fleet: try: fd = int(bot["_resume"]["fd"]) ; break except: fd = None self._resume.fd = fd 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) self._config() self.announce("done")
[docs] def say(self, channel, txt): self._outqueue.put_nowait((channel, txt))
[docs] def some(self): if "ssl" in self and self.ssl: inbytes = self._sock.read() else: inbytes = self._sock.recv(512) txt = str(inbytes, self.encoding) if txt == "": raise EDISCONNECT() self._lastline += txt splitted = self._lastline.split("\r\n") for s in splitted[:-1]: self._buffer.append(s) if "PING" not in s or "PONG" not in s: logging.info("< %s" % s.strip()) self._lastline = splitted[-1]
[docs] def start(self): super().start() self.connect()
[docs] def stop(self, close=True): super().stop() self._status = "" self._outqueue.put(None)
## callbacks
[docs] def noticed(self, event): pass
[docs] def connected(self, event): if "servermodes" in self: self.out("MODE %s %s" % (self._cfg.nick, self._cfg.servermodes)) logging.warn("# connected %s:%s" % (self._cfg.server, self._cfg.port)) self.joinall()
[docs] def invited(self, event): self.join(event.channel)
[docs] def joined(self, event): if event.channel and event.channel not in self.channels: self.channels.append(event.channel)
[docs] def errored(self, event): self._status = event.txt logging.warn("# error %s" % event.txt)
[docs] def pinged(self, event): self.pongcheck = True self.pong(event.txt) self.ready()
[docs] def ponged(self, event): self.pongcheck = False
[docs] def quited(self, event): if "Ping timeout" in event.txt and event._cfg.nick == self._cfg.nick: self.connecting()
[docs] def privmsged(self, event): if event.txt.startswith("\001DCC"): self.dccconnect(event) return elif event.txt.startswith("\001VERSION"): self.ctcpreply(event.nick, "VERSION MEDS #%s - http://pypi.python.org/pypi/meds" % __version__) ; return super().dispatch(event)
[docs] def ctcped(self, event): pass
[docs] def h001(self, event): pass
[docs] def h002(self, event): pass
[docs] def h003(self, event): pass
[docs] def h004(self, event): pass
[docs] def h005(self, event): pass
[docs] def h366(self, event): pass
[docs] def h433(self, event): self.donick(event.target + "_")
[docs] def h513(self, event): self.out("PONG %s" % event.txt.split()[-1])
[docs] def donick(self, name): self.out('NICK %s\n' % name[:16]) self._cfg.nick = name self._cfg.sync()
[docs] def who(self, channel): self.out('WHO %s' % channel)
[docs] def names(self, channel): self.out('NAMES %s' % channel)
[docs] def whois(self, nick): self.out('WHOIS %s' % nick)
[docs] def privmsg(self, channel, txt): self.out('PRIVMSG %s :%s' % (channel, txt))
[docs] def voice(self, channel, nick): self.out('MODE %s +v %s' % (channel, nick))
[docs] def doop(self, channel, nick): self.out('MODE %s +o %s' % (channel, nick))
[docs] def delop(self, channel, nick): self.out('MODE %s -o %s' % (channel, nick))
[docs] def quit(self, reason='https://pikacode.com/bart/meds'): self.out('QUIT :%s' % reason)
[docs] def notice(self, channel, txt): self.out('NOTICE %s :%s' % (channel, txt))
[docs] def ctcp(self, nick, txt): self.out("PRIVMSG %s :\001%s\001" % (nick, txt))
[docs] def ctcpreply(self, channel, txt): self.out("NOTICE %s :\001%s\001" % (channel, txt))
[docs] def action(self, channel, txt): self.out("PRIVMSG %s :\001ACTION %s\001" % (channel, txt))
[docs] def getchannelmode(self, channel): self.out('MODE %s' % channel)
[docs] def settopic(self, channel, txt): self.out('TOPIC %s :%s' % (channel, txt))
[docs] def ping(self, txt): self.out('PING :%s' % txt)
[docs] def pong(self, txt): self.out('PONG :%s' % txt)
[docs] def dcced(self, event, s): s.send(bytes('Welcome to MEDS ' + event.nick + " !!\n", self.encoding)) self.launch(self.dccloop, event, s)
[docs] def dccloop(self, event, s): sockfile = s.makefile('rw') #s.setblocking(True) self.register_fd(s) while 1: try: res = sockfile.readline() if not res: break res = res.rstrip() logging.info("< %s %s" % (event.origin, res)) e = Event() e.btype = "DCC" e.txt = res e.outer = sockfile e.origin = event.origin super().dispatch(e) except socket.timeout: time.sleep(0.01) except socket.error as ex: if ex.errno in [socket.EAGAIN, ]: continue else: raise except Exception as ex: logging.error(get_exception()) sockfile.close()
[docs] def dccconnect(self, event): event.parse() try: addr = event._parsed.args[2] ; port = event._parsed.args[3][:-1] port = int(port) if re.search(':', addr): s = socket.socket(socket.AF_INET6, socket.SOCK_STREAM) else: s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect((addr, port)) except Exception as ex: logging.error(get_exception()) ; return self.dcced(event, s)
[docs] def _connect(self): self.stopped = False 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) self._cfg.server = self._bind() self._config()
[docs] def _config(self): self.blocking = True self._oldsock.setblocking(self.blocking) self._oldsock.settimeout(60.0) if not cfg.resume: logging.info("! connect %s:%s" % (self._cfg.server, self._cfg.port or 6667)) self._oldsock.connect((self._cfg.server, int(str(self._cfg.port or 6667)))) self._oldsock.setblocking(self.blocking) self._oldsock.settimeout(700.0) self.fsock = self._oldsock.makefile("r") if 'ssl' in self and self['ssl']: self._sock = ssl.wrap_socket(self._oldsock) else: self._sock = self._oldsock self._sock.setblocking(self.blocking) self._resume.fd = self._sock.fileno() os.set_inheritable(self._resume.fd, os.O_RDWR) self.register_fd(self._resume.fd)
[docs] def _bind(self): server = self._cfg.server try: self._oldsock.bind((server, 0)) except socket.error: 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