From 8a80d8e67d0827d23f93fd6e7f693dfe5363c9ac Mon Sep 17 00:00:00 2001 From: jesopo Date: Fri, 31 Aug 2018 15:13:56 +0100 Subject: [PATCH 1/7] Add Database.UserChannelSettings.find_all_by_setting --- Database.py | 20 ++++++++++++++++---- IRCChannel.py | 3 +++ 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/Database.py b/Database.py index c315d801..8ea22b9e 100644 --- a/Database.py +++ b/Database.py @@ -207,13 +207,25 @@ class UserChannelSettings(Table): def find_prefix(self, user_id, channel_id, prefix, default=[]): return self.find_user_settings(user_id, channel_id, "%s%" % prefix, default) - def find_by_setting(self, server_id, user_id, setting, default=[]): + def find_by_setting(self, user_id, setting, default=[]): values = self.database.execute_fetchall( """SELECT channels.name, user_channel_settings.value FROM user_channel_settings INNER JOIN channels ON user_channel_settings.channel_id=channels.channel_id - WHERE channels.server_id=? AND - user_channel_settings.user_id=?""", [server_id, user_id]) + WHERE user_channel_settings.setting=? + AND user_channel_settings.user_id=?""", [setting, user_id]) + if values: + for i, value in enumerate(values): + values[i] = value[0], json.loads(value[1]) + return values + return default + def find_all_by_setting(self, channel_id, setting, default=[]): + values = self.database.execute_fetchall( + """SELECT users.name, user_channel_settings.value FROM + user_channel_settings INNER JOIN users ON + user_channel_settings.user_id=users.user_id + WHERE user_channel_setting.setting=? + AND user_channel_settings.channel_id=?""", [setting, channel_id]) if values: for i, value in enumerate(values): values[i] = value[0], json.loads(value[1]) @@ -354,4 +366,4 @@ class Database(object): DELETE CASCADE, PRIMARY KEY (user_id, channel_id, setting))""") self.execute("""CREATE INDEX user_channel_settings_index - ON user_channel_settings (user_id, channel_id, setting)""") \ No newline at end of file + ON user_channel_settings (user_id, channel_id, setting)""") diff --git a/IRCChannel.py b/IRCChannel.py index 15d2e5df..11a5bf9e 100644 --- a/IRCChannel.py +++ b/IRCChannel.py @@ -99,6 +99,9 @@ class Channel(object): def del_user_setting(self, user_id, setting): self.bot.database.user_channel_settings.delete(user_id, self.id, setting) + def find_all_by_setting(self, setting, default=[]): + return self.bot.database.user_channel_settings.find_all_by_setting( + self.id, setting, default) def send_message(self, text, prefix=None): self.server.send_message(self.name, text, prefix=prefix) From 303cf85ac6a74395d4337307af408d2ec25707f5 Mon Sep 17 00:00:00 2001 From: jesopo Date: Fri, 31 Aug 2018 15:17:15 +0100 Subject: [PATCH 2/7] Turns out we didn't need find_all_by_setting --- Database.py | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/Database.py b/Database.py index 8ea22b9e..80444cef 100644 --- a/Database.py +++ b/Database.py @@ -219,18 +219,6 @@ class UserChannelSettings(Table): values[i] = value[0], json.loads(value[1]) return values return default - def find_all_by_setting(self, channel_id, setting, default=[]): - values = self.database.execute_fetchall( - """SELECT users.name, user_channel_settings.value FROM - user_channel_settings INNER JOIN users ON - user_channel_settings.user_id=users.user_id - WHERE user_channel_setting.setting=? - AND user_channel_settings.channel_id=?""", [setting, channel_id]) - if values: - for i, value in enumerate(values): - values[i] = value[0], json.loads(value[1]) - return values - return default def delete(self, user_id, channel_id, setting): self.database.execute( """DELETE FROM user_channel_settings WHERE From 525de94c02db1535ea21179fae09f3e3ba2d8f8b Mon Sep 17 00:00:00 2001 From: jesopo Date: Fri, 31 Aug 2018 15:34:56 +0100 Subject: [PATCH 3/7] Actually, we do need find_all_by_setting --- Database.py | 14 ++++++++++++++ IRCServer.py | 5 ++++- IRCUser.py | 2 +- 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/Database.py b/Database.py index 80444cef..a0aa3529 100644 --- a/Database.py +++ b/Database.py @@ -219,6 +219,20 @@ class UserChannelSettings(Table): values[i] = value[0], json.loads(value[1]) return values return default + def find_all_by_setting(self, server_id, setting, default=[]): + values = self.database.execute_fetchall( + """SELECT channels.name, users.nickname, + user_channel_settings.value FROM + user_channel_settings INNER JOIN channels ON + user_channel_settings.channel_id=channels.channel_id + INNER JOIN users on user_channel_setting.user_id=users.user_id + WHERE user_channel_settings.setting=? AND + users.server_id=?""", [setting, server_id]) + if values: + for i, value in enumerate(values): + values[i] = value[0], value[1], json.loads(value[2]) + return values + return default def delete(self, user_id, channel_id, setting): self.database.execute( """DELETE FROM user_channel_settings WHERE diff --git a/IRCServer.py b/IRCServer.py index 5ed33baf..34358e44 100644 --- a/IRCServer.py +++ b/IRCServer.py @@ -112,9 +112,12 @@ class Server(object): self.id, prefix, default) def del_setting(self, setting): self.bot.database.server_settings.delete(self.id, setting) - def get_all_user_settings(self, setting, default): + def get_all_user_settings(self, setting, default=[]): return self.bot.database.user_settings.find_all_by_setting( self.id, setting, default) + def find_all_user_channel_settings(self, setting, default=[]): + return self.bot.database.user_channel_settings.find_all_by_setting( + self.id, setting, default) def set_own_nickname(self, nickname): self.nickname = nickname diff --git a/IRCUser.py b/IRCUser.py index c99372db..ea54bf6b 100644 --- a/IRCUser.py +++ b/IRCUser.py @@ -39,7 +39,7 @@ class User(object): self.bot.database.user_settings.delete(self.id, setting) def get_channel_settings_per_setting(self, setting, default=[]): return self.bot.database.user_channel_settings.find_by_setting( - self.server.id, self.id, setting, default) + self.id, setting, default) def send_message(self, message, prefix=None): self.server.send_message(self.nickname, message, prefix=prefix) From 1cafc3462adea53df7175f07ef8d191a687d6b72 Mon Sep 17 00:00:00 2001 From: jesopo Date: Fri, 31 Aug 2018 16:02:48 +0100 Subject: [PATCH 4/7] Table name typo --- Database.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Database.py b/Database.py index a0aa3529..c344c71e 100644 --- a/Database.py +++ b/Database.py @@ -225,7 +225,7 @@ class UserChannelSettings(Table): user_channel_settings.value FROM user_channel_settings INNER JOIN channels ON user_channel_settings.channel_id=channels.channel_id - INNER JOIN users on user_channel_setting.user_id=users.user_id + INNER JOIN users on user_channel_settings.user_id=users.user_id WHERE user_channel_settings.setting=? AND users.server_id=?""", [setting, server_id]) if values: From 41817ce25569dcc2bf27d4ffa8def3ee385bff0a Mon Sep 17 00:00:00 2001 From: jesopo Date: Sat, 1 Sep 2018 10:34:55 +0100 Subject: [PATCH 5/7] Add Utils.bold and Utils.underline --- Utils.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Utils.py b/Utils.py index 773e3150..15d8e2be 100644 --- a/Utils.py +++ b/Utils.py @@ -123,6 +123,12 @@ def color(foreground, background=None): return "%s%s%s" % (FONT_COLOR, foreground, "" if not background else ",%s" % background) +def bold(s): + return "%s%s%s" % (FONT_BOLD, s, FONT_BOLD) + +def underline(s): + return "%s%s%s" % (FONT_UNDERLINE, s, FONT_UNDERLINE) + TIME_SECOND = 1 TIME_MINUTE = TIME_SECOND*60 TIME_HOUR = TIME_MINUTE*60 From 29609fffd77e495e86e6ba1a1f7f9e25d1607e1e Mon Sep 17 00:00:00 2001 From: jesopo Date: Sat, 1 Sep 2018 11:29:26 +0100 Subject: [PATCH 6/7] Added functionality to load, unload and reload modules from a command! --- EventManager.py | 15 +++++++++----- ModuleManager.py | 49 +++++++++++++++++++++++++++++----------------- modules/modules.py | 39 ++++++++++++++++++++++++++++++++++++ 3 files changed, 80 insertions(+), 23 deletions(-) create mode 100644 modules/modules.py diff --git a/EventManager.py b/EventManager.py index 4ca5837c..326192ad 100644 --- a/EventManager.py +++ b/EventManager.py @@ -179,7 +179,7 @@ class EventHook(object): returns.append(hook.call(event)) except Exception as e: traceback.print_exc() - self.bot.log.error("failed to call event \"%s", [ + self.bot.log.error("failed to call event \"%s\"", [ event_path], exc_info=True) called += 1 @@ -202,11 +202,9 @@ class EventHook(object): child_name_lower = child_name.lower() if child_name_lower in self._children: del self._children[child_name_lower] - def get_children(self): - return self._children.keys() def check_purge(self): - if len(self.get_hooks()) == 0 and len(self._children + if len(self.get_hooks()) == 0 and len(self.get_children() ) == 0 and not self.parent == None: self.parent.remove_child(self.name) self.parent.check_purge() @@ -218,9 +216,16 @@ class EventHook(object): def purge_context(self, context): if self.has_context(context): self.remove_context(context) - for child in self.get_children(): + for child_name in self.get_children()[:]: + child = self.get_child(child_name) child.purge_context(context) + if child.is_empty(): + self.remove_child(child_name) def get_hooks(self): return sorted(self._hooks + list(itertools.chain.from_iterable( self._context_hooks.values())), key=lambda e: e.priority) + def get_children(self): + return list(self._children.keys()) + def is_empty(self): + return len(self.get_hooks() + self.get_children()) == 0 diff --git a/ModuleManager.py b/ModuleManager.py index 812edb71..404502e8 100644 --- a/ModuleManager.py +++ b/ModuleManager.py @@ -10,13 +10,15 @@ class ModuleManager(object): def list_modules(self): return sorted(glob.glob(os.path.join(self.directory, "*.py"))) - def module_name(self, filename): - return os.path.basename(filename).rsplit(".py", 1)[0].lower() + def _module_name(self, path): + return os.path.basename(path).rsplit(".py", 1)[0].lower() + def _module_path(self, name): + return os.path.join(self.directory, "%s.py" % name) - def _load_module(self, filename): - name = self.module_name(filename) + def _load_module(self, name): + path = self._module_path(name) - with open(filename) as module_file: + with open(path) as module_file: while True: line = module_file.readline().strip() line_split = line.split(" ") @@ -36,11 +38,11 @@ class ModuleManager(object): if not "bitbot_%s" % line_split[1].lower() in sys.modules: if not line_split[1].lower() in self.waiting_requirement: self.waiting_requirement[line_split[1].lower()] = set([]) - self.waiting_requirement[line_split[1].lower()].add(filename) + self.waiting_requirement[line_split[1].lower()].add(path) return None else: break - module = imp.load_source(name, filename) + module = imp.load_source(name, path) if not hasattr(module, "Module"): raise ImportError("module '%s' doesn't have a Module class.") @@ -60,23 +62,34 @@ class ModuleManager(object): "module name '%s' attempted to be used twice.") return module_object - def load_module(self, filename): - name = self.module_name(filename) + def load_module(self, name): try: - module = self._load_module(filename) + module = self._load_module(name) except ImportError as e: - sys.stderr.write("module '%s' not loaded: Could not resolve import.\n" % filename) + self.bot.log.error("failed to load module \"%s\": %s", + [name, e.msg]) return if module: self.modules[module._import_name] = module if name in self.waiting_requirement: - for filename in self.waiting_requirement: - self.load_module(filename) - sys.stderr.write("module '%s' loaded.\n" % filename) + for requirement_name in self.waiting_requirement: + self.load_module(requirement_name) + self.bot.log.info("Module '%s' loaded", [name]) else: - sys.stderr.write("module '%s' not loaded.\n" % filename) + self.bot.log.error("Module '%s' not loaded", [name]) def load_modules(self, whitelist=None): - for filename in self.list_modules(): - if whitelist == None or filename in whitelist: - self.load_module(filename) + for path in self.list_modules(): + name = self._module_name(path) + if whitelist == None or name in whitelist: + self.load_module(name) + + def unload_module(self, name): + module = self.modules[name] + del self.modules[name] + + event_context = module._event_context + self.events.purge_context(event_context) + + del sys.modules[name] + del module diff --git a/modules/modules.py b/modules/modules.py new file mode 100644 index 00000000..0138be1b --- /dev/null +++ b/modules/modules.py @@ -0,0 +1,39 @@ + + +class Module(object): + def __init__(self, bot, events): + self.bot = bot + events.on("received.command.loadmodule").hook(self.load, + min_args=1, permission="load-module", help="Load a module", + usage="") + events.on("received.command.unloadmodule").hook(self.unload, + min_args=1, permission="unload-module", help="Unload a module", + usage="") + events.on("received.command.reloadmodule").hook(self.reload, + min_args=1, permission="reload-module", help="Reload a module", + usage="") + + def load(self, event): + name = event["args_split"][0].lower() + if name in self.bot.modules.modules: + event["stderr"].write("Module '%s' is already loaded" % name) + return + self.bot.modules.load_module(name) + event["stdout"].write("Loaded '%s'" % name) + + def unload(self, event): + name = event["args_split"][0].lower() + if not name in self.bot.modules.modules: + event["stderr"].write("Module '%s' isn't loaded" % name) + return + self.bot.modules.unload_module(name) + event["stdout"].write("Unloaded '%s'" % name) + + def reload(self, event): + name = event["args_split"][0].lower() + if not name in self.bot.modules.modules: + event["stderr"].write("Module '%s' isn't loaded" % name) + return + self.bot.modules.unload_module(name) + self.bot.modules.load_module(name) + event["stdout"].write("Reloaded '%s'" % name) From ef645c338a8a5f201adc907ba8fff623df23cf1c Mon Sep 17 00:00:00 2001 From: jesopo Date: Sat, 1 Sep 2018 12:24:00 +0100 Subject: [PATCH 7/7] BitBot is single threaded! --- modules/ducks.py | 19 ++++--------------- 1 file changed, 4 insertions(+), 15 deletions(-) diff --git a/modules/ducks.py b/modules/ducks.py index 44b03d00..8ca562c7 100644 --- a/modules/ducks.py +++ b/modules/ducks.py @@ -52,12 +52,14 @@ class Module(object): events.on("raw").on("376").hook(self.duck_loop_entry) + events.on("timer").on("duck-decoy").hook(self.duck_decoy) + events.on("timer").on("show-duck").hook(self.show_duck) + def duck_loop_entry(self, event): wait = self.get_random_duck_time() - self.timer = Timer(wait, self.show_duck, [event]) self.bot.log.info("Sending out a wave of ducks in %s seconds", [wait]) - self.timer.start() + self.bot.add_timer("show-duck", wait) def bootstrap(self, event): for server in self.bot.servers.values(): @@ -269,18 +271,5 @@ class Module(object): next_decoy_time = self.decoy_time() - if self.decoy_hooked == 0: - self.events.on("timer").on("duck-decoy").hook(self.duck_decoy) - self.decoy_hooked = 1 - self.bot.add_timer("duck-decoy", next_decoy_time, None, None, False, channel=channel) - -# def coins(self, event): -# if event["args_split"]: -# target = event["server"].get_user(event["args_split"][0]) -# else: -# target = event["user"] -# coins = decimal.Decimal(target.get_setting("coins", "0.0")) -# event["stdout"].write("%s has %s coin%s" % (target.nickname, -# "{0:.2f}".format(coins), "" if coins == 1 else "s"))