# mad/irc.py
#
#
""" IRC bot class. """
from .bot import Bot
from .event import Event
from .error import EDISCONNECT
from .handler import Handler
from .object import Object
from .space import cfg, fleet, kernel, partyline, users
from botlib import __version__
import logging
import _thread
import random
import socket
import queue
import time
import ssl
import re
import os
[docs]def init(event):
bot = IRC()
bot.start()
return bot
[docs]class IRC(Bot):
cc = "!"
default = ""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._buffer = []
self._handlers.register("004", self.connected)
self._handlers.register("005", self.h005)
self._handlers.register("ERROR", self.errored)
self._handlers.register("352", self.h352)
self._handlers.register("353", self.h353)
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._outqueue = queue.Queue()
self._sock = None
self._state.status = "running"
self._userhosts = Object()
self.started = False
self.launch(self.output)
[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
[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.server = self._bind()
self._cfg()
self._error.status = ""
[docs] def _cfg(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 self.cfg.ssl:
self._sock = ssl.wrap_socket(self._oldsock)
else:
self._sock = self._oldsock
self._sock.setblocking(self.blocking)
self._resume.fd = self._sock.fileno()
if cfg.resume:
os.set_inheritable(self._resume.fd, os.O_RDWR)
self.register_fd(self._resume.fd)
else:
self.register_fd(self._sock.fileno())
[docs] def _some(self):
if self.cfg.ssl:
inbytes = self._sock.read()
else:
inbytes = self._sock.recv(512)
txt = str(inbytes, self.cfg.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 announce(self, txt):
for channel in self.channels:
self._outqueue.put_nowait((channel, txt))
[docs] def close(self):
if 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()
return
for i in range(0, 4):
self._counter.connect += 1
try:
self._connect()
self.logon()
break
except BrokenPipeError as ex:
logging.error("connect error to %s: %s" % (self.cfg.server, str(ex)))
time.sleep(10.0 + self._counter.connect * 5)
continue
[docs] def dispatch(self, event):
event.parse()
for handler in self._handlers.get(event.command, []):
handler(event)
[docs] def event(self):
if not self._buffer:
self._some()
line = self._buffer.pop(0)
e = self.parsing(line.rstrip())
e.btype = self.type
return e
[docs] def join(self, channel, password=""):
if password:
self.raw('JOIN %s %s' % (channel, password))
else:
self.raw('JOIN %s' % channel)
if channel not in self.channels:
self.channels.append(channel)
[docs] def joinall(self):
if self.cfg.channel:
self.join(self.cfg.channel)
for channel in self.channels:
self.join(channel)
[docs] def logon(self, *args):
self.raw("NICK %s" % self.cfg.nick or "mad")
self.raw("USER %s localhost %s :%s" % (self.cfg.username,
self.cfg.server,
self.cfg.realname))
[docs] def out(self, channel, line):
for txt in split_txt(line, 450):
self.privmsg(channel, str(txt))
if time.time() - self._last < 3.5:
time.sleep(3.0)
[docs] def output(self):
self._connected.wait()
while self._state.status:
args = self._outqueue.get()
if not args or not self._state.status:
break
try:
self.out(*args)
except (EDISCONNECT,
BrokenPipeError,
ConnectionResetError) as ex:
logging.error("# disconnect #%s %s %s" % (self._counter.push, self.cfg.server, str(ex)))
self._error.status = str(ex)
self._counter.push += 1
time.sleep(10 + self._counter.push * 3.0)
[docs] def parsing(self, txt):
rawstr = str(txt)
object = Event()
object.txt = ""
object.server = self.cfg.server
object.cc = self.cc
object.id = self.id()
object.arguments = []
arguments = rawstr.split()
object.origin = arguments[0]
if object.origin.startswith(":"):
object.origin = object.origin[1:]
if len(arguments) > 1:
object.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:
object.arguments.append(arg)
object.txt = " ".join(txtlist)
else:
object.command = object.origin
object.origin = self.cfg.server
try:
object.nick, object.userhost = object.origin.split("!")
except:
pass
if object.arguments:
object.target = object.arguments[-1]
else:
object.target = ""
if object.target.startswith("#"):
object.channel = object.target
if not object.txt and len(arguments) == 1:
object.txt = arguments[1]
if not object.txt:
object.txt = rawstr.split(":")[-1]
object.id = self.id()
return object
[docs] def part(self, channel):
self.raw('PART %s' % channel)
if channel in self.channels:
self.channels.remove(channel)
self.save()
[docs] def raw(self, txt):
if self._error.status:
return
if self._status == "error":
return
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)
return
except (BrokenPipeError, ConnectionResetError):
pass
except AttributeError:
try:
self._sock.write(txt)
return
except (BrokenPipeError, ConnectionResetError):
return
self._connected.clear()
self._connected.wait()
self.launch(self.connect)
[docs] def register_fd(self, f):
try:
fd = f.fileno()
except:
fd = f
if not fd:
return fd
logging.warn("# engine on %s" % str(fd))
self._poll.register(fd)
self._resume.fd = fd
return fd
[docs] def resume(self):
fd = None
for bot in fleet:
if "type" in bot and bot["type"].lower() != self.id():
continue
try:
fd = bot["_resume"]["fd"]
except:
continue
self.channels = bot["channels"]
if not fd:
self.announce("resume failed")
return
logging.warn("# resume %s %s" % (fd, ",".join([str(x) for x in self.channels])))
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._cfg()
self.start()
self.announce("done")
[docs] def say(self, channel, txt):
self._outqueue.put_nowait((channel, txt))
[docs] def start(self, *args, **kwargs):
self.connect()
super().start(*args, **kwargs)
[docs] def stop(self, close=True):
super().stop()
self._state.status = ""
self._outqueue.put(None)
[docs] def noticed(self, event):
pass
[docs] def connected(self, event):
if "servermodes" in self.cfg:
self.raw("MODE %s %s" % (self.cfg.nick, self.cfg.servermodes))
logging.warn("# connected %s:%s" % (self.cfg.server, self.cfg.port))
self.joinall()
self._error.status = ""
self._connected.ready()
[docs] def invited(self, event):
self.join(event.channel)
[docs] def joined(self, event):
self.who(event.channel)
if event.channel not in self.channels:
self.channels.append(event.channel)
[docs] def errored(self, event):
self._error.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.nick == self.cfg.nick:
self.connect()
[docs] def privmsged(self, event):
if event.txt.startswith("\001DCC"):
self.dccconnect(event)
return
elif event.txt.startswith("\001VERSION"):
self.ctcpreply(event.nick, "VERSION BOTLIB #%s - http://pypi.python.org/pypi/botlib" % __version__)
return
kernel.put(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 h352(self, event):
args = event.arguments
self._userhosts[args[5]] = args[2] + "@" + args[3]
[docs] def h353(self, event):
pass
[docs] def h366(self, event):
pass
[docs] def h433(self, event):
self.donick(event.target + "_")
[docs] def h513(self, event):
self.raw("PONG %s" % event.txt.split()[-1])
[docs] def donick(self, name):
self.raw('NICK %s\n' % name[:16])
self.cfg.nick = name
self.sync()
[docs] def who(self, channel):
self.raw('WHO %s' % channel)
[docs] def names(self, channel):
self.raw('NAMES %s' % channel)
[docs] def whois(self, nick):
self.raw('WHOIS %s' % nick)
[docs] def privmsg(self, channel, txt):
self.raw('PRIVMSG %s :%s' % (channel, txt))
[docs] def voice(self, channel, nick):
self.raw('MODE %s +v %s' % (channel, nick))
[docs] def doop(self, channel, nick):
self.raw('MODE %s +o %s' % (channel, nick))
[docs] def delop(self, channel, nick):
self.raw('MODE %s -o %s' % (channel, nick))
[docs] def quit(self, reason='https://pikacode.com/bart/mad'):
self.raw('QUIT :%s' % reason)
[docs] def notice(self, channel, txt):
self.raw('NOTICE %s :%s' % (channel, txt))
[docs] def ctcp(self, nick, txt):
self.raw("PRIVMSG %s :\001%s\001" % (nick, txt))
[docs] def ctcpreply(self, channel, txt):
self.raw("NOTICE %s :\001%s\001" % (channel, txt))
[docs] def action(self, channel, txt):
self.raw("PRIVMSG %s :\001ACTION %s\001" % (channel, txt))
[docs] def getchannelmode(self, channel):
self.raw('MODE %s' % channel)
[docs] def settopic(self, channel, txt):
self.raw('TOPIC %s :%s' % (channel, txt))
[docs] def ping(self, txt):
self.raw('PING :%s' % txt)
[docs] def pong(self, txt):
self.raw('PONG :%s' % txt)
[docs] def dcced(self, event, s):
s.send(bytes('Welcome to BOTLIB ' + event.nick + " !!\n", self.cfg.encoding))
self.launch(self.dccloop, event, s)
[docs] def dccloop(self, event, s):
sockfile = s.makefile('rw')
s.setblocking(True)
self.register_fd(s.fileno())
partyline.register(event.origin, sockfile)
while 1:
try:
res = sockfile.readline()
if not res:
break
res = res.rstrip()
logging.info("< DCC %s %s" % (event.origin, res))
e = Event()
e._socket = sockfile
e.cc = ""
e.btype = "irc"
e.server = event.server
e.txt = res
e.origin = event.origin
e.parse()
kernel.put(e)
except socket.timeout:
time.sleep(0.01)
except socket.error as ex:
if ex.errno in [socket.EAGAIN, ]:
continue
else:
raise
sockfile.close()
del partyline[event.origin]
[docs] def dccconnect(self, event):
event.parse()
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))
self.dcced(event, s)
[docs]def split_txt(what, l=450):
z = ""
what = str(what)
for txt in what.split("\n"):
if len(txt) < l:
yield txt
else:
for y in txt.split():
if len(y) + len(z) < l:
z += " " + y
continue
yield z.strip()
z = ""
if z:
yield z.strip()