From 46f6966eda3157bc6fa25d6b2ed6b89b223b0ec9 Mon Sep 17 00:00:00 2001 From: deathbybandaid Date: Thu, 24 Feb 2022 10:58:48 -0500 Subject: [PATCH] Core Bot --- .gitignore | 57 +++ COPYING | 23 ++ MANIFEST.in | 9 + NEWS | 0 README.md | 1 + requirements.txt | 1 + setup.cfg | 34 ++ setup.py | 25 ++ sopel_SpiceBot_Core_1/SBCore/__init__.py | 60 ++++ .../SBCore/commands/__init__.py | 333 +++++++++++++++++ .../SBCore/comms/__init__.py | 253 +++++++++++++ .../SBCore/config/__init__.py | 45 +++ .../SBCore/database/__init__.py | 294 +++++++++++++++ .../SBCore/events/__init__.py | 147 ++++++++ .../SBCore/logger/__init__.py | 18 + .../SBCore/versions/__init__.py | 111 ++++++ sopel_SpiceBot_Core_1/__init__.py | 18 + sopel_SpiceBot_Core_1/version.json | 3 + sopel_SpiceBot_Core_Prerun/__init__.py | 148 ++++++++ sopel_SpiceBot_Core_Startup/__init__.py | 81 +++++ .../__init__.py | 13 + sopel_SpiceBot_Runtime_Commands/__init__.py | 32 ++ .../__init__.py | 44 +++ .../__init__.py | 22 ++ spicebot_command_lower/__init__.py | 12 + spicebot_command_upper/__init__.py | 11 + spicemanip/__init__.py | 334 ++++++++++++++++++ 27 files changed, 2129 insertions(+) create mode 100644 .gitignore create mode 100644 COPYING create mode 100644 MANIFEST.in create mode 100644 NEWS create mode 100644 requirements.txt create mode 100644 setup.cfg create mode 100644 setup.py create mode 100644 sopel_SpiceBot_Core_1/SBCore/__init__.py create mode 100644 sopel_SpiceBot_Core_1/SBCore/commands/__init__.py create mode 100644 sopel_SpiceBot_Core_1/SBCore/comms/__init__.py create mode 100644 sopel_SpiceBot_Core_1/SBCore/config/__init__.py create mode 100644 sopel_SpiceBot_Core_1/SBCore/database/__init__.py create mode 100644 sopel_SpiceBot_Core_1/SBCore/events/__init__.py create mode 100644 sopel_SpiceBot_Core_1/SBCore/logger/__init__.py create mode 100644 sopel_SpiceBot_Core_1/SBCore/versions/__init__.py create mode 100644 sopel_SpiceBot_Core_1/__init__.py create mode 100644 sopel_SpiceBot_Core_1/version.json create mode 100644 sopel_SpiceBot_Core_Prerun/__init__.py create mode 100644 sopel_SpiceBot_Core_Startup/__init__.py create mode 100644 sopel_SpiceBot_Runtime_Action_Commands/__init__.py create mode 100644 sopel_SpiceBot_Runtime_Commands/__init__.py create mode 100644 sopel_SpiceBot_Runtime_Nickname_Commands/__init__.py create mode 100644 sopel_SpiceBot_Runtime_Unmatched_Commands/__init__.py create mode 100644 spicebot_command_lower/__init__.py create mode 100644 spicebot_command_upper/__init__.py create mode 100644 spicemanip/__init__.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ba74660 --- /dev/null +++ b/.gitignore @@ -0,0 +1,57 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*,cover + +# Translations +*.mo +*.pot + +# Django stuff: +*.log + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ diff --git a/COPYING b/COPYING new file mode 100644 index 0000000..7d1cd07 --- /dev/null +++ b/COPYING @@ -0,0 +1,23 @@ + + Eiffel Forum License, version 2 + + 1. Permission is hereby granted to use, copy, modify and/or + distribute this package, provided that: + * copyright notices are retained unchanged, + * any distribution of this package, whether modified or not, + includes this license text. + 2. Permission is hereby also granted to distribute binary programs + which depend on this package. If the binary program depends on a + modified version of this package, you are encouraged to publicly + release the modified version of this package. + +*********************** + +THIS PACKAGE IS PROVIDED "AS IS" AND WITHOUT WARRANTY. ANY EXPRESS OR +IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE AUTHORS BE LIABLE TO ANY PARTY FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES ARISING IN ANY WAY OUT OF THE USE OF THIS PACKAGE. + +*********************** diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..87f54d3 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,9 @@ +include NEWS +include COPYING +include README.md + +recursive-exclude * __pycache__ +recursive-exclude * *.py[co] +recursive-include *.py +recursive-include * *.py +recursive-include * * *.py diff --git a/NEWS b/NEWS new file mode 100644 index 0000000..e69de29 diff --git a/README.md b/README.md index eba29d7..6df81dd 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,3 @@ # SpiceBot +A Niche Wrapper around Sopel diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..b397e36 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +schedule diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..394e40c --- /dev/null +++ b/setup.cfg @@ -0,0 +1,34 @@ +[metadata] +name = SpiceBot +version = 0.1.0 +description = A Niche Wrapper around Sopel +author = deathbybandaid +author_email = sam@deathbybandaid.net +url = https://git.deathbybandaid.net/deathbybandaid/SpiceBot.git +license = Eiffel Forum License, version 2 +classifiers = + Intended Audience :: Developers + Intended Audience :: System Administrators + License :: Eiffel Forum License (EFL) + License :: OSI Approved :: Eiffel Forum License + Topic :: Communications :: Chat :: Internet Relay Chat + +[options] +packages = find: +zip_safe = false +include_package_data = true +install_requires = + sopel>=7.0,<8 + +[options.entry_points] +sopel.plugins = + sopel_SpiceBot_Core_1 = sopel_SpiceBot_Core_1 + sopel_SpiceBot_Core_Prerun = sopel_SpiceBot_Core_Prerun + sopel_SpiceBot_Core_Startup = sopel_SpiceBot_Core_Startup + sopel_SpiceBot_Runtime_Commands = sopel_SpiceBot_Runtime_Commands + sopel_SpiceBot_Runtime_Nickname_Commands = sopel_SpiceBot_Runtime_Nickname_Commands + sopel_SpiceBot_Runtime_Action_Commands = sopel_SpiceBot_Runtime_Action_Commands + sopel_SpiceBot_Runtime_Unmatched_Commands = sopel_SpiceBot_Runtime_Unmatched_Commands + spicemanip = spicemanip + spicebot_command_lower = spicebot_command_lower + spicebot_command_upper = spicebot_command_upper diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..9fe3b79 --- /dev/null +++ b/setup.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +from __future__ import print_function +import os +import sys +from setuptools import setup, find_packages + + +if __name__ == '__main__': + print('Sopel does not correctly load plugins installed with setup.py ' + 'directly. Please use "pip install .", or add ' + '{}/sopel_SpiceBot to core.extra in your config.' + .format(os.path.dirname(os.path.abspath(__file__))), + file=sys.stderr) + +with open('README.md') as readme_file: + readme = readme_file.read() + +with open('NEWS') as history_file: + history = history_file.read() + + +setup( + long_description=readme + '\n\n' + history, + long_description_content_type='text/markdown', +) diff --git a/sopel_SpiceBot_Core_1/SBCore/__init__.py b/sopel_SpiceBot_Core_1/SBCore/__init__.py new file mode 100644 index 0000000..424b55d --- /dev/null +++ b/sopel_SpiceBot_Core_1/SBCore/__init__.py @@ -0,0 +1,60 @@ + +from .config import Config +from .versions import Versions +from .logger import Logger +from .database import Database +from .comms import Comms +from .events import Events +from .commands import Commands + + +class SpiceBotCore_OBJ(): + + def __init__(self, script_dir): + + # Set directory for the plugin + self.script_dir = script_dir + + # Allow SpiceBot to interact with Sopel Logger + self.logger = Logger() + self.logger.info("SpiceBot Logging Interface Setup Complete.") + + # Allow Spicebot to mimic Sopel Config + self.config = Config(script_dir) + self.logger.info("SpiceBot Config Interface Setup Complete.") + + # Parse Version Information for the ENV + self.versions = Versions(self.config, self.logger) + self.logger.info("SpiceBot Versions Interface Setup Complete.") + + # Mimic Sopel DB, with enhancements + self.database = Database(self.config) + self.logger.info("SpiceBot Database Interface Setup Complete.") + + # SpiceBots manual event system + self.events = Events(self.logger) + self.logger.info("SpiceBot Events Interface Setup Complete.") + + # Bypass Sopel's method for writing to IRC + self.comms = Comms(self.config) + self.logger.info("SpiceBot Comms Interface Setup Complete.") + + # SpiceBots access to Sopel Command listing + self.commands = Commands(self.config) + self.logger.info("SpiceBot Commands Interface Setup Complete.") + + def setup(self, bot): + """This runs with the plugin setup routine""" + + # store an access interface to sopel.bot + self.bot = bot + + # Re-initialize the bot config properly during plugin setup routine + self.config.config = bot.config + + # Give Spicebot access to bot commands + self.commands.bot = bot + + # OSD shortcut + def osd(self, messages, recipients=None, text_method='PRIVMSG', max_messages=-1): + return self.comms.osd(messages, recipients, text_method, max_messages) diff --git a/sopel_SpiceBot_Core_1/SBCore/commands/__init__.py b/sopel_SpiceBot_Core_1/SBCore/commands/__init__.py new file mode 100644 index 0000000..50cbacc --- /dev/null +++ b/sopel_SpiceBot_Core_1/SBCore/commands/__init__.py @@ -0,0 +1,333 @@ + + +from sopel.trigger import PreTrigger + + +class Commands(): + + def __init__(self, config): + self.bot = None + self.config = config + + @property + def multi_split_key(self): + # TODO config + return "&&" + + @property + def pipe_split_key(self): + # TODO config + return "|" + + @property + def valid_sopel_commands(self): + found = [] + for command_dict in self.sopel_commands: + found.append(command_dict["name"]) + found.extend(command_dict["aliases"]) + return found + + @property + def valid_sopel_nickname_commands(self): + found = [] + for command_dict in self.sopel_nickname_commands: + found.append(command_dict["name"]) + found.extend(command_dict["aliases"]) + return found + + @property + def valid_sopel_action_commands(self): + found = [] + for command_dict in self.sopel_action_commands: + found.append(command_dict["name"]) + found.extend(command_dict["aliases"]) + return found + + @property + def sopel_commands(self): + commands_list = [] + for plugin_name, commands in self.bot.rules.get_all_commands(): + for command in commands.values(): + commands_list.append({ + "name": command.name, + "aliases": command.aliases, + "type": "command" + }) + return commands_list + + @property + def sopel_nickname_commands(self): + commands_list = [] + for plugin_name, commands in self.bot.rules.get_all_nick_commands(): + for command in commands.values(): + commands_list.append({ + "name": command.name, + "aliases": command.aliases, + "type": "nickname_command" + }) + return commands_list + + @property + def sopel_action_commands(self): + commands_list = [] + for plugin_name, commands in self.bot.rules.get_all_action_commands(): + for command in commands.values(): + commands_list.append({ + "name": command.name, + "aliases": command.aliases, + "type": "action_command" + }) + return commands_list + + def dispatch(self, trigger_dict): + if trigger_dict["trigger_type"] == "command": + pretrigger = self.generate_pretrigger_command(trigger_dict) + elif trigger_dict["trigger_type"] == "nickname_command": + pretrigger = self.generate_pretrigger_nickname_command(trigger_dict) + elif trigger_dict["trigger_type"] == "action_command": + pretrigger = self.generate_pretrigger_action_command(trigger_dict) + self.bot.dispatch(pretrigger) + + def generate_pretrigger_command(self, trigger_dict): + # @time=2022-02-23T15:04:01.447Z : + pretrigger = PreTrigger( + self.bot.nick, + ":%s %s %s :%s%s %s" % (trigger_dict["trigger_hostmask"], "PRIVMSG", trigger_dict["trigger_sender"], + trigger_dict["trigger_prefix"], trigger_dict["trigger_command"], trigger_dict["trigger_str"]) + ) + return pretrigger + + def generate_pretrigger_nickname_command(self, trigger_dict): + pretrigger = PreTrigger( + self.bot.nick, + ":%s %s %s :%s %s %s" % (trigger_dict["trigger_hostmask"], "PRIVMSG", trigger_dict["trigger_sender"], + trigger_dict["trigger_prefix"], trigger_dict["trigger_command"], trigger_dict["trigger_str"]) + ) + return pretrigger + + def generate_pretrigger_action_command(self, trigger_dict): + pretrigger = PreTrigger( + self.bot.nick, + ":%s %s %s :%s%s %s%s %s" % (trigger_dict["trigger_hostmask"], "PRIVMSG", trigger_dict["trigger_sender"], + "\x01", "ACTION", trigger_dict["trigger_command"], trigger_dict["trigger_str"], "\x01") + ) + return pretrigger + + def get_command_from_trigger(self, trigger): + + commstring = trigger.args[1] + + if commstring.startswith(tuple(self.config.prefix_list)): + command = commstring[1:].split(" ")[0] + elif commstring.startswith(self.bot.nick): + command = " ".join(commstring.split(" ")[1:]).split(" ")[0] + elif "intent" in trigger.tags and trigger.tags["intent"] == "ACTION": + command = commstring.split(" ")[0] + else: + command = "" + + return command + + def what_command_type(self, trigger): + full_trigger_str = trigger.args[1] + if full_trigger_str.startswith(tuple(self.config.prefix_list)): + return "command" + elif full_trigger_str.startswith(self.bot.nick): + return "nickname_command" + elif "intent" in trigger.tags and trigger.tags["intent"] == "ACTION": + return "action_command" + else: + return "rule" + + def is_real_command(self, trigger_dict): + + if trigger_dict["trigger_type"] == "command": + commands_list = self.valid_sopel_commands + elif trigger_dict["trigger_type"] == "nickname_command": + commands_list = self.valid_sopel_nickname_commands + elif trigger_dict["trigger_type"] == "action_command": + commands_list = self.valid_sopel_action_commands + + if trigger_dict["trigger_command"] in commands_list: + return True + else: + return False + + def is_rulematch(self, function, command_type): + """Determine if function could be called with a rule match""" + rulematch = False + if command_type == "command": + if hasattr(function, 'commands'): + command_aliases = function.commands + elif command_type == "nickname_command": + if hasattr(function, 'nickname_commands'): + command_aliases = function.nickname_commands + elif command_type == "action_command": + if hasattr(function, 'action_commands'): + command_aliases = function.action_commands + if '(.*)' in command_aliases: + rulematch = True + return rulematch + + def get_commands_nosplit(self, trigger): + commands = [] + first_full_trigger_str = trigger.args[1] + + if first_full_trigger_str.startswith(tuple(self.config.prefix_list)): + first_trigger_type = "command" + first_trigger_prefix = first_full_trigger_str[0] + first_trigger_noprefix = first_full_trigger_str[1:] + first_trigger_command = first_trigger_noprefix.split(" ")[0] + first_trigger_str = " ".join([x.strip() for x in first_trigger_noprefix.split(" ")[1:]]) + elif first_full_trigger_str.startswith(self.bot.nick): + first_trigger_type = "nickname_command" + first_trigger_prefix = str(first_full_trigger_str.split(" ")[0]) + first_trigger_noprefix = " ".join(first_full_trigger_str.split(" ")[1:]) + first_trigger_command = first_trigger_noprefix.split(" ")[0] + first_trigger_str = " ".join([x.strip() for x in first_trigger_noprefix.split(" ")[1:]]) + elif "intent" in trigger.tags and trigger.tags["intent"] == "ACTION": + first_trigger_type = "action_command" + first_trigger_prefix = "ACTION" + first_trigger_noprefix = first_full_trigger_str + first_trigger_command = first_trigger_noprefix.split(" ")[0] + first_trigger_str = " ".join([x.strip() for x in first_trigger_noprefix.split(" ")[1:]]) + else: + first_trigger_type = "rule" + first_trigger_prefix = None + first_trigger_noprefix = first_full_trigger_str + first_trigger_command = first_trigger_noprefix.split(" ")[0] + first_trigger_str = " ".join([x.strip() for x in first_trigger_noprefix.split(" ")[1:]]) + + commands.append({ + "trigger_type": first_trigger_type, + "trigger_prefix": first_trigger_prefix, + "trigger_str": first_trigger_str, + "trigger_command": first_trigger_command, + "trigger_hostmask": trigger.hostmask, + "trigger_sender": trigger.sender, + "trigger_time": str(trigger.time) + }) + + return commands + + def get_commands_split(self, trigger, splitkey=None): + commands = [] + + # Get split for multiple commands + if splitkey in trigger.args[1]: + triggers = [x.strip() for x in trigger.args[1].split(splitkey)] + else: + triggers = [trigger.args[1]] + + first_full_trigger_str = triggers[0] + + if first_full_trigger_str.startswith(tuple(self.config.prefix_list)): + first_trigger_type = "command" + first_trigger_prefix = first_full_trigger_str[0] + first_trigger_noprefix = first_full_trigger_str[1:] + first_trigger_command = first_trigger_noprefix.split(" ")[0] + first_trigger_str = " ".join([x.strip() for x in first_trigger_noprefix.split(" ")[1:]]) + elif first_full_trigger_str.startswith(self.bot.nick): + first_trigger_type = "nickname_command" + first_trigger_prefix = str(first_full_trigger_str.split(" ")[0]) + first_trigger_noprefix = " ".join(first_full_trigger_str.split(" ")[1:]) + first_trigger_command = first_trigger_noprefix.split(" ")[0] + first_trigger_str = " ".join([x.strip() for x in first_trigger_noprefix.split(" ")[1:]]) + elif "intent" in trigger.tags and trigger.tags["intent"] == "ACTION": + first_trigger_type = "action_command" + first_trigger_prefix = "ACTION" + first_trigger_noprefix = first_full_trigger_str + first_trigger_command = first_trigger_noprefix.split(" ")[0] + first_trigger_str = " ".join([x.strip() for x in first_trigger_noprefix.split(" ")[1:]]) + else: + first_trigger_type = "rule" + first_trigger_prefix = None + first_trigger_noprefix = first_full_trigger_str + first_trigger_command = first_trigger_noprefix.split(" ")[0] + first_trigger_str = " ".join([x.strip() for x in first_trigger_noprefix.split(" ")[1:]]) + + commands.append({ + "trigger_type": first_trigger_type, + "trigger_prefix": first_trigger_prefix, + "trigger_str": first_trigger_str.replace(splitkey, ""), + "trigger_command": first_trigger_command.replace(splitkey, ""), + "trigger_hostmask": trigger.hostmask, + "trigger_sender": trigger.sender, + "trigger_time": str(trigger.time) + }) + + if not len(triggers) > 1: + return commands + + for full_trigger_str in triggers[1:]: + + if full_trigger_str.startswith(tuple(self.config.prefix_list)): + trigger_type = "command" + trigger_prefix = full_trigger_str[0] + trigger_noprefix = full_trigger_str[1:] + trigger_command = trigger_noprefix.split(" ")[0] + trigger_str = " ".join([x.strip() for x in trigger_noprefix.split(" ")[1:]]) + elif full_trigger_str.startswith(self.bot.nick): + trigger_type = "nickname_command" + trigger_prefix = str(full_trigger_str.split(" ")[0]) + trigger_noprefix = " ".join(full_trigger_str.split(" ")[1:]) + trigger_command = trigger_noprefix.split(" ")[0] + trigger_str = " ".join([x.strip() for x in trigger_noprefix.split(" ")[1:]]) + elif full_trigger_str.startswith(tuple(["ACTION", "/me"])): + trigger_type = "action_command" + trigger_prefix = "ACTION" + trigger_noprefix = full_trigger_str + trigger_command = trigger_noprefix.split(" ")[0] + trigger_str = " ".join([x.strip() for x in trigger_noprefix.split(" ")[1:]]) + else: + trigger_command = full_trigger_str.split(" ")[0] + trigger_str = " ".join([x.strip() for x in full_trigger_str.split(" ")[1:]]) + + command_types = ["command", "nickname_command", "action_command"] + # Assume same command type until proven otherwise + assumed_trigger_type = first_trigger_type + assumed_trigger_prefix = first_trigger_prefix + + # Still under the assumption that the command is most likely the same type as first command + command_types.remove(assumed_trigger_type) + command_types.insert(0, assumed_trigger_type) + + found = [] + for command_type in command_types: + + if command_type == "command": + commands_list = self.valid_sopel_commands + elif command_type == "nickname_command": + commands_list = self.valid_sopel_nickname_commands + elif command_type == "action_command": + commands_list = self.valid_sopel_action_commands + + if trigger_command in commands_list: + found.append(command_type) + + if len(found): + trigger_type = found[0] + if trigger_type == "command": + trigger_prefix = self.config.prefix_list[0] + elif trigger_type == "nickname_command": + trigger_prefix = "%s," % self.bot.nick + elif trigger_type == "action_command": + trigger_prefix = "ACTION" + else: + trigger_type = assumed_trigger_type + trigger_prefix = assumed_trigger_prefix + + if (not full_trigger_str.isspace() and full_trigger_str not in ['', None] + and not trigger_command.isspace() and trigger_command not in ['', None]): + + commands.append({ + "trigger_type": trigger_type, + "trigger_prefix": trigger_prefix, + "trigger_str": trigger_str.replace(splitkey, ""), + "trigger_command": trigger_command.replace(splitkey, ""), + "trigger_hostmask": trigger.hostmask, + "trigger_sender": trigger.sender, + "trigger_time": str(trigger.time) + }) + + return commands diff --git a/sopel_SpiceBot_Core_1/SBCore/comms/__init__.py b/sopel_SpiceBot_Core_1/SBCore/comms/__init__.py new file mode 100644 index 0000000..1bce535 --- /dev/null +++ b/sopel_SpiceBot_Core_1/SBCore/comms/__init__.py @@ -0,0 +1,253 @@ +# coding=utf8 +from __future__ import unicode_literals, absolute_import, division, print_function + +from sopel.tools import Identifier +from sopel.irc.utils import safe + +import threading +import sys +if sys.version_info.major >= 3: + from collections import abc + +if sys.version_info.major >= 3: + unicode = str + + +class Comms(): + + def __init__(self, config): + self.config = config + + self.backend = None + self.sending = threading.RLock() + self.stack = {} + + self.hostmask = None + + def ircbackend_initialize(self, bot): + self.backend = bot.backend + self.dispatch = bot.dispatch + + def hostmask_set(self, bot): + self.hostmask = bot.hostmask + + @property + def botnick(self): + return self.config.core.nick + + @property + def bothostmask(self): + return self.hostmask + + def write(self, args, text=None): + while not self.backend: + pass + args = [safe(arg) for arg in args] + self.backend.send_command(*args, text=text) + + def get_message_recipientgroups(self, recipients, text_method): + """ + Split recipients into groups based on server capabilities. + This defaults to 4 + Input can be + * unicode string + * a comma-seperated unicode string + * list + * dict_keys handy for list(bot.channels.keys()) + """ + + if sys.version_info.major >= 3: + if isinstance(recipients, abc.KeysView): + recipients = [x for x in recipients] + if isinstance(recipients, dict): + recipients = [x for x in recipients] + + if not isinstance(recipients, list): + recipients = recipients.split(",") + + if not len(recipients): + raise ValueError("Recipients list empty.") + + if text_method == 'NOTICE': + maxtargets = 4 + elif text_method in ['PRIVMSG', 'ACTION']: + maxtargets = 4 + maxtargets = int(maxtargets) + + recipientgroups = [] + while len(recipients): + recipients_part = ','.join(x for x in recipients[-maxtargets:]) + recipientgroups.append(recipients_part) + del recipients[-maxtargets:] + + return recipientgroups + + def get_available_message_bytes(self, recipientgroups, text_method): + """ + Get total available bytes for sending a message line + Total sendable bytes is 512 + * 15 are reserved for basic IRC NOTICE/PRIVMSG and a small buffer. + * The bots hostmask plays a role in this count + Note: if unavailable, we calculate the maximum length of a hostmask + * The recipients we send to also is a factor. Multiple recipients reduces + sendable message length + """ + + if text_method == 'ACTION': + text_method_bytes = (len('PRIVMSG') + + len("\x01ACTION \x01") + ) + else: + text_method_bytes = len(text_method) + + if self.bothostmask: + hostmaskbytes = len((self.bothostmask).encode('utf-8')) + else: + hostmaskbytes = (len((self.botnick).encode('utf-8')) # Bot's NICKLEN + + 1 # (! separator) + + len('~') # (for the optional ~ in user) + + 9 # max username length + + 1 # (@ separator) + + 63 # has a maximum length of 63 characters. + ) + + # find the maximum target group length, and use the max + groupbytes = [] + for recipients_part in recipientgroups: + groupbytes.append(len((recipients_part).encode('utf-8'))) + + max_recipients_bytes = max(groupbytes) + + allowedLength = (512 + - len(':') - hostmaskbytes + - len(' ') - text_method_bytes - len(' ') + - max_recipients_bytes + - len(' :') + - len('\r\n') + ) + + return allowedLength + + def get_sendable_message_list(self, messages, max_length=400): + """Get a sendable ``text`` message list. + :param str txt: unicode string of text to send + :param int max_length: maximum length of the message to be sendable + :return: a tuple of two values, the sendable text and its excess text + We're arbitrarily saying that the max is 400 bytes of text when + messages will be split. Otherwise, we'd have to account for the bot's + hostmask, which is hard. + The `max_length` is the max length of text in **bytes**, but we take + care of unicode 2-bytes characters, by working on the unicode string, + then making sure the bytes version is smaller than the max length. + """ + + if not isinstance(messages, list): + messages = [messages] + + messages_list = [''] + message_padding = 4 * " " + + for message in messages: + if len((messages_list[-1] + message_padding + message).encode('utf-8')) <= max_length: + if messages_list[-1] == '': + messages_list[-1] = message + else: + messages_list[-1] = messages_list[-1] + message_padding + message + else: + text_list = [] + while len(message.encode('utf-8')) > max_length and not message.isspace(): + last_space = message.rfind(' ', 0, max_length) + if last_space == -1: + # No last space, just split where it is possible + splitappend = message[:max_length] + if not splitappend.isspace(): + text_list.append(splitappend) + message = message[max_length:] + else: + # Split at the last best space found + splitappend = message[:last_space] + if not splitappend.isspace(): + text_list.append(splitappend) + message = message[last_space:] + if len(message.encode('utf-8')) and not message.isspace(): + text_list.append(message) + messages_list.extend(text_list) + + return messages_list + + def osd(self, messages, recipients=None, text_method='PRIVMSG', max_messages=-1): + """Send ``text`` as a PRIVMSG, CTCP ACTION, or NOTICE to ``recipients``. + In the context of a triggered callable, the ``recipient`` defaults to + the channel (or nickname, if a private message) from which the message + was received. + By default, unless specified in the configuration file, there is some + built-in flood protection. Messages displayed over 5 times in 2 minutes + will be displayed as '...'. + The ``recipient`` can be in list format or a comma seperated string, + with the ability to send to multiple recipients simultaneously. The + default recipients that the bot will send to is 4 if the IRC server + doesn't specify a limit for TARGMAX. + Text can be sent to this function in either string or list format. + List format will insert as small buffering space between entries in the + list. + There are 512 bytes available in a single IRC message. This includes + hostmask of the bot as well as around 15 bytes of reserved IRC message + type. This also includes the destinations/recipients of the message. + This will split given strings/lists into a displayable format as close + to the maximum 512 bytes as possible. + If ``max_messages`` is given, the split mesage will display in as many + lines specified by this argument. Specifying ``0`` or a negative number + will display without limitation. By default this is set to ``-1`` when + called directly. When called from the say/msg/reply/notice/action it + will default to ``1``. + """ + + if not hasattr(self, 'stack'): + self.stack = {} + + text_method = text_method.upper() + if text_method == 'SAY' or text_method not in ['NOTICE', 'ACTION']: + text_method = 'PRIVMSG' + + recipientgroups = self.get_message_recipientgroups(recipients, text_method) + available_bytes = self.get_available_message_bytes(recipientgroups, text_method) + messages_list = self.get_sendable_message_list(messages, available_bytes) + + if max_messages >= 1: + messages_list = messages_list[:max_messages] + + text_method_orig = text_method + + for recipientgroup in recipientgroups: + text_method = text_method_orig + + recipient_id = Identifier(recipientgroup) + + recipient_stack = self.stack.setdefault(recipient_id, { + 'messages': [], + 'flood_left': 999, + 'dots': 0, + }) + recipient_stack['dots'] = 0 + + with self.sending: + + for text in messages_list: + + if recipient_stack['dots'] <= 3: + if text_method == 'ACTION': + text = '\001ACTION {}\001'.format(text) + self.write(('PRIVMSG', recipientgroup), text) + text_method = 'PRIVMSG' + elif text_method == 'NOTICE': + self.write(('NOTICE', recipientgroup), text) + else: + self.write(('PRIVMSG', recipientgroup), text) + + def __getattr__(self, name): + """ + Quick and dirty shortcuts. Will only get called for undefined attributes. + """ + + if hasattr(self.bot, name): + return eval("self.bot.%s" % name) diff --git a/sopel_SpiceBot_Core_1/SBCore/config/__init__.py b/sopel_SpiceBot_Core_1/SBCore/config/__init__.py new file mode 100644 index 0000000..cb64123 --- /dev/null +++ b/sopel_SpiceBot_Core_1/SBCore/config/__init__.py @@ -0,0 +1,45 @@ +# coding=utf8 +from __future__ import unicode_literals, absolute_import, division, print_function + +import sys +import os + +from sopel.cli.run import build_parser, get_configuration + + +class Config(): + + def __init__(self, script_dir): + self.script_dir = script_dir + + # Load config + self.config = get_configuration(self.get_opts()) + + @property + def basename(self): + return os.path.basename(self.config.filename).rsplit('.', 1)[0] + + @property + def prefix_list(self): + return str(self.config.core.prefix).replace("\\", '').split("|") + + def define_section(self, name, cls_, validate=True): + return self.config.define_section(name, cls_, validate) + + def get_opts(self): + parser = build_parser() + if not len(sys.argv[1:]): + argv = ['legacy'] + else: + argv = sys.argv[1:] + return parser.parse_args(argv) + + def __getattr__(self, name): + ''' will only get called for undefined attributes ''' + """We will try to find a core value, or return None""" + + if hasattr(self.config, name): + return eval("self.config." + name) + + else: + return None diff --git a/sopel_SpiceBot_Core_1/SBCore/database/__init__.py b/sopel_SpiceBot_Core_1/SBCore/database/__init__.py new file mode 100644 index 0000000..7313fe2 --- /dev/null +++ b/sopel_SpiceBot_Core_1/SBCore/database/__init__.py @@ -0,0 +1,294 @@ +# coding=utf8 +from __future__ import unicode_literals, absolute_import, division, print_function + +import json + +from sopel.tools import Identifier + +from sopel.db import SopelDB, NickValues, ChannelValues, PluginValues +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.exc import SQLAlchemyError +BASE = declarative_base() + + +class SpiceDB(object): + + # NICK FUNCTIONS + + def adjust_nick_value(self, nick, key, value): + """Sets the value for a given key to be associated with the nick.""" + nick = Identifier(nick) + value = json.dumps(value, ensure_ascii=False) + nick_id = self.get_nick_id(nick) + session = self.ssession() + try: + result = session.query(NickValues) \ + .filter(NickValues.nick_id == nick_id) \ + .filter(NickValues.key == key) \ + .one_or_none() + # NickValue exists, update + if result: + result.value = float(result.value) + float(value) + session.commit() + # DNE - Insert + else: + new_nickvalue = NickValues(nick_id=nick_id, key=key, value=float(value)) + session.add(new_nickvalue) + session.commit() + except SQLAlchemyError: + session.rollback() + raise + finally: + session.close() + + def adjust_nick_list(self, nick, key, entries, adjustmentdirection): + """Sets the value for a given key to be associated with the nick.""" + nick = Identifier(nick) + if not isinstance(entries, list): + entries = [entries] + entries = json.dumps(entries, ensure_ascii=False) + nick_id = self.get_nick_id(nick) + session = self.ssession() + try: + result = session.query(NickValues) \ + .filter(NickValues.nick_id == nick_id) \ + .filter(NickValues.key == key) \ + .one_or_none() + # NickValue exists, update + if result: + if adjustmentdirection == 'add': + for entry in entries: + if entry not in result.value: + result.value.append(entry) + elif adjustmentdirection == 'del': + for entry in entries: + while entry in result.value: + result.value.remove(entry) + session.commit() + # DNE - Insert + else: + values = [] + if adjustmentdirection == 'add': + for entry in entries: + if entry not in values: + values.append(entry) + elif adjustmentdirection == 'del': + for entry in entries: + while entry in values: + values.remove(entry) + new_nickvalue = NickValues(nick_id=nick_id, key=key, value=values) + session.add(new_nickvalue) + session.commit() + except SQLAlchemyError: + session.rollback() + raise + finally: + session.close() + + # CHANNEL FUNCTIONS + + def adjust_channel_value(self, channel, key, value): + """Sets the value for a given key to be associated with the channel.""" + channel = Identifier(channel).lower() + value = json.dumps(value, ensure_ascii=False) + session = self.ssession() + try: + result = session.query(ChannelValues) \ + .filter(ChannelValues.channel == channel)\ + .filter(ChannelValues.key == key) \ + .one_or_none() + # ChannelValue exists, update + if result: + result.value = float(result.value) + float(value) + session.commit() + # DNE - Insert + else: + new_channelvalue = ChannelValues(channel=channel, key=key, value=float(value)) + session.add(new_channelvalue) + session.commit() + except SQLAlchemyError: + session.rollback() + raise + finally: + session.close() + + def adjust_channel_list(self, channel, key, entries, adjustmentdirection): + """Sets the value for a given key to be associated with the channel.""" + channel = Identifier(channel).lower() + if not isinstance(entries, list): + entries = [entries] + entries = json.dumps(entries, ensure_ascii=False) + session = self.ssession() + try: + result = session.query(ChannelValues) \ + .filter(ChannelValues.channel == channel)\ + .filter(ChannelValues.key == key) \ + .one_or_none() + # ChannelValue exists, update + if result: + if adjustmentdirection == 'add': + for entry in entries: + if entry not in result.value: + result.value.append(entry) + elif adjustmentdirection == 'del': + for entry in entries: + while entry in result.value: + result.value.remove(entry) + session.commit() + # DNE - Insert + else: + values = [] + if adjustmentdirection == 'add': + for entry in entries: + if entry not in values: + values.append(entry) + elif adjustmentdirection == 'del': + for entry in entries: + while entry in values: + values.remove(entry) + new_channelvalue = ChannelValues(channel=channel, key=key, value=values) + session.add(new_channelvalue) + session.commit() + except SQLAlchemyError: + session.rollback() + raise + finally: + session.close() + + # PLUGIN FUNCTIONS + + def adjust_plugin_value(self, plugin, key, value): + """Sets the value for a given key to be associated with the plugin.""" + plugin = plugin.lower() + value = json.dumps(value, ensure_ascii=False) + session = self.ssession() + try: + result = session.query(PluginValues) \ + .filter(PluginValues.plugin == plugin)\ + .filter(PluginValues.key == key) \ + .one_or_none() + # PluginValue exists, update + if result: + result.value = float(result.value) + float(value) + session.commit() + # DNE - Insert + else: + new_pluginvalue = PluginValues(plugin=plugin, key=key, value=float(value)) + session.add(new_pluginvalue) + session.commit() + except SQLAlchemyError: + session.rollback() + raise + finally: + session.close() + + def adjust_plugin_list(self, plugin, key, entries, adjustmentdirection): + """Sets the value for a given key to be associated with the plugin.""" + plugin = plugin.lower() + if not isinstance(entries, list): + entries = [entries] + entries = json.dumps(entries, ensure_ascii=False) + session = self.ssession() + try: + result = session.query(PluginValues) \ + .filter(PluginValues.plugin == plugin)\ + .filter(PluginValues.key == key) \ + .one_or_none() + # PluginValue exists, update + if result: + if adjustmentdirection == 'add': + for entry in entries: + if entry not in result.value: + result.value.append(entry) + elif adjustmentdirection == 'del': + for entry in entries: + while entry in result.value: + result.value.remove(entry) + session.commit() + # DNE - Insert + else: + values = [] + if adjustmentdirection == 'add': + for entry in entries: + if entry not in values: + values.append(entry) + elif adjustmentdirection == 'del': + for entry in entries: + while entry in values: + values.remove(entry) + new_pluginvalue = PluginValues(plugin=plugin, key=key, value=values) + session.add(new_pluginvalue) + session.commit() + except SQLAlchemyError: + session.rollback() + raise + finally: + session.close() + + +class Database(): + + def __init__(self, config): + SopelDB.adjust_nick_value = SpiceDB.adjust_nick_value + SopelDB.adjust_nick_list = SpiceDB.adjust_nick_list + + SopelDB.adjust_channel_value = SpiceDB.adjust_channel_value + SopelDB.adjust_channel_list = SpiceDB.adjust_channel_list + + SopelDB.adjust_plugin_value = SpiceDB.adjust_plugin_value + SopelDB.adjust_plugin_list = SpiceDB.adjust_plugin_list + + self.db = SopelDB(config) + BASE.metadata.create_all(self.db.engine) + + @property + def botnick(self): + return self.config.core.nick + + def __getattr__(self, name): + ''' will only get called for undefined attributes ''' + if hasattr(self.db, name): + return eval("self.db." + name) + else: + return None + + """Nick""" + + def adjust_nick_value(self, nick, key, value): + return self.db.adjust_nick_value(nick, key, value) + + def adjust_nick_list(self, nick, key, entries, adjustmentdirection): + return self.db.adjust_nick_list(nick, key, entries, adjustmentdirection) + + """Bot""" + + def get_bot_value(self, key): + return self.db.get_nick_value(self.botnick, key) + + def set_bot_value(self, key, value): + return self.db.set_nick_value(self.botnick, key, value) + + def delete_bot_value(self, key): + return self.db.delete_nick_value(self.botnick, key) + + def adjust_bot_value(self, key, value): + return self.db.adjust_nick_value(self.botnick, key, value) + + def adjust_bot_list(self, key, entries, adjustmentdirection): + return self.db.adjust_nick_list(self.botnick, key, entries, adjustmentdirection) + + """Channels""" + + def adjust_channel_value(self, channel, key, value): + return self.db.adjust_channel_value(channel, key, value) + + def adjust_channel_list(self, nick, key, entries, adjustmentdirection): + return self.db.adjust_channel_list(nick, key, entries, adjustmentdirection) + + """Plugins""" + + def adjust_plugin_value(self, plugin, key, value): + return self.db.adjust_plugin_value(plugin, key, value) + + def adjust_plugin_list(self, plugin, key, entries, adjustmentdirection): + return self.db.adjust_plugin_list(plugin, key, entries, adjustmentdirection) diff --git a/sopel_SpiceBot_Core_1/SBCore/events/__init__.py b/sopel_SpiceBot_Core_1/SBCore/events/__init__.py new file mode 100644 index 0000000..9c9bed8 --- /dev/null +++ b/sopel_SpiceBot_Core_1/SBCore/events/__init__.py @@ -0,0 +1,147 @@ +# coding=utf8 +from __future__ import unicode_literals, absolute_import, division, print_function +""" +This is the SpiceBot events system. +We utilize the Sopel code for event numbers and +self-trigger the bot into performing actions +""" + +from sopel.trigger import PreTrigger +import functools +import threading +import time + + +class Events(object): + """A dynamic listing of all the notable Bot numeric events. + Events will be assigned a 4-digit number above 1000. + This allows you to do, ``@plugin.event(events.BOT_WELCOME)```` + Triggers handled by this plugin will be processed immediately. + Others will be placed into a queue. + Triggers will be logged by ID and content + """ + + def __init__(self, logger): + self.logger = logger + self.lock = threading.Lock() + + # This is a defined IRC event + self.RPL_WELCOME = '001' + self.RPL_MYINFO = '004' + self.RPL_ISUPPORT = '005' + self.RPL_WHOREPLY = '352' + self.RPL_NAMREPLY = '353' + + # this is an unrealircd event + self.RPL_WHOISREGNICK = '307' + + # These Are Speicebot generated events + self.BOT_UPTIME = time.time() + self.BOT_WELCOME = '1001' + self.BOT_READY = '1002' + self.BOT_CONNECTED = '1003' + self.BOT_LOADED = '1004' + self.BOT_RECONNECTED = '1005' + + self.defaultevents = [self.BOT_WELCOME, self.BOT_READY, self.BOT_CONNECTED, self.BOT_LOADED, self.BOT_RECONNECTED] + self.dict = { + "assigned_IDs": {1000: "BOT_UPTIME", 1001: "BOT_WELCOME", 1002: "BOT_READY", 1003: "BOT_CONNECTED", 1004: "BOT_LOADED", 1005: "BOT_RECONNECTED"}, + "triggers_recieved": {}, + "trigger_queue": [], + "startup_required": [self.BOT_WELCOME, self.BOT_READY, self.BOT_CONNECTED], + "RPL_WELCOME_Count": 0 + } + + def __getattr__(self, name): + ''' will only get called for undefined attributes ''' + self.lock.acquire() + eventnumber = max(list(self.dict["assigned_IDs"].keys())) + 1 + self.dict["assigned_IDs"][eventnumber] = str(name) + setattr(self, name, str(eventnumber)) + self.lock.release() + return str(eventnumber) + + def trigger(self, bot, number, message="SpiceBot_Events"): + pretriggerdict = {"number": str(number), "message": message} + if number in self.defaultevents: + self.dispatch(bot, pretriggerdict) + else: + self.dict["trigger_queue"].append(pretriggerdict) + + def dispatch(self, bot, pretriggerdict): + number = pretriggerdict["number"] + message = pretriggerdict["message"] + pretrigger = PreTrigger( + bot.nick, + ":SpiceBot_Events %s %s :%s" % (number, bot.nick, message) + ) + bot.dispatch(pretrigger) + self.recieved({"number": number, "message": message}) + + def recieved(self, trigger): + self.lock.acquire() + + if isinstance(trigger, dict): + eventnumber = str(trigger["number"]) + message = str(trigger["message"]) + else: + eventnumber = str(trigger.event) + message = trigger.args[1] + self.logger.info('SpiceBot_Events: %s %s' % (eventnumber, message)) + if eventnumber not in self.dict["triggers_recieved"]: + self.dict["triggers_recieved"][eventnumber] = [] + self.dict["triggers_recieved"][eventnumber].append(message) + self.lock.release() + + def check(self, checklist): + if not isinstance(checklist, list): + checklist = [str(checklist)] + for number in checklist: + if str(number) not in list(self.dict["triggers_recieved"].keys()): + return False + return True + + def startup_add(self, startlist): + self.lock.acquire() + if not isinstance(startlist, list): + startlist = [str(startlist)] + for eventitem in startlist: + if eventitem not in self.dict["startup_required"]: + self.dict["startup_required"].append(eventitem) + self.lock.release() + + def startup_check(self): + for number in self.dict["startup_required"]: + if str(number) not in list(self.dict["triggers_recieved"].keys()): + return False + return True + + def startup_debug(self): + not_done = [] + for number in self.dict["startup_required"]: + if str(number) not in list(self.dict["triggers_recieved"].keys()): + not_done.append(int(number)) + reference_not_done = [] + for item in not_done: + reference_not_done.append(str(self.dict["assigned_IDs"][item])) + return reference_not_done + + def check_ready(self, checklist): + def actual_decorator(function): + @functools.wraps(function) + def _nop(*args, **kwargs): + while not self.check(checklist): + pass + return function(*args, **kwargs) + return _nop + return actual_decorator + + def startup_check_ready(self): + def actual_decorator(function): + @functools.wraps(function) + def _nop(*args, **kwargs): + while not self.startup_check(): + pass + return function(*args, **kwargs) + return _nop + return actual_decorator diff --git a/sopel_SpiceBot_Core_1/SBCore/logger/__init__.py b/sopel_SpiceBot_Core_1/SBCore/logger/__init__.py new file mode 100644 index 0000000..1db35ba --- /dev/null +++ b/sopel_SpiceBot_Core_1/SBCore/logger/__init__.py @@ -0,0 +1,18 @@ +from sopel import tools + + +class Logger(): + + def __init__(self): + self.logger = tools.get_logger('SpiceBot') + + def __getattr__(self, name): + """ + Quick and dirty shortcuts. Will only get called for undefined attributes. + """ + + if hasattr(self.logger, name): + return eval("self.logger.%s" % name) + + elif hasattr(self.logger, name.lower()): + return eval("self.logger.%s" % name.lower()) diff --git a/sopel_SpiceBot_Core_1/SBCore/versions/__init__.py b/sopel_SpiceBot_Core_1/SBCore/versions/__init__.py new file mode 100644 index 0000000..25d079e --- /dev/null +++ b/sopel_SpiceBot_Core_1/SBCore/versions/__init__.py @@ -0,0 +1,111 @@ +import os +import sys +import platform +import pathlib +import json +import re + + +class Versions(): + + def __init__(self, config, logger): + self.config = config + self.logger = logger + + self.dict = {} + + self.register_spicebot() + + self.register_env() + + def get_core_versions(self): + returndict = {} + for item in list(self.dict.keys()): + if self.dict[item]["type"] == "SpiceBot": + returndict[item] = self.dict[item].copy() + return returndict + + def register_version(self, item_name, item_version, item_type): + """ + Register a version item. + """ + + self.logger.debug("Registering %s item: %s %s" % (item_type, item_name, item_version)) + self.dict[item_name] = { + "name": item_name, + "version": item_version, + "type": item_type + } + + def register_spicebot(self): + """ + Register core version items. + """ + + version_file = pathlib.Path(self.config.script_dir).joinpath("version.json") + with open(version_file, 'r') as jsonversion: + versions = json.load(jsonversion) + + for key in list(versions.keys()): + self.register_version(key, versions[key], "SpiceBot") + + def is_docker(self): + path = "/proc/self/cgroup" + if not os.path.isfile(path): + return False + with open(path) as f: + for line in f: + if re.match("\d+:[\w=]+:/docker(-[ce]e)?/\w+", line): + return True + return False + + def is_virtualenv(self): + # return True if started from within a virtualenv or venv + base_prefix = getattr(sys, "base_prefix", None) + # real_prefix will return None if not in a virtualenv enviroment or the default python path + real_prefix = getattr(sys, "real_prefix", None) or sys.prefix + return base_prefix != real_prefix + + def register_env(self): + """ + Register env version items. + """ + + self.register_version("Python", sys.version, "env") + if sys.version_info.major == 2 or sys.version_info < (3, 7): + self.logger.error('Error: SpiceBot requires python 3.7+. Do NOT expect support for older versions of python.') + + opersystem = platform.system() + self.register_version("Operating System", opersystem, "env") + + system_alias = platform.release() + self.register_version("OS Release", system_alias, "env") + + if opersystem in ["Linux", "Darwin"]: + + # Linux/Mac + if os.getuid() == 0 or os.geteuid() == 0: + self.logger.warning('Do not run SpiceBot with root privileges.') + + elif opersystem in ["Windows"]: + + # Windows + if os.environ.get("USERNAME") == "Administrator": + self.logger.warning('Do not run SpiceBot as Administrator.') + + else: + # ['Java'] + if not len(opersystem): + os_string = "." + else: + os_string = ": %s" % opersystem + self.logger.warning("Uncommon Operating System, use at your own risk%s" % os_string) + + cpu_type = platform.machine() + self.register_version("CPU Type", cpu_type, "env") + + isvirtualenv = self.is_virtualenv() + self.register_version("Virtualenv", isvirtualenv, "env") + + isdocker = self.is_docker() + self.register_version("Docker", isdocker, "env") diff --git a/sopel_SpiceBot_Core_1/__init__.py b/sopel_SpiceBot_Core_1/__init__.py new file mode 100644 index 0000000..ca32962 --- /dev/null +++ b/sopel_SpiceBot_Core_1/__init__.py @@ -0,0 +1,18 @@ +# coding=utf8 +"""SpiceBot +A Niche Wrapper around Sopel +""" +from __future__ import unicode_literals, absolute_import, division, print_function + +import os +import pathlib + +from .SBCore import SpiceBotCore_OBJ + +SCRIPT_DIR = pathlib.Path(os.path.dirname(os.path.abspath(__file__))) + +sb = SpiceBotCore_OBJ(SCRIPT_DIR) + + +def setup(bot): + sb.setup(bot) diff --git a/sopel_SpiceBot_Core_1/version.json b/sopel_SpiceBot_Core_1/version.json new file mode 100644 index 0000000..ae8c5a5 --- /dev/null +++ b/sopel_SpiceBot_Core_1/version.json @@ -0,0 +1,3 @@ +{ + "SpiceBot": "v0.9.3-beta" +} diff --git a/sopel_SpiceBot_Core_Prerun/__init__.py b/sopel_SpiceBot_Core_Prerun/__init__.py new file mode 100644 index 0000000..e1dc94a --- /dev/null +++ b/sopel_SpiceBot_Core_Prerun/__init__.py @@ -0,0 +1,148 @@ +import functools + +from sopel_SpiceBot_Core_1 import sb + + +def prerun(): + """This decorator is the hub of handling for all SpiceBot Commands""" + + def actual_decorator(function): + + @functools.wraps(function) + def internal_prerun(bot, trigger, *args, **kwargs): + + comrun = ComRun(function, trigger) + + # Since there was more than one command, + # we are going to redispatch commands + # This will give sopel the appearance of recieving individual commands + if comrun.is_multi_command: + if len(comrun.commands) > 1: + for trigger_dict in comrun.commands: + sb.commands.dispatch(trigger_dict) + return + + # If the original trigger is not the same after splits + # so we will now redispatch to help get the correct function passed + if comrun.has_command_been_sanitized: + if comrun.is_pipe_command: + trigger_dict = rebuild_pipes(comrun.commands) + else: + trigger_dict = comrun.command + sb.commands.dispatch(trigger_dict) + return + + if comrun.is_rulematch and comrun.is_real_command: + return + + # Run function + function(bot, trigger, comrun, *args, **kwargs) + + # if not comrun.is_pipe_command: + # bot.say(comrun.say) + + return internal_prerun + return actual_decorator + + +def rebuild_pipes(commands): + + repipe_trigger_dict = commands[0] + + for trigger_dict in commands[1:]: + + if trigger_dict["trigger_type"] == "command": + repipe_trigger_dict["trigger_str"] += " %s %s%s %s" % (sb.commands.pipe_split_key, + trigger_dict["trigger_prefix"], + trigger_dict["trigger_command"], + trigger_dict["trigger_str"]) + + elif trigger_dict["trigger_type"] == "nickname_command": + repipe_trigger_dict["trigger_str"] += " %s %s %s %s" % (sb.commands.pipe_split_key, + trigger_dict["trigger_prefix"], + trigger_dict["trigger_command"], + trigger_dict["trigger_str"]) + + elif trigger_dict["trigger_type"] == "action_command": + repipe_trigger_dict["trigger_str"] += " %s %s %s %s" % (sb.commands.pipe_split_key, + "/me", + trigger_dict["trigger_command"], + trigger_dict["trigger_str"]) + + return repipe_trigger_dict + + +class ComRun(): + + def __init__(self, function, trigger): + self.function = function + self.trigger = trigger + + @property + def is_real_command(self): + return sb.commands.is_real_command(self.command) + + @property + def command_type(self): + return sb.commands.what_command_type(self.trigger) + + @property + def is_rulematch(self): + return sb.commands.is_rulematch(self.function, self.command_type) + + @property + def is_multi_command(self): + if sb.commands.multi_split_key in self.trigger.args[1]: + return True + return False + + @property + def is_pipe_command(self): + if sb.commands.pipe_split_key in self.trigger.args[1]: + return True + return False + + @property + def multi_split_count(self): + if self.is_multi_command: + return len([x.strip() for x in self.trigger.args[1].split(sb.commands.multi_split_key)]) + return "N/A" + + @property + def pipe_split_count(self): + if self.is_pipe_command: + return len([x.strip() for x in self.trigger.args[1].split(sb.commands.pipe_split_key)]) + return "N/A" + + @property + def commands(self): + if self.is_multi_command: + return sb.commands.get_commands_split(self.trigger, sb.commands.multi_split_key) + elif self.is_pipe_command: + return sb.commands.get_commands_split(self.trigger, sb.commands.pipe_split_key) + else: + return sb.commands.get_commands_nosplit(self.trigger) + + @property + def command(self): + return self.commands[0] + + @property + def has_command_been_sanitized(self): + + trigger_command = sb.commands.get_command_from_trigger(self.trigger) + multi_split_count = self.multi_split_count + pipe_split_count = self.pipe_split_count + + if trigger_command != self.command["trigger_command"]: + return True + + elif multi_split_count != "N/A": + if multi_split_count != len(self.commands): + return True + + elif pipe_split_count != "N/A": + if pipe_split_count != len(self.commands): + return True + + return False diff --git a/sopel_SpiceBot_Core_Startup/__init__.py b/sopel_SpiceBot_Core_Startup/__init__.py new file mode 100644 index 0000000..20d3ee0 --- /dev/null +++ b/sopel_SpiceBot_Core_Startup/__init__.py @@ -0,0 +1,81 @@ +# coding=utf8 +"""SpiceBot +A Niche Wrapper around Sopel +""" +from __future__ import unicode_literals, absolute_import, division, print_function + +from threading import Thread + +from sopel import plugin + +from sopel_SpiceBot_Core_1 import sb + + +""" +Events +""" + + +@plugin.event("001") +@plugin.rule('.*') +def welcome_setup_start(bot, trigger): + sb.comms.ircbackend_initialize(bot) + + +@plugin.event(sb.events.BOT_CONNECTED) +@plugin.rule('.*') +def bot_events_start_set_hostmask(bot, trigger): + sb.comms.hostmask_set(bot) + + +@plugin.event(sb.events.BOT_WELCOME, sb.events.BOT_READY, sb.events.BOT_CONNECTED, sb.events.BOT_LOADED) +@plugin.rule('.*') +def bot_events_complete(bot, trigger): + """This is here simply to log to stderr that this was recieved.""" + sb.logger.info('SpiceBot_Events: %s' % trigger.args[1]) + + +@plugin.event(sb.events.RPL_WELCOME) +@plugin.rule('.*') +def bot_events_connected(bot, trigger): + + # Handling for connection count + sb.events.dict["RPL_WELCOME_Count"] += 1 + if sb.events.dict["RPL_WELCOME_Count"] > 1: + sb.events.trigger(bot, sb.events.BOT_RECONNECTED, "Bot ReConnected to IRC") + else: + sb.events.trigger(bot, sb.events.BOT_WELCOME, "Welcome to the SpiceBot Events System") + + """For items tossed in a queue, this will trigger them accordingly""" + Thread(target=events_thread, args=(bot,)).start() + + +def events_thread(bot): + while True: + if len(sb.events.dict["trigger_queue"]): + pretriggerdict = sb.events.dict["trigger_queue"][0] + sb.events.dispatch(bot, pretriggerdict) + try: + del sb.events.dict["trigger_queue"][0] + except IndexError: + pass + + +@plugin.event(sb.events.BOT_WELCOME) +@plugin.rule('.*') +def bot_events_start(bot, trigger): + """This stage is redundant, but shows the system is working.""" + sb.events.trigger(bot, sb.events.BOT_READY, "Ready To Process plugin setup procedures") + + """Here, we wait until we are in at least one channel""" + while not len(list(bot.channels.keys())) > 0: + pass + sb.events.trigger(bot, sb.events.BOT_CONNECTED, "Bot Connected to IRC") + + +@sb.events.startup_check_ready() +@plugin.event(sb.events.BOT_READY) +@plugin.rule('.*') +def bot_events_startup_complete(bot, trigger): + """All events registered as required for startup have completed""" + sb.events.trigger(bot, sb.events.BOT_LOADED, "All registered plugins setup procedures have completed") diff --git a/sopel_SpiceBot_Runtime_Action_Commands/__init__.py b/sopel_SpiceBot_Runtime_Action_Commands/__init__.py new file mode 100644 index 0000000..ea957b1 --- /dev/null +++ b/sopel_SpiceBot_Runtime_Action_Commands/__init__.py @@ -0,0 +1,13 @@ + + +from sopel import plugin + +from sopel_SpiceBot_Core_1 import sb + +from sopel_SpiceBot_Core_Prerun import prerun + + +@prerun() +@plugin.action_command('test') +def sb_test_commands(bot, trigger, comrun): + bot.say("%s" % trigger.raw) diff --git a/sopel_SpiceBot_Runtime_Commands/__init__.py b/sopel_SpiceBot_Runtime_Commands/__init__.py new file mode 100644 index 0000000..3bc265f --- /dev/null +++ b/sopel_SpiceBot_Runtime_Commands/__init__.py @@ -0,0 +1,32 @@ + + +from sopel import plugin + +from sopel_SpiceBot_Core_Prerun import prerun + + +@prerun() +@plugin.command('test', "testnew") +def commands_test(bot, trigger, comrun): + bot.say("%s" % trigger.raw) + + +@prerun() +@plugin.command('testa') +def commands_test_a(bot, trigger, comrun): + bot.say("test a") + bot.say("%s" % trigger.raw) + + +@prerun() +@plugin.command('testb') +def commands_test_b(bot, trigger, comrun): + bot.say("test b") + bot.say("%s" % trigger.raw) + + +@plugin.command('testc') +def commands_test_c(bot, trigger, comrun): + bot.say("test c") + + bot.say("test c: %s" % trigger.raw) diff --git a/sopel_SpiceBot_Runtime_Nickname_Commands/__init__.py b/sopel_SpiceBot_Runtime_Nickname_Commands/__init__.py new file mode 100644 index 0000000..7a1a7c8 --- /dev/null +++ b/sopel_SpiceBot_Runtime_Nickname_Commands/__init__.py @@ -0,0 +1,44 @@ + +from sopel import plugin + +from sopel_SpiceBot_Core_1 import sb + +from sopel_SpiceBot_Core_Prerun import prerun + + +@prerun() +@plugin.nickname_command('test') +def sb_test_commands(bot, trigger, comrun): + bot.say("%s" % trigger.raw) + + +@plugin.nickname_command('plugins') +def sb_test_command_groups(bot, trigger, comrun): + for bplugin in list(bot._plugins.keys()): + sb.osd(str(bot._plugins[bplugin].get_meta_description()), trigger.sender) + + +@prerun() +@plugin.nickname_command('commands') +def sopel_commands(bot, trigger, comrun): + + bot.say("testing commands") + + sb.osd("%s" % sb.commands.sopel_commands, trigger.sender) + + +@prerun() +@plugin.nickname_command('nickname_commands') +def sopel_nickname_commands(bot, trigger, comrun): + bot.say("testing nickname_commands") + + sb.osd("%s" % sb.commands.sopel_nickname_commands, trigger.sender) + + +@prerun() +@plugin.nickname_command('action_commands') +def sopel_action_commands(bot, trigger, comrun): + + bot.say("testing action_commands") + + sb.osd("%s" % sb.commands.sopel_action_commands, trigger.sender) diff --git a/sopel_SpiceBot_Runtime_Unmatched_Commands/__init__.py b/sopel_SpiceBot_Runtime_Unmatched_Commands/__init__.py new file mode 100644 index 0000000..7a3324b --- /dev/null +++ b/sopel_SpiceBot_Runtime_Unmatched_Commands/__init__.py @@ -0,0 +1,22 @@ + +from sopel import plugin + +from sopel_SpiceBot_Core_Prerun import prerun + + +@prerun() +@plugin.command('(.*)') +def rule_command(bot, trigger, comrun): + bot.say("%s" % trigger.raw) + + +@prerun() +@plugin.nickname_command('(.*)') +def rule_nickname_command(bot, trigger, comrun): + bot.say("%s" % trigger.raw) + + +@prerun() +@plugin.action_command('(.*)') +def rule_action_command(bot, trigger, comrun): + bot.say("%s" % trigger.raw) diff --git a/spicebot_command_lower/__init__.py b/spicebot_command_lower/__init__.py new file mode 100644 index 0000000..d657707 --- /dev/null +++ b/spicebot_command_lower/__init__.py @@ -0,0 +1,12 @@ + + +from sopel import plugin + +from sopel_SpiceBot_Core_Prerun import prerun + + +@prerun() +@plugin.nickname_command('lower') +@plugin.command('lower') +def lower(bot, trigger, comrun): + comrun.say = str(comrun.trigger_dict["trigger_str"]).lower() diff --git a/spicebot_command_upper/__init__.py b/spicebot_command_upper/__init__.py new file mode 100644 index 0000000..42c2642 --- /dev/null +++ b/spicebot_command_upper/__init__.py @@ -0,0 +1,11 @@ + + +from sopel import plugin + +from sopel_SpiceBot_Core_Prerun import prerun + + +@prerun() +@plugin.command('upper') +def upper(bot, trigger, comrun): + comrun.say = str(comrun.trigger_dict["trigger_str"]).upper() diff --git a/spicemanip/__init__.py b/spicemanip/__init__.py new file mode 100644 index 0000000..6add5fb --- /dev/null +++ b/spicemanip/__init__.py @@ -0,0 +1,334 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +"""Top-level package for spicemanip.""" + +__author__ = """Sam Zick""" +__email__ = 'sam@deathbybandaid.net' +__version__ = '0.1.8' + +import random +import collections + +# TODO 'this*that' or '1*that' replace either all strings matching, or an index value +# TODO reverse sort z.sort(reverse = True) +# list.extend adds lists to eachother + + +class Spicemanip(): + + def __init__(self): + pass + + def __call__(self, inputs, outputtask, output_type='default'): + + mainoutputtask, suboutputtask = None, None + + # Input needs to be a list, but don't split a word into letters + if not inputs: + inputs = [] + if isinstance(inputs, collections.abc.KeysView): + inputs = list(inputs) + elif isinstance(inputs, dict): + inputs = list(inputs.keys()) + if not isinstance(inputs, list): + inputs = list(inputs.split(" ")) + inputs = [x for x in inputs if x and x not in ['', ' ']] + inputs = [inputspart.strip() for inputspart in inputs] + + # Create return + if outputtask == 'create': + return inputs + + # Make temparray to preserve original order + temparray = [] + for inputpart in inputs: + temparray.append(inputpart) + inputs = temparray + + # Convert outputtask to standard + if outputtask in [0, 'complete']: + outputtask = 'string' + elif outputtask == 'index': + mainoutputtask = inputs[1] + suboutputtask = inputs[2] + inputs = inputs[0] + elif str(outputtask).isdigit(): + mainoutputtask, outputtask = int(outputtask), 'number' + elif "^" in str(outputtask): + mainoutputtask = str(outputtask).split("^", 1)[0] + suboutputtask = str(outputtask).split("^", 1)[1] + outputtask = 'rangebetween' + if int(suboutputtask) < int(mainoutputtask): + mainoutputtask, suboutputtask = suboutputtask, mainoutputtask + elif str(outputtask).startswith("split_"): + mainoutputtask = str(outputtask).replace("split_", "") + outputtask = 'split' + elif str(outputtask).endswith(tuple(["!", "+", "-", "<", ">"])): + mainoutputtask = str(outputtask) + if str(outputtask).endswith("!"): + outputtask = 'exclude' + if str(outputtask).endswith("+"): + outputtask = 'incrange_plus' + if str(outputtask).endswith("-"): + outputtask = 'incrange_minus' + if str(outputtask).endswith(">"): + outputtask = 'excrange_plus' + if str(outputtask).endswith("<"): + outputtask = 'excrange_minus' + for r in (("!", ""), ("+", ""), ("-", ""), ("<", ""), (">", "")): + mainoutputtask = mainoutputtask.replace(*r) + if mainoutputtask == 'last': + mainoutputtask = len(inputs) + + if outputtask == 'string': + returnvalue = inputs + else: + returnvalue = eval( + 'self.' + outputtask + + '(inputs, outputtask, mainoutputtask, suboutputtask)') + + # default return if not specified + if output_type == 'default': + if outputtask in [ + 'string', 'number', 'rangebetween', 'exclude', 'random', + 'incrange_plus', 'incrange_minus', 'excrange_plus', + 'excrange_minus' + ]: + output_type = 'string' + elif outputtask in ['count']: + output_type = 'dict' + + # verify output is correct + if output_type == 'return': + return returnvalue + if output_type == 'string': + if isinstance(returnvalue, list): + returnvalue = ' '.join(returnvalue) + elif output_type in ['list', 'array']: + if not isinstance(returnvalue, list): + returnvalue = list(returnvalue.split(" ")) + returnvalue = [x for x in returnvalue if x and x not in ['', ' ']] + returnvalue = [inputspart.strip() for inputspart in returnvalue] + return returnvalue + + # compare 2 lists, based on the location of an index item, passthrough needs to be [indexitem, arraytoindex, arraytocompare] + def index(self, indexitem, outputtask, arraytoindex, arraytocompare): + item = '' + for x, y in zip(arraytoindex, arraytocompare): + if x == indexitem: + item = y + return item + + # split list by string + def split(self, inputs, outputtask, mainoutputtask, suboutputtask): + split_array = [] + restring = ' '.join(inputs) + if mainoutputtask not in inputs: + split_array = [restring] + else: + split_array = restring.split(mainoutputtask) + split_array = [x for x in split_array if x and x not in ['', ' ']] + split_array = [inputspart.strip() for inputspart in split_array] + if split_array == []: + split_array = [[]] + return split_array + + # dedupe list + def dedupe(self, inputs, outputtask, mainoutputtask, suboutputtask): + newlist = [] + for inputspart in inputs: + if inputspart not in newlist: + newlist.append(inputspart) + return newlist + + # Sort list + def sort(self, inputs, outputtask, mainoutputtask, suboutputtask): + return sorted(inputs) + + # reverse sort list + def rsort(self, inputs, outputtask, mainoutputtask, suboutputtask): + return sorted(inputs)[::-1] + + # count items in list, return dictionary + def count(self, inputs, outputtask, mainoutputtask, suboutputtask): + returndict = dict() + if not len(inputs): + return returndict + uniqueinputitems, uniquecount = [], [] + for inputspart in inputs: + if inputspart not in uniqueinputitems: + uniqueinputitems.append(inputspart) + for uniqueinputspart in uniqueinputitems: + count = 0 + for ele in inputs: + if (ele == uniqueinputspart): + count += 1 + uniquecount.append(count) + for inputsitem, unumber in zip(uniqueinputitems, uniquecount): + returndict[inputsitem] = unumber + return returndict + + # random item from list + def random(self, inputs, outputtask, mainoutputtask, suboutputtask): + if not len(inputs): + return '' + randomselectlist = [] + for temppart in inputs: + randomselectlist.append(temppart) + while len(randomselectlist) > 1: + random.shuffle(randomselectlist) + randomselect = randomselectlist[random.randint( + 0, + len(randomselectlist) - 1)] + randomselectlist.remove(randomselect) + randomselect = randomselectlist[0] + return randomselect + + # remove random item from list + def exrandom(self, inputs, outputtask, mainoutputtask, suboutputtask): + if not len(inputs): + return [] + randremove = self.random(inputs, outputtask, mainoutputtask, suboutputtask) + inputs.remove(randremove) + return inputs + + # Convert list into lowercase + def lower(self, inputs, outputtask, mainoutputtask, suboutputtask): + if not len(inputs): + return '' + return [inputspart.lower() for inputspart in inputs] + + # Convert list to uppercase + def upper(self, inputs, outputtask, mainoutputtask, suboutputtask): + if not len(inputs): + return '' + return [inputspart.upper() for inputspart in inputs] + + # Convert list to uppercase + def title(self, inputs, outputtask, mainoutputtask, suboutputtask): + if not len(inputs): + return '' + return [inputspart.title() for inputspart in inputs] + + # Reverse List Order + def reverse(self, inputs, outputtask, mainoutputtask, suboutputtask): + if not len(inputs): + return [] + return inputs[::-1] + + # comma seperated list + def list(self, inputs, outputtask, mainoutputtask, suboutputtask): + if not len(inputs): + return '' + return ', '.join(str(x) for x in inputs) + + def list_nospace(self, inputs, outputtask, mainoutputtask, suboutputtask): + if not len(inputs): + return '' + return ','.join(str(x) for x in inputs) + + # comma seperated list with and + def andlist(self, inputs, outputtask, mainoutputtask, suboutputtask): + if not len(inputs): + return '' + if len(inputs) < 2: + return ' '.join(inputs) + lastentry = str("and " + str(inputs[len(inputs) - 1])) + del inputs[-1] + inputs.append(lastentry) + if len(inputs) == 2: + return ' '.join(inputs) + return ', '.join(str(x) for x in inputs) + + # comma seperated list with or + def orlist(self, inputs, outputtask, mainoutputtask, suboutputtask): + if not len(inputs): + return '' + if len(inputs) < 2: + return ' '.join(inputs) + lastentry = str("or " + str(inputs[len(inputs) - 1])) + del inputs[-1] + inputs.append(lastentry) + if len(inputs) == 2: + return ' '.join(inputs) + return ', '.join(str(x) for x in inputs) + + # exclude number + def exclude(self, inputs, outputtask, mainoutputtask, suboutputtask): + if not len(inputs): + return '' + del inputs[int(mainoutputtask) - 1] + return ' '.join(inputs) + + # Convert list to string + def string(self, inputs, outputtask, mainoutputtask, suboutputtask): + if not len(inputs): + return '' + return ' '.join(inputs) + + # Get number item from list + def number(self, inputs, outputtask, mainoutputtask, suboutputtask): + if not len(inputs): + return '' + elif int(mainoutputtask) > len(inputs) or int(mainoutputtask) < 0: + return '' + else: + return inputs[int(mainoutputtask) - 1] + + # Get Last item from list + def last(self, inputs, outputtask, mainoutputtask, suboutputtask): + if not len(inputs): + return '' + return inputs[len(inputs) - 1] + + # range between items in list + def rangebetween(self, inputs, outputtask, mainoutputtask, suboutputtask): + if not len(inputs): + return '' + if not str(mainoutputtask).isdigit() or not str( + suboutputtask).isdigit(): + return '' + mainoutputtask, suboutputtask = int(mainoutputtask), int(suboutputtask) + if suboutputtask == mainoutputtask: + return self.number(inputs, outputtask, mainoutputtask, suboutputtask) + if suboutputtask < mainoutputtask: + return [] + if mainoutputtask < 0: + mainoutputtask = 1 + if suboutputtask > len(inputs): + suboutputtask = len(inputs) + newlist = [] + for i in range(mainoutputtask, suboutputtask + 1): + newlist.append( + str( + self.number(inputs, outputtask, i, suboutputtask))) + if newlist == []: + return '' + return ' '.join(newlist) + + # Forward Range includes index number + def incrange_plus(self, inputs, outputtask, mainoutputtask, suboutputtask): + if not len(inputs): + return '' + return self.rangebetween(inputs, outputtask, int(mainoutputtask), len(inputs)) + + # Reverse Range includes index number + def incrange_minus(self, inputs, outputtask, mainoutputtask, suboutputtask): + if not len(inputs): + return '' + return self.rangebetween(inputs, outputtask, 1, int(mainoutputtask)) + + # Forward Range excludes index number + def excrange_plus(self, inputs, outputtask, mainoutputtask, suboutputtask): + if not len(inputs): + return '' + return self.rangebetween(inputs, outputtask, int(mainoutputtask) + 1, len(inputs)) + + # Reverse Range excludes index number + def excrange_minus(self, inputs, outputtask, mainoutputtask, suboutputtask): + if not len(inputs): + return '' + return self.rangebetween(inputs, outputtask, 1, int(mainoutputtask) - 1) + + +spicemanip = Spicemanip()