From 959c0d7def8d82ddb84fc66e01617e600cd171c5 Mon Sep 17 00:00:00 2001 From: decoded Date: Sat, 27 Aug 2022 06:58:33 -0500 Subject: [PATCH] v2.666-3 --- .../plugins/command_plugin.py | 528 ++++++++++++++++++ hydra_core__standalone/plugins/core_plugin.py | 118 ++++ hydra_core__standalone/plugins/fifo_plugin.py | 101 ++++ .../plugins/net_hydra_plugin.py | 165 ++++++ .../plugins/sasl_custom_plugin.py | 48 ++ .../plugins/storage_plugin.py | 293 ++++++++++ 6 files changed, 1253 insertions(+) create mode 100644 hydra_core__standalone/plugins/command_plugin.py create mode 100644 hydra_core__standalone/plugins/core_plugin.py create mode 100644 hydra_core__standalone/plugins/fifo_plugin.py create mode 100644 hydra_core__standalone/plugins/net_hydra_plugin.py create mode 100644 hydra_core__standalone/plugins/sasl_custom_plugin.py create mode 100644 hydra_core__standalone/plugins/storage_plugin.py diff --git a/hydra_core__standalone/plugins/command_plugin.py b/hydra_core__standalone/plugins/command_plugin.py new file mode 100644 index 0000000..e5f974d --- /dev/null +++ b/hydra_core__standalone/plugins/command_plugin.py @@ -0,0 +1,528 @@ +# -*- coding: utf-8 -*- +from irc3.compat import asyncio +from irc3 import utils +from collections import defaultdict +import functools +import venusian +import fnmatch +import logging +import docopt +import shlex +import irc3 +import sys +import re +__doc__ = ''' +========================================== +:mod:`irc3.plugins.command` Command plugin +========================================== + +Introduce a ``@command`` decorator + +The decorator use `docopts `_ to parse command arguments. + +Usage +===== + +Create a python module with some commands: + +.. literalinclude:: ../../examples/mycommands.py + +.. + >>> import sys + >>> sys.path.append('examples') + >>> from irc3.testing import IrcBot + >>> from irc3.testing import ini2config + +And register it:: + + >>> bot = IrcBot() + >>> bot.include('irc3.plugins.command') # register the plugin + >>> bot.include('mycommands') # register your commands + + +Check the result:: + + >>> bot.test(':gawel!user@host PRIVMSG #chan :!echo foo') + PRIVMSG #chan :foo + +In the docstring, ``%%`` is replaced by the command character. ``!`` by +default. You can override it by passing a ``cmd`` parameter to bot's config. + +When a command is not public, you can't use it on a channel:: + + >>> bot.test(':gawel!user@host PRIVMSG #chan :!adduser foo pass') + PRIVMSG gawel :You can only use the 'adduser' command in private. + +If a command is tagged with ``show_in_help_list=False``, it won't be shown +on the result of ``!help``. + + >>> bot.test(':gawel!user@host PRIVMSG #chan :!help') + PRIVMSG #chan :Available commands: !adduser, !echo, !help + +View extra info about a command by specifying it to ``!help``. + + >>> bot.test(':gawel!user@host PRIVMSG #chan :!help echo') + PRIVMSG #chan :Echo command + PRIVMSG #chan :!echo ... + >>> bot.test(':gawel!user@host PRIVMSG #chan :!help nonexistant') + PRIVMSG #chan :No such command. Try !help for an overview of all commands. + +Guard +===== + +You can use a guard to prevent untrusted users to run some commands. The +:class:`free_policy` is used by default. + +There is two builtin policy: + +.. autoclass:: free_policy + + +.. autoclass:: mask_based_policy + +Mask based guard using permissions:: + + >>> config = ini2config(""" + ... [bot] + ... nick = nono + ... includes = + ... irc3.plugins.command + ... mycommands + ... [irc3.plugins.command] + ... guard = irc3.plugins.command.mask_based_policy + ... [irc3.plugins.command.masks] + ... gawel!*@* = all_permissions + ... foo!*@* = help + ... """) + >>> bot = IrcBot(**config) + +foo is allowed to use command without permissions:: + + >>> bot.test(':foo!u@h PRIVMSG nono :!echo got the power') + PRIVMSG foo :got the power + +foo is not allowed to use command except those with the help permission:: + + >>> bot.test(':foo!u@h PRIVMSG nono :!ping') + PRIVMSG foo :You are not allowed to use the 'ping' command + +gawel is allowed:: + + >>> bot.test(':gawel!u@h PRIVMSG nono :!ping') + NOTICE gawel :PONG gawel! + +Async commands +============== + +Commands can be coroutines: + +.. literalinclude:: ../../examples/async_command.py + :language: py + +Available options +================= + +The plugin accept the folowing options: + +.. code-block:: ini + + [irc3.plugins.command] + cmd = ! + use_shlex = true + antiflood = true + casesensitive = true + guard = irc3.plugins.command.mask_based_policy + + +Command arguments +================= + +The :func:`command` decorator accept the folowing arguments: + +**name**: if set, use this name as the command name instead of the function +name. + +**permission**: if set, this permission will be required to run the command. +See Guard section + +**use_shlex**: if `False`, do not use `shlex` to parse command line. + +**options_first**: if `True` use docopt's options_first options. Allow to have +args that starts with `-` as arguments. + +**error_format**: allow to customize error messages. must be a callable that +accept keyword arguments `cmd`, `args` and `exc`. +For example, `error_format="Error for {cmd}".format` will work. + +**quiet**: if `True` don't show errors + +**aliases**: this argument, when present, should be a list of strings. All +those strings will become alternative command names (i.e. aliases). +For example, command 'mycmd' with aliases=['theircmd', 'noonescmd'] could +be called via all three names. + + +''' + + +class free_policy: + """Default policy""" + def __init__(self, bot): + self.context = bot + + def __call__(self, predicates, meth, client, target, args, **kwargs): + return meth(client, target, args) + + +class mask_based_policy: + """Allow only valid masks. Able to take care or permissions""" + + key = __name__ + '.masks' + + def __init__(self, bot): + self.context = bot + self.log = logging.getLogger(__name__) + self.log.debug('Masks: %r', self.masks) + + @property + def masks(self): + masks = self.context.config[self.key] + if hasattr(self.context, 'db'): + # update config with storage values + try: + value = self.context.db[self] + except KeyError: + pass + else: + if isinstance(value, dict): + masks.update(value) + return masks + + def has_permission(self, mask, permission): + if permission is None: + return True + for pattern in self.masks: + if fnmatch.fnmatch(mask, pattern): + if not isinstance(self.masks, dict): + return True + perms = self.masks[pattern] + if permission in perms or 'all_permissions' in perms: + return True + return False + + def __call__(self, predicates, meth, client, target, args, **kwargs): + if self.has_permission(client, predicates.get('permission')): + return meth(client, target, args) + cmd_name = predicates.get('name', meth.__name__) + self.context.privmsg( + client.nick, + 'You are not allowed to use the %r command' % cmd_name) + + +def attach_command(func, depth=2, **predicates): + commands = predicates.pop('commands', + 'irc3.plugins.command.Commands') + category = predicates.pop('venusian_category', + 'irc3.plugins.command') + + def callback(context, name, ob): + obj = context.context + if info.scope == 'class': + callback = func.__get__(obj.get_plugin(ob), ob) + else: + callback = utils.wraps_with_context(func, obj) + plugin = obj.get_plugin(utils.maybedotted(commands)) + predicates.update(module=func.__module__) + cmd_name = predicates.get('name', func.__name__) + if not plugin.case_sensitive: + cmd_name = cmd_name.lower() + plugin[cmd_name] = (predicates, callback) + aliases = predicates.get('aliases', None) + if aliases is not None: + for alias in aliases: + plugin.aliases[alias] = cmd_name + obj.log.debug('Register command %r %r', cmd_name, aliases) + else: + obj.log.debug('Register command %r', cmd_name) + info = venusian.attach(func, callback, + category=category, depth=depth) + + +def command(*func, **predicates): + if func: + func = func[0] + attach_command(func, **predicates) + return func + else: + def wrapper(func): + attach_command(func, **predicates) + return func + return wrapper + + +@irc3.plugin +class Commands(dict): + + __reloadable__ = False + + requires = [ + __name__.replace('command', 'core'), + ] + default_policy = free_policy + case_sensitive = False + + def __init__(self, context): + self.context = context + module = self.__class__.__module__ + self.config = config = context.config.get(module, {}) + self.log = logging.getLogger(module) + self.log.debug('Config: %r', config) + + if 'cmd' in context.config: # in case of + config['cmd'] = context.config['cmd'] + context.config['cmd'] = self.cmd = config.get('cmd', '!') + context.config['re_cmd'] = re.escape(self.cmd) + + self.use_shlex = self.config.get('use_shlex', True) + self.antiflood = self.config.get('antiflood', False) + self.case_sensitive = self.config.get('casesensitive', + self.case_sensitive) + + guard = utils.maybedotted(config.get('guard', self.default_policy)) + self.log.debug('Guard: %s', guard.__name__) + self.guard = guard(context) + + self.error_format = utils.maybedotted(config.get('error_format', + "Invalid arguments.".format)) + self.handles = defaultdict(Done) + self.tasks = defaultdict(Done) + + self.aliases = {} + + def split_command(self, data, use_shlex=None): + if not data: + return [] + return shlex.split(data) if use_shlex else data.split(' ') + + @irc3.event((r'(@(?P\S+) )?:(?P\S+) PRIVMSG (?P\S+) ' + r':{re_cmd}(?P[\w-]+)(\s+(?P\S.*)|(\s*$))')) + def on_command(self, cmd, mask=None, target=None, client=None, **kw): + if not self.case_sensitive: + cmd = cmd.lower() + cmd = self.aliases.get(cmd, cmd) + predicates, meth = self.get(cmd, (None, None)) + if meth is not None: + if predicates.get('public', True) is False and target.is_channel: + self.context.privmsg( + mask.nick, + 'You can only use the %r command in private.' % str(cmd)) + else: + return self.do_command(predicates, meth, mask, target, **kw) + + def do_command(self, predicates, meth, client, target, data=None, **kw): + nick = self.context.nick or '-' + to = client.nick if target == nick else target + doc = meth.__doc__ or '' + doc = [line.strip() for line in doc.strip().split('\n')] + doc = [nick + ' ' + line.strip('%%') + for line in doc if line.startswith('%%')] + doc = 'Usage:' + '\n ' + '\n '.join(doc) + if data: + if not isinstance(data, str): # pragma: no cover + encoding = self.context.encoding + data = data.encode(encoding) + try: + data = self.split_command( + data, use_shlex=predicates.get('use_shlex', self.use_shlex)) + except ValueError as e: + if not predicates.get('quiet', False): + self.context.privmsg(to, 'Invalid arguments: {}.'.format(e)) + return + docopt_args = dict(help=False) + if "options_first" in predicates: + docopt_args.update(options_first=predicates["options_first"]) + cmd_name = predicates.get('name', meth.__name__) + try: + args = docopt.docopt(doc, [cmd_name] + data, **docopt_args) + except docopt.DocoptExit as exc: + if not predicates.get('quiet', False): + args = {'cmd': cmd_name, 'args': data, + 'args_str': " ".join(data), 'exc': exc} + error_format = predicates.get('error_format', + self.error_format) + self.context.privmsg(to, error_format(**args)) + else: + uid = (cmd_name, to) + use_client = isinstance(client, asyncio.Protocol) + if not self.tasks[uid].done(): + self.context.notice( + client if use_client else client.nick, + "Another task is already running. " + "Please be patient and don't flood me", nowait=True) + elif not self.handles[uid].done() and self.antiflood: + self.context.notice( + client if use_client else client.nick, + "Please be patient and don't flood me", nowait=True) + else: + # get command result + res = self.guard(predicates, meth, client, target, args=args) + + callback = functools.partial(self.command_callback, uid, to) + if res is not None: + coros = ( + asyncio.iscoroutinefunction(meth), + asyncio.iscoroutinefunction(self.guard.__call__) + ) + if any(coros): + task = asyncio.ensure_future( + res, loop=self.context.loop) + # use a callback if command is a coroutine + task.add_done_callback(callback) + self.tasks[uid] = task + return task + else: + # no callback needed + callback(res) + + def command_callback(self, uid, to, msgs): + if isinstance(msgs, asyncio.Future): # pragma: no cover + msgs = msgs.result() + if msgs is not None: + def iterator(msgs): + for msg in msgs: + yield to, msg + if isinstance(msgs, str): + msgs = [msgs] + handle = self.context.call_many('privmsg', iterator(msgs)) + if handle is not None: + self.handles[uid] = handle + + @command + def help(self, mask, target, args): + """Show help + + %%help [] + """ + return "not today stan" + if args['']: + args = args[''] + # Strip out self.context.config.cmd from args so we can use + # both !help !foo and !help foo + if args.startswith(self.context.config.cmd): + args = args[len(self.context.config.cmd):] + args = self.aliases.get(args, args) + predicates, meth = self.get(args, (None, None)) + if meth is not None: + doc = meth.__doc__ or '' + doc = [ + line.strip() for line in doc.split('\n') + if line.strip() + ] + buf = '' + for line in doc: + if '%%' not in line and buf is not None: + buf += line + ' ' + else: + if buf is not None: + for b in utils.split_message(buf, 160): + yield b + buf = None + line = line.replace('%%', self.context.config.cmd) + yield line + aliases = predicates.get('aliases', None) + if aliases is not None: + yield 'Aliases: {0}'.format(','.join(sorted(aliases))) + else: + yield ('No such command. Try %shelp for an ' + 'overview of all commands.' + % self.context.config.cmd) + else: + cmds = sorted((k for (k, (p, m)) in self.items() + if p.get('show_in_help_list', True))) + cmds_str = ', '.join([self.cmd + k for k in cmds]) + lines = utils.split_message( + 'Available commands: %s ' % cmds_str, 160) + for line in lines: + yield line + url = self.config.get('url') + if url: + yield 'Full help is available at ' + url + + def __repr__(self): + return '' % sorted([self.cmd + k for k in self.keys()]) + + +class Done: + + def done(self): + return True + + +@command(permission='admin', show_in_help_list=False, public=False) +def ping(bot, mask, target, args): + """ping/pong + + %%ping + """ + bot.send('NOTICE %(nick)s :PONG %(nick)s!' % dict(nick=mask.nick)) + + +@command(venusian_category='irc3.debug', show_in_help_list=False) +def quote(bot, mask, target, args): + """send quote to the server + + %%quote ... + """ + msg = ' '.join(args['']) + bot.log.info('quote> %r', msg) + bot.send(msg) + + +@command(venusian_category='irc3.debug', show_in_help_list=False) +def reconnect(bot, mask, target, args): + """force reconnect + + %%reconnect + """ + plugin = bot.get_plugin(utils.maybedotted('irc3.plugins.core.Core')) + bot.loop.call_soon(plugin.reconnect) + + +@irc3.extend +def print_help_page(bot, file=sys.stdout): + """print help page""" + def p(text): + print(text, file=file) + plugin = bot.get_plugin(Commands) + title = "Available Commands for {nick} at {host}".format(**bot.config) + p("=" * len(title)) + p(title) + p("=" * len(title)) + p('') + p('.. contents::') + p('') + modules = {} + for name, (predicates, callback) in plugin.items(): + commands = modules.setdefault(callback.__module__, []) + commands.append((name, callback, predicates)) + + for module in sorted(modules): + p(module) + p('=' * len(module)) + p('') + for name, callback, predicates in sorted(modules[module]): + p(name) + p('-' * len(name)) + p('') + doc = callback.__doc__ + doc = doc.replace('%%', bot.config.cmd) + for line in doc.split('\n'): + line = line.strip() + if line.startswith(bot.config.cmd): + line = ' ``{}``'.format(line) + p(line) + if 'permission' in predicates: + p('*Require {0[permission]} permission.*'.format(predicates)) + if predicates.get('public', True) is False: + p('*Only available in private.*') + p('') diff --git a/hydra_core__standalone/plugins/core_plugin.py b/hydra_core__standalone/plugins/core_plugin.py new file mode 100644 index 0000000..585666a --- /dev/null +++ b/hydra_core__standalone/plugins/core_plugin.py @@ -0,0 +1,118 @@ +# -*- coding: utf-8 -*- +from irc3 import event +from irc3 import rfc +__doc__ = ''' +============================================== +:mod:`irc3.plugins.core` Core plugin +============================================== + +Core events + +.. autoclass:: Core + :members: + +.. + >>> from irc3.testing import IrcBot + +Usage:: + + >>> bot = IrcBot() + >>> bot.include('irc3.plugins.core') +''' + + +class Core: + + def __init__(self, bot): + self.bot = bot + self.timeout = int(self.bot.config.get('timeout')) + self.max_lag = int(self.bot.config.get('max_lag')) + self.reconn_handle = None + self.ping_handle = None + self.nick_handle = None + self.before_connect_events = [ + event(rfc.CONNECTED, self.connected), + event(r"^:\S+ 005 \S+ (?P.+) :\S+.*", + self.set_config), + ] + + def connection_made(self, client=None): + # handle server config + config = self.bot.defaults['server_config'].copy() + self.bot.config['server_config'] = config + self.bot.detach_events(*self.before_connect_events) + self.bot.attach_events(insert=True, *self.before_connect_events) + + # ping/ping + self.connection_made_at = self.bot.loop.time() + self.pong(event='CONNECT', data='') + + def connected(self, **kwargs): + """triger the server_ready event""" + self.bot.log.info('Server config: %r', self.bot.server_config) + + # recompile when I'm sure of my nickname + self.bot.config['nick'] = kwargs['me'] + self.bot.recompile() + + # Let all plugins know that server can handle commands + self.bot.notify('server_ready') + + # detach useless events + self.bot.detach_events(*self.before_connect_events) + + def reconnect(self): # pragma: no cover + self.bot.log.info( + "We're waiting a ping for too long. Trying to reconnect...") + self.bot.loop.call_soon( + self.bot.protocol.connection_lost, + 'No pong reply' + ) + self.pong(event='RECONNECT', data='') + + @event(rfc.PONG) + def pong(self, event='PONG', data='', **kw): # pragma: no cover + """P0NG/PING""" + self.bot.log.debug('%s ping-pong (%s)', event, data) + if self.reconn_handle is not None: + self.reconn_handle.cancel() + self.reconn_handle = self.bot.loop.call_later(self.timeout, + self.reconnect) + if self.ping_handle is not None: + self.ping_handle.cancel() + self.ping_handle = self.bot.loop.call_later( + self.timeout - self.max_lag, self.bot.send, + 'PING :%s' % int(self.bot.loop.time())) + + @event(rfc.PING) + def ping(self, data): + """PING reply""" + self.bot.send('PONG :' + data) + self.pong(event='PING', data=data) + + @event(rfc.NEW_NICK) + def recompile(self, nick=None, new_nick=None, **kw): + """recompile regexp on new nick""" + if self.bot.nick == nick.nick: + self.bot.config['nick'] = new_nick + self.bot.recompile() + + @event(rfc.ERR_NICK) + def badnick(self, me=None, nick=None, **kw): + """Use alt nick on nick error""" + if me == '*': + self.bot.set_nick(self.bot.nick + '_') + self.bot.log.debug('Trying to regain nickname in 30s...') + self.nick_handle = self.bot.loop.call_later( + 30, self.bot.set_nick, self.bot.original_nick) + + def set_config(self, data=None, **kwargs): + """Store server config""" + config = self.bot.config['server_config'] + for opt in data.split(' '): + if '=' in opt: + opt, value = opt.split('=', 1) + else: + value = True + if opt.isupper(): + config[opt] = value diff --git a/hydra_core__standalone/plugins/fifo_plugin.py b/hydra_core__standalone/plugins/fifo_plugin.py new file mode 100644 index 0000000..37b1ad3 --- /dev/null +++ b/hydra_core__standalone/plugins/fifo_plugin.py @@ -0,0 +1,101 @@ +# -*- coding: utf-8 -*- ############################################################### SOF +import os +import irc3 +from stat import S_ISFIFO +########################################################################################### +@irc3.plugin +class Fifo: + ####################################################################################### + BLOCK_SIZE = 1024 + MAX_BUFFER_SIZE = 800 + ####################################################################################### + def __init__(self, context): + self.context = context + self.config = self.context.config + self.send_blank_line = self.config.get('send_blank_line', True) + self.runpath = self.config.get('runpath', f'{os.getcwd()}/fifo') + if not os.path.exists(self.runpath): + os.makedirs(self.runpath) + self.loop = self.context.loop + self.fifos = {} + self.buffers = {} + self.create_fifo(None) + ####################################################################################### + @classmethod + def read_fd(cls, fd): + while True: + try: + return os.read(fd, cls.BLOCK_SIZE) + except InterruptedError: + continue + except BlockingIOError: + return b"" + ####################################################################################### + def handle_line(self, line, channel): + if not line: + return + line = line.decode("utf8") + if not self.send_blank_line and not line.strip(): + return + if channel is None: + self.context.send_line(line) + else: + self.context.privmsg(channel, line) + ####################################################################################### + def data_received(self, data, channel): + if not data: + return + prev = self.buffers.get(channel, b"") + lines = (prev + data).splitlines(True) + last = lines[-1] + for line in lines[:-1]: + line = line.rstrip(b'\r\n') + self.handle_line(line, channel) + if last.endswith(b'\n'): + line = last.rstrip(b'\r\n') + self.handle_line(line, channel) + self.buffers[channel] = b"" + return + if len(last) > self.MAX_BUFFER_SIZE: + self.handle_line(last, channel) + self.buffers[channel] = b"" + else: + self.buffers[channel] = last + ####################################################################################### + def watch_fd(self, fd, channel): + reading = True + + while reading: + data = self.read_fd(fd) + reading = len(data) == self.BLOCK_SIZE + self.data_received(data, channel) + ####################################################################################### + def create_fifo(self, channel): + if channel is None: + path = os.path.join(self.runpath, ':raw') + else: + path = os.path.join(self.runpath, channel.strip('#&+')) + try: + mode = os.stat(path).st_mode + except OSError: + pass + else: + if not S_ISFIFO(mode): + self.context.log.warn( + 'file %s created without mkfifo. remove it', + path) + os.remove(path) + if not os.path.exists(path): + os.mkfifo(path) + fd = os.open(path, os.O_RDWR | os.O_NONBLOCK) + self.loop.add_reader(fd, self.watch_fd, fd, channel) + self.context.log.debug("%s's fifo is %s %r", + channel or ':raw', path, fd) + return fd + ####################################################################################### + @irc3.event(irc3.rfc.JOIN) + def join(self, mask=None, channel=None, **kwargs): + if mask.nick == self.context.nick: + if channel not in self.fifos: + self.fifos[channel] = self.create_fifo(channel) +####################################################################################### EOF diff --git a/hydra_core__standalone/plugins/net_hydra_plugin.py b/hydra_core__standalone/plugins/net_hydra_plugin.py new file mode 100644 index 0000000..1c432db --- /dev/null +++ b/hydra_core__standalone/plugins/net_hydra_plugin.py @@ -0,0 +1,165 @@ +# -*- coding: utf-8 -*- ############################################################### SOF +import irc3, os +from irc3.plugins.command import command +from irc3.plugins.cron import cron +from irc3.plugins import core +from random import randint as rint +from random import shuffle +from datetime import datetime +########################################################################################### +class dr1p: + def __init__(): + dr1p.designation="" + dr1p.enforcing=False + dr1p.purpose="" + dr1p.color="" + dr1p.keyid="" + dr1p.token="" + dr1p.home="" +########################################################################################### +@irc3.plugin +class Plugin: + ####################################################################################### + def __init__(self,bot): + self.bot=bot + try: + dr1p.purpose=os.environ['HYDRA_PURPOSE'] + dr1p.designation=os.environ['HYDRA_DESIGNATION'] + dr1p.home=os.environ['HYDRA_HOME'] + dr1p.enforcing=False + except: + dr1p.designation="dupe" + dr1p.enforcing=False + return + if dr1p.designation=="core": dr1p.color="\x0304" + dr1p.keyid=self.hydra_id(1) + dr1p.token=self.hydra_id(0) + ####################################################################################### + def hydra_id(self,mode=1): + hydra="" + for i in range(7): hydra+=hex(rint(0,255))[2:].zfill(2).upper() + hydra+=hex(int(datetime.now().timestamp()))[-4:].upper() + hydra=list(hydra) + shuffle(hydra) + if mode: + hydra=''.join(hydra) + else: + hydra=''.join(hydra)[6:14] + return hydra + ####################################################################################### + def server_ready(self): + if not dr1p.designation=='core': + self.bot.privmsg("maple",f"[hydra:{dr1p.keyid}] - dupe - connected") + else: + self.bot.privmsg("maple",f"core - connected") + ####################################################################################### + @irc3.event(irc3.rfc.ERR_NICK) + def on_errnick(self,srv=None,retcode=None,me=None,nick=None,data=None): + ################################################################################### + if not dr1p.designation=='core': return + msg=f'err_nick - srv:{srv} - retcode:{retcode} - me:{me} - nick:{nick} - data:{data}' + self.bot.privmsg("maple",msg.lower()) + ####################################################################################### + @irc3.event(irc3.rfc.NEW_NICK) + def on_newnick(self,nick=None,new_nick=None): + ################################################################################### + if not dr1p.designation=='core': return + if nick==self.bot.config['nick'] or new_nick==self.bot.config['nick']: + msg=f'new_nick - nick:{nick} - new_nick:{new_nick}' + self.bot.privmsg("maple",msg.lower()) + ####################################################################################### + @irc3.event(irc3.rfc.CTCP) + def on_ctcp(self,mask=None,event=None,target=None,ctcp=None): + ################################################################################### + if not dr1p.designation=='core': return + msg=f'ctcpd - mask:{mask} - event:{event} - target:{target} - ctcp:{ctcp}' + self.bot.privmsg("maple",msg.lower()) + ####################################################################################### + @irc3.event(irc3.rfc.INVITE) + def on_invite(self,mask=None,channel=None): + ################################################################################### + if not dr1p.designation=='core': return + msg=f'invited - mask:{mask} - channel:{channel}' + self.bot.privmsg("maple",msg.lower()) + ####################################################################################### + @irc3.event(irc3.rfc.KICK) + def on_kick(self,mask=None,event=None,channel=None,target=None,data=None): + ################################################################################### + if not dr1p.designation=='core': return + msg=f'kicked - mask:{mask} - event:{event} - target:{target} - data:{data}' + self.bot.privmsg("maple",msg) + ####################################################################################### + @irc3.event(irc3.rfc.PRIVMSG) + def on_privmsg(self,mask=None,event=None,target=None,data=None,**kw): + ################################################################################### + # if dr1p.enforcing==False: return + if target!=self.bot.config['nick'] and mask.nick==self.bot.nick: return + if mask.nick==self.bot.nick and target==self.bot.config['nick'] and dr1p.designation=='core': + if data.endswith('dupe - connected'): + _keyid=data.split("[hydra:")[1].split("]")[0] + _diyek=_keyid[::-1] + msg=f'[KEYID:{_keyid}] - [DIYEK:{_diyek}] - COLOR:{rint(16,87)}' + self.bot.privmsg(self.bot.config['nick'],msg) + if mask.nick==self.bot.nick and target==self.bot.config['nick'] and dr1p.designation=='dupe': + if not data.find('DIYEK')==-1: + _keyid=data.split(":")[1].split("]")[0] + if _keyid.lower()==dr1p.keyid.lower(): + if not data.find("] - [DIYEK:")==-1: + _diyek=data.split("] - [DIYEK:")[1].split("]")[0] + if _keyid.lower()==_diyek[::-1].lower(): + _color=int(data.split(" - COLOR:")[1].strip()) + if not dr1p.color: + dr1p.color=f"\x03{str(_color)}" + if dr1p.designation=='core': + msg=f"{dr1p.color}[maple:{dr1p.keyid}] - " + else: + try: + msg=f"{dr1p.color}[hydra:{dr1p.keyid}] - " + except: + dr1p.color="\x0303" + msg=f"{dr1p.color}[hydra:{dr1p.keyid}] - " + if mask.nick!=self.bot.config['nick']: + if target!=dr1p.home: return + if target==dr1p.home: return + msg+=f'event:{event} - mask:{mask} - target:{target} - data:' + msg+=f'{data}' + if kw: msg+=f" - kw:{kw}" + self.bot.privmsg(dr1p.home,msg.lower()) + ####################################################################################### + @irc3.event(irc3.rfc.MY_PRIVMSG) + def on_my_privmsg(self,mask=None,event=None,target=None,data=None,**kw): + ################################################################################### + pass + ####################################################################################### + @irc3.event(irc3.rfc.JOIN_PART_QUIT) + def on_join_part_quit(self,mask=None,target=None,data=None,**kw): + target=kw['channel'] + ################################################################################### + if mask.nick==self.bot.config['nick']: + ############################################################################### + if kw['event']=='JOIN': + self.bot.privmsg("maple",f"joined {target}".lower()) + if target!=dr1p.home: + if dr1p.enforcing: + reason=".[d]." + self.bot.part(target,reason) + self.bot.privmsg("maple",f"parted {target} - {reason}".lower()) + if dr1p.designation=="core": + msg=f"[maple:{dr1p.keyid}] - core - maple online - purpose: {dr1p.purpose}" + self.bot.privmsg(dr1p.home,msg) + else: + msg=f"[hydra:{dr1p.keyid}] - dupe - hydra online - purpose: {dr1p.purpose}" + self.bot.privmsg(dr1p.home,msg) + if kw['event']=='PART': + if dr1p.designation=="core": + msg=f"[maple:{dr1p.keyid}] -" + else: + msg=f"[hydra:{dr1p.keyid}] -" + self.bot.privmsg("maple",msg+f"parted {target} - {data}") + if kw['event']=='QUIT': + if dr1p.designation=="core": + msg=f"[maple:{dr1p.keyid}] -" + else: + msg=f"[hydra:{dr1p.keyid}] -" + self.bot.privmsg("maple",msg+f"quit {target} - {data}") +####################################################################################### EOF diff --git a/hydra_core__standalone/plugins/sasl_custom_plugin.py b/hydra_core__standalone/plugins/sasl_custom_plugin.py new file mode 100644 index 0000000..efd682f --- /dev/null +++ b/hydra_core__standalone/plugins/sasl_custom_plugin.py @@ -0,0 +1,48 @@ +# -*- coding: utf-8 -*- ########################################################## SOF +import irc3, os, base64 +###################################################################################### +BOT_SASL_USERNAME=os.environ['BOT_SASL_USERNAME'] +BOT_SASL_PASSWORD=os.environ['BOT_SASL_PASSWORD'] +###################################################################################### +@irc3.plugin +class DR1PSASL: + ################################################################################## + def __init__(self, bot): + print('<<< _sasl_custom_plugin >>> [ custom sasl initiated ]') + self.bot=bot + self.auth=(f'{BOT_SASL_USERNAME}\0{BOT_SASL_USERNAME}\0{BOT_SASL_PASSWORD}') + self.auth=base64.encodebytes(self.auth.encode('utf8')) + self.auth=self.auth.decode('utf8').rstrip('\n') + self.events = [ + irc3.event(r'^:\S+ CAP \S+ LS :(?P.*)', self.cap_ls), + irc3.event(r'^:\S+ CAP \S+ ACK sasl', self.cap_ack), + irc3.event(r'AUTHENTICATE +', self.authenticate), + irc3.event(r'^:\S+ 903 \S+ :Authentication successful',self.cap_end), + ] + ################################################################################## + def connection_ready(self, *args, **kwargs): + print('<<< _sasl_custom_plugin >>> [ CAP LS ]') + self.bot.send('CAP LS\r\n') + self.bot.attach_events(*self.events) + ################################################################################## + def cap_ls(self, data=None, **kwargs): + print('<<< _sasl_custom_plugin >>> [ CAP REQ :sasl ]') + if 'sasl' in data.lower(): + self.bot.send_line('CAP REQ :sasl') + else: + self.cap_end() + ################################################################################## + def cap_ack(self, **kwargs): + print('<<< _sasl_custom_plugin >>> [ AUTHENTICATE PLAIN ]') + self.bot.send_line('AUTHENTICATE PLAIN') + ################################################################################## + def authenticate(self, **kwargs): + print(f'<<< _sasl_custom_plugin >>> [ AUTHENTICATE {self.auth} ]') + self.bot.send_line(f'AUTHENTICATE {self.auth}\n') + ################################################################################## + def cap_end(self, **kwargs): + print('<<< _sasl_custom_plugin >>> [ CAP END ]') + self.bot.send_line('CAP END\r\n') + self.bot.detach_events(*self.events) + ################################################################################## +################################################################################## EOF diff --git a/hydra_core__standalone/plugins/storage_plugin.py b/hydra_core__standalone/plugins/storage_plugin.py new file mode 100644 index 0000000..d8631a9 --- /dev/null +++ b/hydra_core__standalone/plugins/storage_plugin.py @@ -0,0 +1,293 @@ +# -*- coding: utf-8 -*- ############################################################### SOF +import os +try: + import ujson as json +except ImportError: + import json +import irc3 +import shelve +########################################################################################### +class Shelve: + ####################################################################################### + def __init__(self, uri=None, **kwargs): + self.filename = uri[9:] + self.db = shelve.open(self.filename) + ####################################################################################### + def set(self, key, value): + self.db[key] = value + self.db.sync() + ####################################################################################### + def get(self, key): + return self.db[key] + ####################################################################################### + def delete(self, key): + del self.db[key] + self.sync() + ####################################################################################### + def contains(self, key): + return key in self.db + ####################################################################################### + def sync(self): + self.db.sync() + ####################################################################################### + def close(self): + self.db.close() +########################################################################################### +class JSON: + def __init__(self, uri=None, **kwargs): + self.filename = uri[7:] + if os.path.isfile(self.filename): # pragma: no cover + with open(self.filename) as fd: + self.db = json.load(fd) + else: + self.db = {} + ####################################################################################### + def set(self, key, value): + self.db[key] = value + self.sync() + ####################################################################################### + def get(self, key): + return self.db[key] + ####################################################################################### + def delete(self, key): + del self.db[key] + self.sync() + ####################################################################################### + def contains(self, key): + return key in self.db + ####################################################################################### + def sync(self): + with open(self.filename, 'w') as fd: + json.dump(self.db, fd, indent=2, sort_keys=True) + ####################################################################################### + def close(self): + self.sync() +########################################################################################### +class Redis: + def __init__(self, uri=None, **kwargs): + ConnectionPool = irc3.utils.maybedotted( + 'redis.connection.ConnectionPool') + pool = ConnectionPool.from_url(uri) + StrictRedis = irc3.utils.maybedotted('redis.client.StrictRedis') + self.db = StrictRedis(connection_pool=pool) + ####################################################################################### + def set(self, key, value): + self.db.hmset(key, value) + ####################################################################################### + def get(self, key): + keys = self.db.hkeys(key) + if not keys: + raise KeyError() + values = self.db.hmget(key, keys) + keys = [k.decode('utf8') for k in keys] + values = [v.decode('utf8') for v in values] + values = dict(zip(keys, values)) + return values + ####################################################################################### + def delete(self, key): + self.db.delete(key) + ####################################################################################### + def contains(self, key): + return self.db.exists(key) + ####################################################################################### + def flushdb(self): + self.db.flushdb() + ####################################################################################### + def sync(self): + self.db.save() + ####################################################################################### + def close(self): + self.sync() +########################################################################################### +class SQLite: + CREATE_TABLE = """ + CREATE TABLE IF NOT EXISTS + irc3_storage ( + key text not null, + value text default '', + PRIMARY KEY (key) + ); + """ + UPSERT = """ + INSERT OR REPLACE INTO irc3_storage(key,value) VALUES(?, ?); + """ + ####################################################################################### + def __init__(self, uri=None, **kwargs): + self.sqlite = irc3.utils.maybedotted('sqlite3') + self.uri = uri.split('://')[-1] + conn = self.sqlite.connect(self.uri) + cursor = conn.cursor() + cursor.execute(self.CREATE_TABLE) + conn.commit() + conn.close() + ####################################################################################### + def set(self, key, value): + conn = self.sqlite.connect(self.uri) + cursor = conn.cursor() + cursor.execute(self.UPSERT, (key, json.dumps(value))) + cursor.fetchall() + conn.commit() + conn.close() + ####################################################################################### + def get(self, key): + value = None + conn = self.sqlite.connect(self.uri) + cursor = conn.cursor() + cursor.execute("SELECT value FROM irc3_storage where key=?;", (key,)) + for row in cursor.fetchall(): + value = json.loads(row[0]) + break + cursor.close() + conn.close() + if value is None: + raise KeyError(key) + return value + ####################################################################################### + def delete(self, key): + conn = self.sqlite.connect(self.uri) + cursor = conn.cursor() + cursor.execute("DELETE FROM irc3_storage where key=?;", (key,)) + cursor.close() + conn.commit() + conn.close() + ####################################################################################### + def contains(self, key): + conn = self.sqlite.connect(self.uri) + cursor = conn.cursor() + cursor.execute("SELECT value FROM irc3_storage where key=?;", (key,)) + res = False + if len(list(cursor.fetchall())) == 1: + res = True + cursor.close() + conn.close() + return res + ####################################################################################### + def flushdb(self): + conn = self.sqlite.connect(self.uri) + cursor = conn.cursor() + cursor.execute("DROP TABLE IF EXISTS irc3_storage;") + cursor.execute(self.CREATE_TABLE) + cursor.close() + conn.commit() + conn.close() + ####################################################################################### + def sync(self): + pass + ####################################################################################### + def close(self): + pass +########################################################################################### +@irc3.plugin +class Storage: + backends = { + 'shelve': Shelve, + 'json': JSON, + 'unix': Redis, + 'redis': Redis, + 'rediss': Redis, + 'sqlite': SQLite, + } + ####################################################################################### + def __init__(self, context): + uri = context.config.storage + name = uri.split('://', 1)[0] + try: + factory = self.backends[name] + except KeyError: # pragma: no cover + raise LookupError('No such backend %s' % name) + self.backend = factory(uri) + self.context = context + self.context.db = self + ####################################################################################### + def setdefault(self, key_, **kwargs): + """Update storage value for key with kwargs iif the keys doesn't + exist. Return stored values""" + stored = self[key_] + changed = False + for k, v in kwargs.items(): + if k not in stored: + stored[k] = v + changed = True + else: + kwargs[k] = stored[k] + if changed: + self[key_] = stored + return kwargs + ####################################################################################### + def get(self, key_, default=None): + """Get storage value for key or return default""" + if key_ not in self: + return default + else: + return self[key_] + ####################################################################################### + def getlist(self, key_, default=None): + """Get storage value (as list) for key or return default""" + if key_ not in self: + return default + else: + value = self[key_] + value = [(int(i), v) for i, v in value.items()] + return [v for k, v in sorted(value)] + ####################################################################################### + def set(self, key_, **kwargs): + """Update storage value for key with kwargs""" + stored = self.get(key_, dict()) + changed = False + for k, v in kwargs.items(): + if k not in stored or stored[k] != v: + stored[k] = v + changed = True + if changed: + self[key_] = stored + ####################################################################################### + def setlist(self, key_, value): + """Update storage value (as list)""" + value = dict([(str(i), v) for i, v in enumerate(value)]) + if key_ in self: + del self[key_] + self.set(key_, **value) + ####################################################################################### + def __setitem__(self, key, value): + """Set storage value for key""" + key = getattr(key, '__module__', key) + if not isinstance(value, dict): # pragma: no cover + raise TypeError('value must be a dict') + try: + return self.backend.set(key, value) + except Exception as e: # pragma: no cover + self.context.log.exception(e) + raise + ####################################################################################### + def __getitem__(self, key): + """Get storage value for key""" + key = getattr(key, '__module__', key) + try: + return self.backend.get(key) + except KeyError: + raise KeyError(key) + except Exception as e: # pragma: no cover + self.context.log.exception(e) + raise + ####################################################################################### + def __delitem__(self, key): + """Delete key in storage""" + key = getattr(key, '__module__', key) + try: + self.backend.delete(key) + except Exception as e: # pragma: no cover + self.context.log.exception(e) + raise + ####################################################################################### + def __contains__(self, key): + """Return True if storage contains key""" + key = getattr(key, '__module__', key) + try: + return self.backend.contains(key) + except Exception as e: # pragma: no cover + self.context.log.exception(e) + raise + ####################################################################################### + def SIGINT(self): + self.backend.close() +####################################################################################### EOF