diff --git a/sopel_modules/SpiceBot/Database.py b/sopel_modules/SpiceBot/Database.py index 8d667f6..8ede899 100644 --- a/sopel_modules/SpiceBot/Database.py +++ b/sopel_modules/SpiceBot/Database.py @@ -20,6 +20,9 @@ import threading BASE = declarative_base() +MYSQL_TABLE_ARGS = {'mysql_engine': 'InnoDB', + 'mysql_charset': 'utf8mb4', + 'mysql_collate': 'utf8mb4_unicode_ci'} class NickIDs(BASE): @@ -35,7 +38,7 @@ class Nicknames(BASE): Nicknames SQLAlchemy Class """ __tablename__ = 'spice_nicknames' - __table_args__ = {'mysql_engine': 'InnoDB', 'mysql_charset': 'utf8mb4', 'mysql_collate': 'utf8mb4_unicode_ci'} + __table_args__ = MYSQL_TABLE_ARGS nick_id = Column(Integer, ForeignKey('spice_nick_ids.nick_id'), primary_key=True) slug = Column(String(255), primary_key=True) canonical = Column(String(255)) @@ -46,7 +49,7 @@ class NickValues(BASE): NickValues SQLAlchemy Class """ __tablename__ = 'spice_nick_values' - __table_args__ = {'mysql_engine': 'InnoDB', 'mysql_charset': 'utf8mb4', 'mysql_collate': 'utf8mb4_unicode_ci'} + __table_args__ = MYSQL_TABLE_ARGS nick_id = Column(Integer, ForeignKey('spice_nick_ids.nick_id'), primary_key=True) namespace = Column(String(255), primary_key=True) key = Column(String(255), primary_key=True) @@ -58,19 +61,31 @@ class ChannelValues(BASE): ChannelValues SQLAlchemy Class """ __tablename__ = 'spice_channel_values' - __table_args__ = {'mysql_engine': 'InnoDB', 'mysql_charset': 'utf8mb4', 'mysql_collate': 'utf8mb4_unicode_ci'} + __table_args__ = MYSQL_TABLE_ARGS channel = Column(String(255), primary_key=True) namespace = Column(String(255), primary_key=True) key = Column(String(255), primary_key=True) value = Column(Text()) +class ServerValues(BASE): + """ + ServerValues SQLAlchemy Class + """ + __tablename__ = 'spice_server_values' + __table_args__ = MYSQL_TABLE_ARGS + server = Column(String(255), primary_key=True) + namespace = Column(String(255), primary_key=True) + key = Column(String(255), primary_key=True) + value = Column(Text()) + + class PluginValues(BASE): """ PluginValues SQLAlchemy Class """ __tablename__ = 'spice_plugin_values' - __table_args__ = {'mysql_engine': 'InnoDB', 'mysql_charset': 'utf8mb4', 'mysql_collate': 'utf8mb4_unicode_ci'} + __table_args__ = MYSQL_TABLE_ARGS plugin = Column(String(255), primary_key=True) namespace = Column(String(255), primary_key=True) key = Column(String(255), primary_key=True) @@ -393,11 +408,148 @@ class SpiceDB(object): finally: session.close() + # SERVER FUNCTIONS + + def set_server_value(self, server, key, value, namespace='default'): + """Sets the value for a given key to be associated with the server.""" + server = server.lower() + value = json.dumps(value, ensure_ascii=False) + session = self.ssession() + try: + result = session.query(ServerValues) \ + .filter(ServerValues.server == server)\ + .filter(ServerValues.namespace == namespace)\ + .filter(ServerValues.key == key) \ + .one_or_none() + # ServerValue exists, update + if result: + result.value = value + session.commit() + # DNE - Insert + else: + new_servervalue = ServerValues(server=server, namespace=namespace, key=key, value=value) + session.add(new_servervalue) + session.commit() + except SQLAlchemyError: + session.rollback() + raise + finally: + session.close() + + def get_server_value(self, server, key, namespace='default'): + """Retrieves the value for a given key associated with a server.""" + server = server.lower() + session = self.ssession() + try: + result = session.query(ServerValues) \ + .filter(ServerValues.server == server)\ + .filter(ServerValues.namespace == namespace)\ + .filter(ServerValues.key == key) \ + .one_or_none() + if result is not None: + result = result.value + return _deserialize(result) + except SQLAlchemyError: + session.rollback() + raise + finally: + session.close() + + def delete_server_value(self, server, key, namespace='default'): + """Sets the value for a given key to be associated with the server.""" + server = server.lower() + session = self.ssession() + try: + result = session.query(ServerValues) \ + .filter(ServerValues.server == server)\ + .filter(ServerValues.namespace == namespace)\ + .filter(ServerValues.key == key) \ + .one_or_none() + # ServerValue exists, delete + if result: + session.delete(result) + session.commit() + except SQLAlchemyError: + session.rollback() + raise + finally: + session.close() + + def adjust_server_value(self, server, key, value, namespace='default'): + """Sets the value for a given key to be associated with the server.""" + server = server.lower() + value = json.dumps(value, ensure_ascii=False) + session = self.ssession() + try: + result = session.query(ServerValues) \ + .filter(ServerValues.server == server)\ + .filter(ServerValues.namespace == namespace)\ + .filter(ServerValues.key == key) \ + .one_or_none() + # ServerValue exists, update + if result: + result.value = float(result.value) + float(value) + session.commit() + # DNE - Insert + else: + new_servervalue = ServerValues(server=server, namespace=namespace, key=key, value=float(value)) + session.add(new_servervalue) + session.commit() + except SQLAlchemyError: + session.rollback() + raise + finally: + session.close() + + def adjust_server_list(self, server, key, entries, adjustmentdirection, namespace='default'): + """Sets the value for a given key to be associated with the server.""" + server = server.lower() + if not isinstance(entries, list): + entries = [entries] + entries = json.dumps(entries, ensure_ascii=False) + session = self.ssession() + try: + result = session.query(ServerValues) \ + .filter(ServerValues.server == server)\ + .filter(ServerValues.namespace == namespace)\ + .filter(ServerValues.key == key) \ + .one_or_none() + # ServerValue 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_servervalue = ServerValues(server=server, namespace=namespace, key=key, value=values) + session.add(new_servervalue) + session.commit() + except SQLAlchemyError: + session.rollback() + raise + finally: + session.close() + # PLUGIN FUNCTIONS def set_plugin_value(self, plugin, key, value, namespace='default'): """Sets the value for a given key to be associated with the plugin.""" - plugin = Identifier(plugin).lower() + plugin = plugin.lower() value = json.dumps(value, ensure_ascii=False) session = self.ssession() try: @@ -423,7 +575,7 @@ class SpiceDB(object): def get_plugin_value(self, plugin, key, namespace='default'): """Retrieves the value for a given key associated with a plugin.""" - plugin = Identifier(plugin).lower() + plugin = plugin.lower() session = self.ssession() try: result = session.query(PluginValues) \ @@ -442,7 +594,7 @@ class SpiceDB(object): def delete_plugin_value(self, plugin, key, namespace='default'): """Deletes the value for a given key to be associated with the plugin.""" - plugin = Identifier(plugin).lower() + plugin = plugin.lower() session = self.ssession() try: result = session.query(PluginValues) \ @@ -462,7 +614,7 @@ class SpiceDB(object): def adjust_plugin_value(self, plugin, key, value, namespace='default'): """Sets the value for a given key to be associated with the plugin.""" - plugin = Identifier(plugin).lower() + plugin = plugin.lower() value = json.dumps(value, ensure_ascii=False) session = self.ssession() try: @@ -471,7 +623,7 @@ class SpiceDB(object): .filter(PluginValues.namespace == namespace)\ .filter(PluginValues.key == key) \ .one_or_none() - # ChannelValue exists, update + # PluginValue exists, update if result: result.value = float(result.value) + float(value) session.commit() @@ -488,7 +640,7 @@ class SpiceDB(object): def adjust_plugin_list(self, plugin, key, entries, adjustmentdirection, namespace='default'): """Sets the value for a given key to be associated with the plugin.""" - plugin = Identifier(plugin).lower() + plugin = plugin.lower() if not isinstance(entries, list): entries = [entries] entries = json.dumps(entries, ensure_ascii=False) @@ -499,7 +651,7 @@ class SpiceDB(object): .filter(PluginValues.namespace == namespace)\ .filter(PluginValues.key == key) \ .one_or_none() - # ChannelValue exists, update + # PluginValue exists, update if result: if adjustmentdirection == 'add': for entry in entries: @@ -554,6 +706,13 @@ class BotDatabase(): SopelDB.adjust_channel_value = SpiceDB.adjust_channel_value SopelDB.adjust_channel_list = SpiceDB.adjust_channel_list + sopel.db.ServerValues = ServerValues + SopelDB.get_server_value = SpiceDB.get_server_value + SopelDB.set_server_value = SpiceDB.set_server_value + SopelDB.delete_server_value = SpiceDB.delete_server_value + SopelDB.adjust_server_value = SpiceDB.adjust_server_value + SopelDB.adjust_server_list = SpiceDB.adjust_server_list + sopel.db.PluginValues = PluginValues SopelDB.get_plugin_value = SpiceDB.get_plugin_value SopelDB.set_plugin_value = SpiceDB.set_plugin_value @@ -631,6 +790,23 @@ class BotDatabase(): def adjust_channel_list(self, nick, key, entries, adjustmentdirection, namespace='default'): return self.db.adjust_channel_list(nick, key, entries, adjustmentdirection, namespace) + """Servers""" + + def get_server_value(self, _server, key, namespace='default'): + return self.db.get__server_value(_server, key, namespace) + + def set__server_value(self, _server, key, value, namespace='default'): + return self.db.set__server_value(_server, key, value, namespace) + + def delete__server_value(self, _server, key, namespace='default'): + return self.db.delete__server_value(_server, key, namespace) + + def adjust__server_value(self, _server, key, value, namespace='default'): + return self.db.adjust__server_value(_server, key, value, namespace) + + def adjust__server_list(self, nick, key, entries, adjustmentdirection, namespace='default'): + return self.db.adjust__server_list(nick, key, entries, adjustmentdirection, namespace) + """Plugins""" def get_plugin_value(self, plugin, key, namespace='default'): @@ -645,8 +821,8 @@ class BotDatabase(): def adjust_plugin_value(self, plugin, key, value, namespace='default'): return self.db.adjust_plugin_value(plugin, key, value, namespace) - def adjust_plugin_list(self, nick, key, entries, adjustmentdirection, namespace='default'): - return self.db.adjust_plugin_list(nick, key, entries, adjustmentdirection, namespace) + def adjust_plugin_list(self, plugin, key, entries, adjustmentdirection, namespace='default'): + return self.db.adjust_plugin_list(plugin, key, entries, adjustmentdirection, namespace) db = BotDatabase() diff --git a/sopel_modules/SpiceBot/Events.py b/sopel_modules/SpiceBot/Events.py index ac5cfc5..a795e92 100644 --- a/sopel_modules/SpiceBot/Events.py +++ b/sopel_modules/SpiceBot/Events.py @@ -35,8 +35,10 @@ class BotEvents(object): def __init__(self): self.lock = threading.Lock() self.RPL_WELCOME = '001' # This is a defined IRC event - self.RPL_NAMREPLY = '353' # This is a defined IRC event + self.RPL_MYINFO = '004' # This is a defined IRC event + self.RPL_ISUPPORT = '005' # This is a defined IRC event self.RPL_WHOREPLY = '352' # This is a defined IRC event + self.RPL_NAMREPLY = '353' # This is a defined IRC event self.BOT_UPTIME = time.time() self.BOT_WELCOME = '1001' self.BOT_READY = '1002' diff --git a/sopel_modules/SpiceBot/Prerun.py b/sopel_modules/SpiceBot/Prerun.py index 59d9d82..aee4d0d 100644 --- a/sopel_modules/SpiceBot/Prerun.py +++ b/sopel_modules/SpiceBot/Prerun.py @@ -119,6 +119,14 @@ def prerun_query(t_command_type='module', t_command_subtype=None): @functools.wraps(function) def internal_prerun(bot, trigger, *args, **kwargs): + # verify the bot is at a loaded state + while not botevents.check(botevents.BOT_LOADED): + pass + + # Verify channel and user exist + verify_channel(trigger) + verify_user(trigger) + botcom = class_create('botcom') if t_command_type == "nickname": @@ -410,6 +418,7 @@ def trigger_hyphen_args(trigger_args_part): def trigger_hyphen_arg_handler(bot, trigger): # Commands that cannot run via privmsg + # TODO --check should work for commands that don't exist if trigger.sb["hyphen_arg"] in ['check']: if trigger.sb["com"].lower() != trigger.sb["realcom"]: botmessagelog.messagelog(trigger.sb["log_id"], trigger.sb["comtext"] + " is a valid alias command for " + trigger.sb["realcomtext"]) diff --git a/sopel_modules/SpiceBot/Server.py b/sopel_modules/SpiceBot/Server.py new file mode 100644 index 0000000..3502294 --- /dev/null +++ b/sopel_modules/SpiceBot/Server.py @@ -0,0 +1,87 @@ +# coding=utf8 +from __future__ import unicode_literals, absolute_import, division, print_function +""" +This is the SpiceBot Server system. +""" + +from .Config import config as botconfig + + +class BotServer(): + """This Logs all server values of relevance'""" + def __init__(self): + self.linenumber = 0 + self.dict = { + "host_connect": botconfig.core.host, + "host": botconfig.core.host, + } + self.isupport = { + "NETWORK": botconfig.core.host, + "TARGMAX": { + "KICK": 1, + 'NOTICE': 1, + 'PRIVMSG': 1, + }, + } + self.myinfo = { + "servername": botconfig.core.host, + "version": None, + "usermodes": [], + "channelmodes": [] + } + + def rpl_welcome(self, trigger): + self.dict["host"] = str(trigger.sender).lower() + + def parse_reply_myinfo(self, trigger): + self.myinfo["servername"] = trigger.args[1] + self.myinfo["version"] = trigger.args[2] + self.myinfo["usermodes"] = list(trigger.args[3]) + self.myinfo["channelmodes"] = list(trigger.args[4]) + + def parse_reply_isupport(self, trigger): + + # check against 005_Bounce + if trigger.args[-1] != 'are supported by this server': + return + + parameters = trigger.args[1:-1] + for param in parameters: + + # check for value associated with the parameter + if '=' not in param: + self.isupport[str(param)] = None + else: + + key, raw_value = param.split('=') + if ',' not in raw_value: + if str(raw_value).isdigit(): + setting_value = int(raw_value) + self.isupport[str(key)] = raw_value + else: + + if str(key) not in list(self.isupport.keys()): + self.isupport[str(key)] = {} + + if not isinstance(self.isupport[str(key)], dict): + self.isupport[str(key)] = {} + + settings = str(raw_value).split(',') + for setting in settings: + + if ":" not in setting: + self.isupport[str(key)][str(setting)] = None + else: + + setting_name, setting_value = setting.split(":") + try: + setting_value = str(setting).split(':')[1] or None + except IndexError: + setting_value = None + if str(setting_value).isdigit(): + setting_value = int(setting_value) + + self.isupport[str(key)][str(setting_name)] = setting_value + + +server = BotServer() diff --git a/sopel_modules/SpiceBot/Users.py b/sopel_modules/SpiceBot/Users.py index c55c1e8..bd67ca7 100644 --- a/sopel_modules/SpiceBot/Users.py +++ b/sopel_modules/SpiceBot/Users.py @@ -65,6 +65,7 @@ class BotUsers(): except Exception as e: nick_id = e nick_id = None + return None if usercreate and nick_id: self.add_to_all(nick, nick_id) return int(nick_id) diff --git a/sopel_modules/SpiceBot/__init__.py b/sopel_modules/SpiceBot/__init__.py index 7826238..e0836fa 100644 --- a/sopel_modules/SpiceBot/__init__.py +++ b/sopel_modules/SpiceBot/__init__.py @@ -21,6 +21,7 @@ from .Prerun import * from .Commands import * from .Events import * from .StartupMonologue import * +from .Server import * from .Channels import * from .Users import * from .AI import * diff --git a/sopel_modules/SpiceBot/osd.py b/sopel_modules/SpiceBot/osd.py index 7b39f6d..ca892f2 100644 --- a/sopel_modules/SpiceBot/osd.py +++ b/sopel_modules/SpiceBot/osd.py @@ -12,6 +12,8 @@ import time from collections import abc import sys +from .Server import server as botserver + class SpiceBot_OSD(StaticSection): @@ -62,9 +64,9 @@ class ToolsOSD: raise ValueError("Recipients list empty.") if text_method == 'NOTICE': - maxtargets = bot.config.SpiceBot_OSD.notice + maxtargets = botserver.isupport["TARGMAX"]["NOTICE"] elif text_method in ['PRIVMSG', 'ACTION']: - maxtargets = bot.config.SpiceBot_OSD.privmsg + maxtargets = botserver.isupport["TARGMAX"]["PRIVMSG"] maxtargets = int(maxtargets) recipientgroups = [] diff --git a/sopel_modules/SpiceBot_Core_Commands/Database.py b/sopel_modules/SpiceBot_Core_Commands/Database.py index 8a811e8..d2807c2 100644 --- a/sopel_modules/SpiceBot_Core_Commands/Database.py +++ b/sopel_modules/SpiceBot_Core_Commands/Database.py @@ -12,5 +12,5 @@ import sopel_modules.SpiceBot as SpiceBot @SpiceBot.prerun('nickname') @sopel.module.nickname_commands('database') -def bot_command_database(bot, trigger): +def bot_command_database(bot, trigger, botcom): bot.osd("Database is " + SpiceBot.config.db_type) diff --git a/sopel_modules/SpiceBot_Core_Commands/Docs.py b/sopel_modules/SpiceBot_Core_Commands/Docs.py index 9e9090f..0d9883d 100644 --- a/sopel_modules/SpiceBot_Core_Commands/Docs.py +++ b/sopel_modules/SpiceBot_Core_Commands/Docs.py @@ -9,7 +9,7 @@ import sopel_modules.SpiceBot as SpiceBot @SpiceBot.prerun('nickname') @sopel.module.nickname_commands('help', 'docs', 'wiki') -def bot_command_docs(bot, trigger): +def bot_command_docs(bot, trigger, botcom): sb_git_url = str( SpiceBot.github_dict["url_main"] + SpiceBot.github_dict["repo_owner"] + "/" + diff --git a/sopel_modules/SpiceBot_Core_Commands/Server.py b/sopel_modules/SpiceBot_Core_Commands/Server.py new file mode 100644 index 0000000..484892d --- /dev/null +++ b/sopel_modules/SpiceBot_Core_Commands/Server.py @@ -0,0 +1,17 @@ +# coding=utf8 +from __future__ import unicode_literals, absolute_import, division, print_function +""" +This is the SpiceBot Users system. +""" +import sopel + +import sopel_modules.SpiceBot as SpiceBot + + +@SpiceBot.prerun('nickname') +@sopel.module.nickname_commands('server') +def nickname_comand_server(bot, trigger, botcom): + network = SpiceBot.server.isupport["NETWORK"] + server = SpiceBot.server.myinfo["servername"] + version = SpiceBot.server.myinfo["version"] + bot.osd("I am connected to " + str(server) + " on the " + str(network) + " network. This server runs " + str(version)) diff --git a/sopel_modules/SpiceBot_Core_Commands/__init__.py b/sopel_modules/SpiceBot_Core_Commands/__init__.py index 128d453..bcdf439 100644 --- a/sopel_modules/SpiceBot_Core_Commands/__init__.py +++ b/sopel_modules/SpiceBot_Core_Commands/__init__.py @@ -9,6 +9,7 @@ This module contains references only for other modules to utilize. from .CommandsQuery import * from .Commands import * +from .Server import * from .Channels import * from .Users import * from .Database import * diff --git a/sopel_modules/SpiceBot_Core_Processing/Kick.py b/sopel_modules/SpiceBot_Core_Processing/Kick.py index f0586e9..faa5f92 100644 --- a/sopel_modules/SpiceBot_Core_Processing/Kick.py +++ b/sopel_modules/SpiceBot_Core_Processing/Kick.py @@ -5,10 +5,12 @@ This is the SpiceBot OSD system. """ import sopel +import sopel_modules.SpiceBot as SpiceBot -@sopel.module.event('005') + +@sopel.module.event(SpiceBot.events.RPL_ISUPPORT) @sopel.module.rule('.*') -def parse_event_005(bot, trigger): +def parse_event_005_kick(bot, trigger): if trigger.args[-1] != 'are supported by this server': return parameters = trigger.args[1:-1] diff --git a/sopel_modules/SpiceBot_Core_Processing/Server.py b/sopel_modules/SpiceBot_Core_Processing/Server.py new file mode 100644 index 0000000..b5cb6ef --- /dev/null +++ b/sopel_modules/SpiceBot_Core_Processing/Server.py @@ -0,0 +1,26 @@ +# coding=utf8 +from __future__ import unicode_literals, absolute_import, division, print_function +""" +This is the SpiceBot Channels system. +""" +import sopel + +import sopel_modules.SpiceBot as SpiceBot + + +@sopel.module.event(SpiceBot.events.RPL_WELCOME) +@sopel.module.rule('.*') +def server_name(bot, trigger): + SpiceBot.server.rpl_welcome(trigger) + + +@sopel.module.event(SpiceBot.events.RPL_ISUPPORT) +@sopel.module.rule('.*') +def parse_event_005(bot, trigger): + SpiceBot.server.parse_reply_isupport(trigger) + + +@sopel.module.event(SpiceBot.events.RPL_MYINFO) +@sopel.module.rule('.*') +def parse_event_004(bot, trigger): + SpiceBot.server.parse_reply_myinfo(trigger) diff --git a/sopel_modules/SpiceBot_Core_Processing/__init__.py b/sopel_modules/SpiceBot_Core_Processing/__init__.py index ed8f775..108492b 100644 --- a/sopel_modules/SpiceBot_Core_Processing/__init__.py +++ b/sopel_modules/SpiceBot_Core_Processing/__init__.py @@ -7,6 +7,7 @@ This module contains references only for other modules to utilize. """ # pylama:ignore=W0401,W0611 +from .Server import * from .Channels import * from .Users import * from .Events import * diff --git a/sopel_modules/SpiceBot_Core_Processing/osd.py b/sopel_modules/SpiceBot_Core_Processing/osd.py index 88d2bf5..245aeda 100644 --- a/sopel_modules/SpiceBot_Core_Processing/osd.py +++ b/sopel_modules/SpiceBot_Core_Processing/osd.py @@ -6,9 +6,12 @@ This is the SpiceBot OSD system. import sopel -@sopel.module.event('005') +import sopel_modules.SpiceBot as SpiceBot + + +@sopel.module.event(SpiceBot.events.RPL_ISUPPORT) @sopel.module.rule('.*') -def parse_event_005(bot, trigger): +def parse_event_005_osd(bot, trigger): if trigger.args[-1] != 'are supported by this server': return parameters = trigger.args[1:-1]