Source code for president.modules.irc

# This file is placed in the Public Domain.
#
# pylint: disable=C,I,R,W0401,W0622,W0613
# flake8: noqa=C901


"internet relay chat"


import base64
import os
import queue
import random
import socket
import ssl
import time
import textwrap
import threading
import _thread


from ..bus     import Bus
from ..command import Command
from ..error   import Error
from ..event   import Event
from ..object  import Default, Object
from ..object  import edit, keys, printable, update
from ..persist import Persist, find, fntime, last, write
from ..reactor import Reactor
from ..thread  import launch
from ..utils   import laps


NAME = __file__.split(os.sep)[-3]


saylock = _thread.allocate_lock()


[docs]def start(): irc = IRC() irc.start() irc.events.joined.wait() return irc
[docs]def stop(): for bot in Bus.objs: if "IRC" in str(type(bot)): bot.stop()
[docs]class NoUser(Exception): pass
[docs]class Config(Object): channel = f'#{NAME}' control = '!' edited = time.time() nick = NAME nocommands = False password = '' port = 6667 realname = NAME sasl = False server = 'localhost' servermodes = '' sleep = 60 username = NAME users = False verbose = False def __init__(self): Object.__init__(self) self.channel = Config.channel self.nick = Config.nick self.port = Config.port self.realname = Config.realname self.server = Config.server self.username = Config.username def __edited__(self): return Config.edited def __size__(self): return len(Config)
Persist.add(Config)
[docs]class TextWrap(textwrap.TextWrapper): def __init__(self): super().__init__() self.break_long_words = False self.drop_whitespace = True self.fix_sentence_endings = True self.replace_whitespace = True self.tabsize = 4 self.width = 450
[docs]class Output(Object): cache = Object() def __init__(self): Object.__init__(self) self.oqueue = queue.Queue() self.dostop = threading.Event()
[docs] def dosay(self, channel, txt): raise NotImplementedError
[docs] def extend(self, channel, txtlist): if channel not in self.cache: setattr(self.cache, channel, []) cache = getattr(self.cache, channel, None) cache.extend(txtlist)
[docs] def gettxt(self, channel): txt = None try: cache = getattr(self.cache, channel, None) txt = cache.pop(0) except (KeyError, IndexError): pass return txt
[docs] def oput(self, channel, txt): if channel not in self.cache: setattr(self.cache, channel, []) self.oqueue.put_nowait((channel, txt))
[docs] def output(self): while not self.dostop.is_set(): (channel, txt) = self.oqueue.get() if channel is None and txt is None: break if self.dostop.is_set(): break wrapper = TextWrap() try: txtlist = wrapper.wrap(txt) except AttributeError: continue if len(txtlist) > 3: self.extend(channel, txtlist) length = len(txtlist) self.dosay( channel, f"use !mre to show more (+{length})" ) continue _nr = -1 for txt in txtlist: _nr += 1 self.dosay(channel, txt)
[docs] def size(self, chan): if chan in self.cache: return len(getattr(self.cache, chan, [])) return 0
[docs] def start(self): self.dostop.clear() launch(self.output) return self
[docs] def stop(self): self.dostop.set() self.oqueue.put_nowait((None, None))
[docs]class IRC(Reactor, Output): def __init__(self): Reactor.__init__(self) Output.__init__(self) self.buffer = [] self.cfg = Config() self.events = Default() self.events.authed = threading.Event() self.events.connected = threading.Event() self.events.joined = threading.Event() self.channels = [] self.sock = None self.state = Default() self.state.keeprunning = False self.state.nrconnect = 0 self.state.nrsend = 0 self.zelf = '' self.register('903', cb_h903) self.register('904', cb_h903) self.register('AUTHENTICATE', cb_auth) self.register('CAP', cb_cap) self.register('ERROR', cb_error) self.register('LOG', cb_log) self.register('NOTICE', cb_notice) self.register('PRIVMSG', cb_privmsg) self.register('QUIT', cb_quit) Bus.add(self)
[docs] def announce(self, txt): for channel in self.channels: self.say(channel, txt)
[docs] def command(self, cmd, *args): with saylock: if not args: self.raw(cmd) elif len(args) == 1: self.raw(f'{cmd.upper()} {args[0]}') elif len(args) == 2: txt = ' '.join(args[1:]) self.raw(f'{cmd.upper()} {args[0]} :{txt}') elif len(args) >= 3: txt = ' '.join(args[2:]) self.raw("{cmd.upper()} {args[0]} {args[1]} :{txt}") if (time.time() - self.state.last) < 5.0: time.sleep(5.0) self.state.last = time.time()
[docs] def connect(self, server, port=6667): self.state.nrconnect += 1 self.events.connected.clear() Error.debug(f"connecting to {server}:{port}") if self.cfg.password: Error.debug("using SASL") self.cfg.sasl = True self.cfg.port = "6697" ctx = ssl.SSLContext(ssl.PROTOCOL_TLS) ctx.check_hostname = False sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.sock = ctx.wrap_socket(sock) self.sock.connect((server, port)) time.sleep(1.0) self.command('CAP LS 302') else: addr = socket.getaddrinfo(server, port, socket.AF_INET)[-1][-1] self.sock = socket.create_connection(addr) self.events.authed.set() if self.sock: os.set_inheritable(self.sock.fileno(), os.O_RDWR) self.sock.setblocking(True) self.sock.settimeout(180.0) self.events.connected.set() return True return False
[docs] def direct(self, txt): Error.debug(txt) self.sock.send(bytes(txt.rstrip()+'\r\n', 'utf-8'))
[docs] def disconnect(self): try: self.sock.shutdown(2) except ( ssl.SSLError, OSError, BrokenPipeError ) as ex: Error.errors.append(ex)
[docs] def doconnect(self, server, nck, port=6667): while 1: try: if self.connect(server, port): break except ( ssl.SSLError, OSError, ConnectionResetError ) as ex: self.state.errors = str(ex) Error.debug(str(ex)) Error.debug(f"sleeping {self.cfg.sleep} seconds") time.sleep(self.cfg.sleep)
#self.logon(server, nck)
[docs] def dosay(self, channel, txt): self.events.joined.wait() #txt = str(txt).replace('\n', '') #txt = txt.replace(' ', ' ') self.command('PRIVMSG', channel, txt)
[docs] def event(self, txt): evt = self.parsing(txt) cmd = evt.command if cmd == "020": self.logon(self.cfg.server, self.cfg.nick) if cmd == 'PING': self.state.pongcheck = True self.command('PONG', evt.txt or '') elif cmd == 'PONG': self.state.pongcheck = False if cmd == '001': self.state.needconnect = False if self.cfg.servermodes: self.command(f'MODE {self.cfg.nick} {self.cfg.servermodes}') self.zelf = evt.args[-1] elif cmd == "376": self.joinall() elif cmd == '002': self.state.host = evt.args[2][:-1] elif cmd == '366': self.state.errors = [] self.events.joined.set() elif cmd == '433': self.state.errors = txt nck = self.cfg.nick + '_' + str(random.randint(1, 10)) self.command('NICK', nck) return evt
[docs] def joinall(self): for channel in self.channels: self.command('JOIN', channel)
[docs] def keep(self): while 1: self.events.connected.wait() self.events.authed.wait() self.state.keeprunning = True time.sleep(self.cfg.sleep) self.state.pongcheck = True self.command('PING', self.cfg.server) if self.state.pongcheck: Error.debug("failed pongcheck, restarting") self.state.pongcheck = False self.state.keeprunning = False self.events.connected.clear() self.stop() start() break
[docs] def logon(self, server, nck): self.events.connected.wait() self.events.authed.wait() nck = self.cfg.username self.direct(f'NICK {nck}') self.direct(f'USER {nck} {server} {server} {nck}')
[docs] def parsing(self, txt): rawstr = str(txt) rawstr = rawstr.replace('\u0001', '') rawstr = rawstr.replace('\001', '') Error.debug(txt) obj = Event() obj.rawstr = rawstr obj.command = '' obj.arguments = [] arguments = rawstr.split() if arguments: obj.origin = arguments[0] else: obj.origin = self.cfg.server if obj.origin.startswith(':'): obj.origin = obj.origin[1:] if len(arguments) > 1: obj.command = arguments[1] obj.type = obj.command if len(arguments) > 2: txtlist = [] adding = False for arg in arguments[2:]: if arg.count(':') <= 1 and 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.origin = obj.origin.split('!') except ValueError: obj.nick = '' target = '' if obj.arguments: target = obj.arguments[0] if target.startswith('#'): obj.channel = target else: obj.channel = obj.nick if not obj.txt: obj.txt = rawstr.split(':', 2)[-1] if not obj.txt and len(arguments) == 1: obj.txt = arguments[1] spl = obj.txt.split() if len(spl) > 1: obj.args = spl[1:] if obj.args: obj.rest = " ".join(obj.args) obj.orig = repr(self) obj.txt = obj.txt.strip() obj.type = obj.command return obj
[docs] def poll(self): self.events.connected.wait() if not self.buffer: try: self.some() except BlockingIOError as ex: time.sleep(1.0) return self.event(str(ex)) except ( OSError, socket.timeout, ssl.SSLError, ssl.SSLZeroReturnError, ConnectionResetError, BrokenPipeError ) as ex: Error.errors.append(ex) self.stop() Error.debug("handler stopped") return self.event(str(ex)) try: txt = self.buffer.pop(0) except IndexError: txt = "" return self.event(txt)
[docs] def raw(self, txt): txt = txt.rstrip() Error.debug(txt) if not txt.endswith('\r\n'): txt += '\r\n' txt = txt[:512] txt += '\n' txt = bytes(txt, 'utf-8') if self.sock: try: self.sock.send(txt) except ( OSError, ssl.SSLError, ssl.SSLZeroReturnError, ConnectionResetError, BrokenPipeError ) as ex: Error.errors.append(ex) self.stop() return self.state.last = time.time() self.state.nrsend += 1
[docs] def reconnect(self): Error.debug(f"reconnecting to {self.cfg.server}") try: self.disconnect() except (ssl.SSLError, OSError): pass self.events.connected.clear() self.events.joined.clear() self.doconnect(self.cfg.server, self.cfg.nick, int(self.cfg.port))
[docs] def say(self, channel, txt): self.oput(channel, txt)
[docs] def some(self): self.events.connected.wait() if not self.sock: return inbytes = self.sock.recv(512) txt = str(inbytes, 'utf-8') if txt == '': raise ConnectionResetError self.state.lastline += txt splitted = self.state.lastline.split('\r\n') for line in splitted[:-1]: self.buffer.append(line) self.state.lastline = splitted[-1]
[docs] def start(self): last(self.cfg) if self.cfg.channel not in self.channels: self.channels.append(self.cfg.channel) self.events.connected.clear() self.events.joined.clear() Reactor.start(self) Output.start(self) launch( self.doconnect, self.cfg.server or "localhost", self.cfg.nick, int(self.cfg.port or '6667') ) if not self.state.keeprunning: launch(self.keep)
[docs] def stop(self): Bus.remove(self) Reactor.stop(self) Output.stop(self) self.disconnect()
[docs]class User(Object): def __init__(self, val=None): Object.__init__(self) self.user = '' self.perms = [] if val: update(self, val)
[docs] def isok(self): return True
[docs] def isthere(self): return True
Persist.add(User)
[docs]class Users(Object):
[docs] @staticmethod def allowed(origin, perm): perm = perm.upper() user = Users.get_user(origin) val = False if user and perm in user.perms: val = True return val
[docs] @staticmethod def delete(origin, perm): res = False for user in Users.get_users(origin): try: user.perms.remove(perm) write(user) res = True except ValueError: pass return res
[docs] @staticmethod def get_users(origin=''): selector = {'user': origin} return find('user', selector)
[docs] @staticmethod def get_user(origin): users = list(Users.get_users(origin)) res = None if len(users) > 0: res = users[-1] return res
[docs] @staticmethod def perm(origin, permission): user = Users.get_user(origin) if not user: raise NoUser(origin) if permission.upper() not in user.perms: user.perms.append(permission.upper()) write(user) return user
# CALLBACKS
[docs]def cb_auth(evt): bot = Bus.byorig(evt.orig) assert bot.cfg.password bot.command(f'AUTHENTICATE {bot.cfg.password}')
[docs]def cb_cap(evt): bot = Bus.byorig(evt.orig) if bot.cfg.password and 'ACK' in evt.arguments: bot.command('AUTHENTICATE PLAIN') else: bot.command('CAP REQ :sasl')
[docs]def cb_command(evt): Command.handle(evt)
[docs]def cb_error(evt): bot = Bus.byorig(evt.orig) bot.state.nrerror += 1 bot.state.errors.append(evt.txt) Error.debug(evt.txt)
[docs]def cb_h903(evt): assert evt bot = Bus.byorig(evt.orig) bot.command('CAP END') bot.events.authed.set()
[docs]def cb_h904(evt): assert evt bot = Bus.byorig(evt.orig) bot.command('CAP END') bot.events.authed.set()
[docs]def cb_kill(evt): pass
[docs]def cb_log(evt): pass
[docs]def cb_001(evt): bot = Bus.byorig(evt.orig) bot.logon()
[docs]def cb_notice(evt): bot = Bus.byorig(evt.orig) if evt.txt.startswith('VERSION'): txt = f'\001VERSION {NAME.upper()} 140 - {bot.cfg.username}\001' bot.command('NOTICE', evt.channel, txt)
[docs]def cb_privmsg(evt): bot = Bus.byorig(evt.orig) if bot.cfg.nocommands: return if evt.txt: if evt.txt[0] in ['!',]: evt.txt = evt.txt[1:] elif evt.txt.startswith(f'{bot.cfg.nick}:'): evt.txt = evt.txt[len(bot.cfg.nick)+1:] else: return if evt.txt: evt.txt = evt.txt[0].lower() + evt.txt[1:] if bot.cfg.users and not Users.allowed(evt.origin, 'USER'): return Error.debug(f"command from {evt.origin}: {evt.txt}") Command.handle(evt)
[docs]def cb_quit(evt): bot = Bus.byorig(evt.orig) Error.debug(f"quit from {bot.cfg.server}") if evt.orig and evt.orig in bot.zelf: bot.stop()
# COMMANDS
[docs]def cfg(event): config = Config() last(config) if not event.sets: event.reply( printable( config, keys(config), skip='control,password,realname,sleep,username' ) ) else: edit(config, event.sets) write(config) event.reply('ok')
[docs]def dlt(event): if not event.args: event.reply('dlt <username>') return selector = {'user': event.args[0]} for obj in find('user', selector): obj.__deleted__ = True write(obj) event.reply('ok') break
[docs]def met(event): if not event.args: nmr = 0 for obj in find('user'): lap = laps(time.time() - fntime(obj.__fnm__)) event.reply(f'{nmr} {obj.user} {obj.perms} {lap}s') nmr += 1 if not nmr: event.reply('no user') return user = User() user.user = event.rest user.perms = ['USER'] write(user) event.reply('ok')
[docs]def mre(event): if not event.channel: event.reply('channel is not set.') return bot = event.bot() if 'cache' not in dir(bot): event.reply('bot is missing cache') return if event.channel not in bot.cache: event.reply(f'no output in {event.channel} cache.') return for _x in range(3): txt = bot.gettxt(event.channel) if txt: bot.say(event.channel, txt) size = bot.size(event.channel) event.reply(f'{size} more in cache')
[docs]def pwd(event): if len(event.args) != 2: event.reply('pwd <nick> <password>') return arg1 = event.args[0] arg2 = event.args[1] txt = f'\x00{arg1}\x00{arg2}' enc = txt.encode('ascii') base = base64.b64encode(enc) dcd = base.decode('ascii') event.reply(dcd)