# 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 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