diff --git a/main.py b/main.py index f05c284..d5e310e 100644 --- a/main.py +++ b/main.py @@ -34,6 +34,20 @@ class Module: self.name = name MODULES.update({self.name: self}) + @property + def type(self) -> str: + """ + Return module type. It can be python or lua + + :return: Module type + :rtype: str + """ + if not os.path.exists(os.path.join("modules", self.name, "version.json")): + return "" + with open(os.path.join("modules", self.name, "version.json")) as file: + versions = json.load(file) + return versions["type"] + @property def exists(self) -> bool: """ @@ -65,6 +79,8 @@ class Module: return False if "bot_version" not in versions.keys(): return False + if "type" not in versions.keys(): + return False return True @property @@ -274,21 +290,34 @@ class LBI(discord.Client): for dep in deps.keys(): if dep not in self.modules.keys(): self.load_module(dep) - try: - self.info("Start loading module {module}...".format(module=module)) - imported = importlib.import_module('modules.' + module) + if MODULES[module].type == "python": + try: + self.info("Start loading module {module}...".format(module=module)) + imported = importlib.import_module('modules.' + module) + importlib.reload(imported) + initialized_class = imported.MainClass(self) + self.modules.update({module: {"imported": imported, "initialized_class": initialized_class}}) + self.info("Module {module} successfully imported.".format(module=module)) + initialized_class.dispatch("load") + if module not in self.config["modules"]: + self.config["modules"].append(module) + self.save_config() + except AttributeError as e: + self.error("Module {module} doesn't have MainClass.".format(module=module)) + return e + return 0 + elif MODULES[module].type == "lua": + self.info(f"Start loading module {module}...") + imported = importlib.import_module('modules.base.BaseLua') importlib.reload(imported) - initialized_class = imported.MainClass(self) + initialized_class = imported.BaseClassLua(self, path=f"modules/{module}/main") self.modules.update({module: {"imported": imported, "initialized_class": initialized_class}}) - self.info("Module {module} successfully imported.".format(module=module)) + self.info(f"Module {module} successfully imported.") initialized_class.dispatch("load") if module not in self.config["modules"]: self.config["modules"].append(module) self.save_config() - except AttributeError as e: - self.error("Module {module} doesn't have MainClass.".format(module=module)) - return e - return 0 + return 0 @modules_edit def unload_module(self, module): diff --git a/modules/base/Base.py b/modules/base/Base.py new file mode 100644 index 0000000..ff7c3ae --- /dev/null +++ b/modules/base/Base.py @@ -0,0 +1,195 @@ +"""Base class for module, never use directly !!!""" +import asyncio +import sys +import pickle +import traceback + +import discord + +from storage import FSStorage, FSObjects +import storage.path as path + + +class BaseClass: + """Base class for all modules, Override it to make submodules""" + name = "" + help = { + "description": "", + "commands": { + + } + } + help_active = False + color = 0x000000 + command_text = None + super_users = [] + authorized_roles = [] + + def __init__(self, client): + """Initialize module class + + Initialize module class, always call it to set self.client when you override it. + + :param client: client instance + :type client: NikolaTesla""" + self.client = client + self.storage = FSStorage(path.join(self.client.base_path, self.name)) + self.objects = FSObjects(self.storage) + if not self.storage.isdir(path.join("storage", self.name)): + self.storage.makedirs(path.join("storage", self.name), exist_ok=True) + + async def send_help(self, channel): + embed = discord.Embed( + title="[{nom}] - Aide".format(nom=self.name), + description=self.help["description"].format(prefix=self.client.config['prefix']), + color=self.color + ) + for command, description in self.help["commands"].items(): + embed.add_field(name=command.format(prefix=self.client.config['prefix'], command=self.command_text), + value=description.format(prefix=self.client.config['prefix'], command=self.command_text), + inline=False) + await channel.send(embed=embed) + + async def auth(self, user, role_list): + if type(role_list) == list: + if user.id in self.client.owners: + return True + for guild in self.client.guilds: + if guild.get_member(user.id): + for role_id in role_list: + if role_id in [r.id for r in guild.get_member(user.id).roles]: + return True + elif type(role_list) == str: + module_name = role_list + if user.id in self.client.owners: + return True + authorized_roles = self.client.modules[module_name]["class"].authorized_roles + if len(authorized_roles): + for guild in self.client.guilds: + if guild.get_member(user.id): + for role_id in authorized_roles: + if role_id in [r.id for r in guild.get_member(user.id).roles]: + return True + else: + return True + return False + + async def parse_command(self, message): + """Parse a command_text from received message and execute function + %git update + com_update(m..) + Parse message like `{prefix}{command_text} subcommand` and call class method `com_{subcommand}`. + + :param message: message to parse + :type message: discord.Message""" + if message.content.startswith(self.client.config["prefix"] + (self.command_text if self.command_text else "")): + + content = message.content.lstrip( + self.client.config["prefix"] + (self.command_text if self.command_text else "")) + sub_command, args, kwargs = self._parse_command_content(content) + sub_command = "com_" + sub_command + if sub_command in dir(self): + await self.__getattribute__(sub_command)(message, args, kwargs) + else: + await self.command(message, [sub_command[4:]] + args, kwargs) + + @staticmethod + def _parse_command_content(content): + """Parse string + + Parse string like `subcommand argument "argument with spaces" -o -shortwaytopassoncharacteroption --longoption + -o "option with argument"`. You can override this function to change parsing. + + :param content: content to parse + :type content: str + + :return: parsed arguments: [subcommand, [arg1, arg2, ...], [(option1, arg1), (option2, arg2), ...]] + :rtype: list[str, list, list]""" + if not len(content.split()): + return "", [], [] + # Sub_command + sub_command = content.split()[0] + args_ = [] + kwargs = [] + if len(content.split()) > 1: + # Take the other part of command_text + content = content.split(" ", 1)[1].replace("\"", "\"\"") + # Splitting around quotes + quotes = [element.split("\" ") for element in content.split(" \"")] + # Split all sub chains but brute chains and flat the resulting list + args = [item.split() if item[0] != "\"" else [item, ] for sublist in quotes for item in sublist] + # Second plating + args = [item for sublist in args for item in sublist] + # args_ are arguments, kwargs are options with arguments + i = 0 + while i < len(args): + if args[i].startswith("\""): + args_.append(args[i][1:-1]) + elif args[i].startswith("--"): + if i + 1 >= len(args): + kwargs.append((args[i].lstrip("-"), None)) + break + if args[i + 1][0] != "-": + kwargs.append((args[i].lstrip("-"), args[i + 1].strip("\""))) + i += 1 + else: + kwargs.append((args[i].lstrip("-"), None)) + elif args[i].startswith("-"): + if len(args[i]) == 2: + if i + 1 >= len(args): + break + if args[i + 1][0] != "-": + kwargs.append((args[i].lstrip("-"), args[i + 1].strip("\""))) + i += 1 + else: + kwargs.append((args[i].lstrip("-"), None)) + else: + kwargs.extend([(arg, None) for arg in args[i][1:]]) + else: + args_.append(args[i]) + i += 1 + return sub_command, args_, kwargs + + async def on_message(self, message): + """Override this function to deactivate command_text parsing""" + await self.parse_command(message) + + async def command(self, message, args, kwargs): + """Override this function to handle all messages starting with `{prefix}{command_text}` + + Function which is executed for all command_text doesn't match with a `com_{subcommand}` function""" + pass + + + + def dispatch(self, event, *args, **kwargs): + # Method to call + method = 'on_' + event + try: + # Try to get coro, if not exists pass without raise an error + coro = getattr(self, method) + except AttributeError: + pass + else: + # Run event + asyncio.ensure_future(self._run_event(coro, method, *args, **kwargs), loop=self.client.loop) + + async def _run_event(self, coro, event_name, *args, **kwargs): + # Run event + try: + await coro(*args, **kwargs) + except asyncio.CancelledError: + # If function is cancelled pass silently + pass + except Exception: + try: + # Call error function + await self.on_error(event_name, *args, **kwargs) + except asyncio.CancelledError: + # If error event is canceled pass silently + pass + + async def on_error(self, event_method, *args, **kwargs): + # Basic error handler + print('Ignoring exception in {}'.format(event_method), file=sys.stderr) + traceback.print_exc() diff --git a/modules/base/BaseLua.py b/modules/base/BaseLua.py index 20e0b68..7c4a105 100644 --- a/modules/base/BaseLua.py +++ b/modules/base/BaseLua.py @@ -1,5 +1,6 @@ """Base class for module, never use directly !!!""" import asyncio +import os import sys import pickle import traceback @@ -7,11 +8,12 @@ import traceback import discord import lupa +from modules.base.Base import BaseClass from storage import FSStorage import storage.path as path -class BaseClassLua: +class BaseClassLua(BaseClass): """Base class for all modules, Override it to make submodules""" name = "" help = { @@ -26,22 +28,48 @@ class BaseClassLua: super_users = [] authorized_roles = [] - def __init__(self, client): + def __init__(self, client, path): """Initialize module class Initialize module class, always call it to set self.client when you override it. :param client: client instance :type client: NikolaTesla""" - self.client = client - self.storage = FSStorage(path.join(self.client.base_path, self.name)) - if not self.storage.isdir(path.join("storage", self.name)): - self.storage.makedirs(path.join("storage", self.name), exist_ok=True) + super().__init__(client) # Get lua globals self.lua = lupa.LuaRuntime(unpack_returned_tuples=True) - self.luaMethods = self.lua.require("modules/test_lua/main") + print(os.path.abspath(path)) + self.luaMethods = self.lua.require(path) def dispatch(self, event, *args, **kwargs): - if self.luaMethods["on_"+event] is not None: - self.luaMethods["on_"+event](asyncio.ensure_future, self.client, *args, **kwargs) + method = "on_"+event + if self.luaMethods[method] is not None: + self.luaMethods[method](asyncio.ensure_future, self, *args, **kwargs) + else: # If lua methods not found, dispatch to python methods + super().dispatch(event, *args, **kwargs) + + async def _run_event(self, coro, event_name, *args, **kwargs): + # Overide here to execute lua on_error if it exists + # Run event + try: + await coro(*args, **kwargs) + except asyncio.CancelledError: + # If function is cancelled pass silently + pass + except Exception: + try: + # Call error function + if self.luaMethods["on_error"] is not None: + self.luaMethods["on_error"](self, asyncio.ensure_future, discord, *args, **kwargs) + else: + await self.on_error(event_name, *args, **kwargs) + except asyncio.CancelledError: + # If error event is canceled pass silently + pass + + async def on_error(self, event_method, *args, **kwargs): + # Base on_error event, executed if lua not provide it + # Basic error handler + print('Ignoring exception in {}'.format(event_method), file=sys.stderr) + traceback.print_exc() diff --git a/modules/base/BasePython.py b/modules/base/BasePython.py index 85c465f..fbb52a8 100644 --- a/modules/base/BasePython.py +++ b/modules/base/BasePython.py @@ -1,209 +1,8 @@ """Base class for module, never use directly !!!""" -import asyncio -import sys -import pickle -import traceback -import discord - -from storage import FSStorage -import storage.path as path +from modules.base.Base import BaseClass -class BaseClassPython: +class BaseClassPython(BaseClass): """Base class for all modules, Override it to make submodules""" - name = "" - help = { - "description": "", - "commands": { - - } - } - help_active = False - color = 0x000000 - command_text = None - super_users = [] - authorized_roles = [] - - def __init__(self, client): - """Initialize module class - - Initialize module class, always call it to set self.client when you override it. - - :param client: client instance - :type client: NikolaTesla""" - self.client = client - self.storage = FSStorage(path.join(self.client.base_path, self.name)) - if not self.storage.isdir(path.join("storage", self.name)): - self.storage.makedirs(path.join("storage", self.name), exist_ok=True) - - async def send_help(self, channel): - embed = discord.Embed( - title="[{nom}] - Aide".format(nom=self.name), - description=self.help["description"].format(prefix=self.client.config['prefix']), - color=self.color - ) - for command, description in self.help["commands"].items(): - embed.add_field(name=command.format(prefix=self.client.config['prefix'], command=self.command_text), - value=description.format(prefix=self.client.config['prefix'], command=self.command_text), - inline=False) - await channel.send(embed=embed) - - async def auth(self, user, role_list): - if type(role_list) == list: - if user.id in self.client.owners: - return True - for guild in self.client.guilds: - if guild.get_member(user.id): - for role_id in role_list: - if role_id in [r.id for r in guild.get_member(user.id).roles]: - return True - elif type(role_list) == str: - module_name = role_list - if user.id in self.client.owners: - return True - authorized_roles = self.client.modules[module_name]["class"].authorized_roles - if len(authorized_roles): - for guild in self.client.guilds: - if guild.get_member(user.id): - for role_id in authorized_roles: - if role_id in [r.id for r in guild.get_member(user.id).roles]: - return True - else: - return True - return False - - async def parse_command(self, message): - """Parse a command_text from received message and execute function - %git update - com_update(m..) - Parse message like `{prefix}{command_text} subcommand` and call class method `com_{subcommand}`. - - :param message: message to parse - :type message: discord.Message""" - if message.content.startswith(self.client.config["prefix"] + (self.command_text if self.command_text else "")): - - content = message.content.lstrip( - self.client.config["prefix"] + (self.command_text if self.command_text else "")) - sub_command, args, kwargs = self._parse_command_content(content) - sub_command = "com_" + sub_command - if sub_command in dir(self): - await self.__getattribute__(sub_command)(message, args, kwargs) - else: - await self.command(message, [sub_command[4:]] + args, kwargs) - - @staticmethod - def _parse_command_content(content): - """Parse string - - Parse string like `subcommand argument "argument with spaces" -o -shortwaytopassoncharacteroption --longoption - -o "option with argument"`. You can override this function to change parsing. - - :param content: content to parse - :type content: str - - :return: parsed arguments: [subcommand, [arg1, arg2, ...], [(option1, arg1), (option2, arg2), ...]] - :rtype: list[str, list, list]""" - if not len(content.split()): - return "", [], [] - # Sub_command - sub_command = content.split()[0] - args_ = [] - kwargs = [] - if len(content.split()) > 1: - # Take the other part of command_text - content = content.split(" ", 1)[1].replace("\"", "\"\"") - # Splitting around quotes - quotes = [element.split("\" ") for element in content.split(" \"")] - # Split all sub chains but brute chains and flat the resulting list - args = [item.split() if item[0] != "\"" else [item, ] for sublist in quotes for item in sublist] - # Second plating - args = [item for sublist in args for item in sublist] - # args_ are arguments, kwargs are options with arguments - i = 0 - while i < len(args): - if args[i].startswith("\""): - args_.append(args[i][1:-1]) - elif args[i].startswith("--"): - if i + 1 >= len(args): - kwargs.append((args[i].lstrip("-"), None)) - break - if args[i + 1][0] != "-": - kwargs.append((args[i].lstrip("-"), args[i + 1].strip("\""))) - i += 1 - else: - kwargs.append((args[i].lstrip("-"), None)) - elif args[i].startswith("-"): - if len(args[i]) == 2: - if i + 1 >= len(args): - break - if args[i + 1][0] != "-": - kwargs.append((args[i].lstrip("-"), args[i + 1].strip("\""))) - i += 1 - else: - kwargs.append((args[i].lstrip("-"), None)) - else: - kwargs.extend([(arg, None) for arg in args[i][1:]]) - else: - args_.append(args[i]) - i += 1 - return sub_command, args_, kwargs - - async def on_message(self, message): - """Override this function to deactivate command_text parsing""" - await self.parse_command(message) - - async def command(self, message, args, kwargs): - """Override this function to handle all messages starting with `{prefix}{command_text}` - - Function which is executed for all command_text doesn't match with a `com_{subcommand}` function""" - pass - - def save_object(self, object_instance, object_name): - """Save object into pickle file""" - with self.storage.open(object_name, "wb") as f: - pickler = pickle.Pickler(f) - pickler.dump(object_instance) - - def load_object(self, object_name): - """Load object from pickle file""" - if self.save_exists(object_name): - with self.storage.open(object_name, "rb") as f: - unpickler = pickle.Unpickler(f) - return unpickler.load() - - def save_exists(self, object_name): - """Check if pickle file exists""" - return self.storage.exists(object_name) - - def dispatch(self, event, *args, **kwargs): - # Method to call - method = 'on_' + event - try: - # Try to get coro, if not exists pass without raise an error - coro = getattr(self, method) - except AttributeError: - pass - else: - # Run event - asyncio.ensure_future(self._run_event(coro, method, *args, **kwargs), loop=self.client.loop) - - async def _run_event(self, coro, event_name, *args, **kwargs): - # Run event - try: - await coro(*args, **kwargs) - except asyncio.CancelledError: - # If function is cancelled pass silently - pass - except Exception: - try: - # Call error function - await self.on_error(event_name, *args, **kwargs) - except asyncio.CancelledError: - # If error event is canceled pass silently - pass - - async def on_error(self, event_method, *args, **kwargs): - # Basic error handler - print('Ignoring exception in {}'.format(event_method), file=sys.stderr) - traceback.print_exc() + pass diff --git a/modules/base/version.json b/modules/base/version.json index b8b6b5e..b27fd9a 100644 --- a/modules/base/version.json +++ b/modules/base/version.json @@ -1,5 +1,6 @@ { "version":"0.1.0", + "type": "python", "dependencies": { }, diff --git a/modules/errors/__init__.py b/modules/errors/__init__.py index 42bfb75..23d8480 100644 --- a/modules/errors/__init__.py +++ b/modules/errors/__init__.py @@ -32,8 +32,8 @@ class MainClass(BaseClassPython): self.icon = "" async def on_ready(self): - if self.save_exists('errorsDeque'): - self.errorsDeque = self.load_object('errorsDeque') + if self.objects.save_exists('errorsDeque'): + self.errorsDeque = self.objects.load_object('errorsDeque') else: self.errorsDeque = collections.deque() for i in range(len(self.errorsDeque)): @@ -44,7 +44,7 @@ class MainClass(BaseClassPython): await delete_message.delete() except: raise - self.save_object(self.errorsDeque, 'errorsDeque') + self.objects.save_object(self.errorsDeque, 'errorsDeque') async def command(self, message, args, kwargs): raise Exception("Si cette erreur apparait, alors tout est normal") @@ -72,7 +72,7 @@ class MainClass(BaseClassPython): embed=embed.set_footer(text="Ce message ne s'autodétruira pas.", icon_url=self.icon)) except: pass - self.save_object(self.errorsDeque, 'errorsDeque') + self.objects.save_object(self.errorsDeque, 'errorsDeque') await asyncio.sleep(60) try: channel = self.client.get_channel(message_list[0]) @@ -85,4 +85,4 @@ class MainClass(BaseClassPython): self.errorsDeque.remove(message_list) except ValueError: pass - self.save_object(self.errorsDeque, 'errorsDeque') + self.objects.save_object(self.errorsDeque, 'errorsDeque') diff --git a/modules/errors/version.json b/modules/errors/version.json index acfacc8..4cb3a98 100644 --- a/modules/errors/version.json +++ b/modules/errors/version.json @@ -1,5 +1,6 @@ { "version": "0.1.0", + "type": "python", "dependencies": { "base": { "min": "0.1.0", diff --git a/modules/modules/__init__.py b/modules/modules/__init__.py index 1794fbd..40cbbcc 100644 --- a/modules/modules/__init__.py +++ b/modules/modules/__init__.py @@ -17,7 +17,7 @@ class MainClass(BaseClassPython): "`{prefix}{command} list`": "List of available modules.", "`{prefix}{command} enable `": "Enable module ``.", "`{prefix}{command} disable `": "Disable module ``.", - "`{prefix}{command} reload `":"Reload module ``", + "`{prefix}{command} reload `": "Reload module ``", "`{prefix}{command} web_list`": "List all available modules from repository", } } diff --git a/modules/modules/version.json b/modules/modules/version.json index acfacc8..4cb3a98 100644 --- a/modules/modules/version.json +++ b/modules/modules/version.json @@ -1,5 +1,6 @@ { "version": "0.1.0", + "type": "python", "dependencies": { "base": { "min": "0.1.0", diff --git a/modules/test_lua/__init__.py b/modules/test_lua/__init__.py deleted file mode 100644 index 49ff50e..0000000 --- a/modules/test_lua/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -from modules.base import BaseClassLua - - -class MainClass(BaseClassLua): - pass diff --git a/modules/test_lua/main.lua b/modules/test_lua/main.lua index db9cc6d..ad966f0 100644 --- a/modules/test_lua/main.lua +++ b/modules/test_lua/main.lua @@ -1,11 +1,8 @@ main = {} - -function main.on_message(await, client, message, ...) - print("I LOVE LUA") - print(message.content) +function main.on_message(self, await, discord, message) if message.author.bot == false then - await(message.channel.send("Tu n'es pas un bot")) + end end diff --git a/modules/test_lua/version.json b/modules/test_lua/version.json index acfacc8..bfd7285 100644 --- a/modules/test_lua/version.json +++ b/modules/test_lua/version.json @@ -1,5 +1,6 @@ { "version": "0.1.0", + "type":"lua", "dependencies": { "base": { "min": "0.1.0", diff --git a/storage/FileSystem.py b/storage/FileSystem.py index 5a783a3..3295412 100644 --- a/storage/FileSystem.py +++ b/storage/FileSystem.py @@ -1,7 +1,6 @@ import os -from storage.base import Storage - +from storage.base import Storage, Objects class FSStorage(Storage): """ @@ -73,3 +72,6 @@ class FSStorage(Storage): def isdir(self, path): return os.path.isdir(self._topath(path)) + +class FSObjects(Objects): + pass \ No newline at end of file diff --git a/storage/__init__.py b/storage/__init__.py index a3570da..d6d9c7d 100644 --- a/storage/__init__.py +++ b/storage/__init__.py @@ -1 +1,2 @@ from storage.FileSystem import FSStorage +from storage.FileSystem import FSObjects \ No newline at end of file diff --git a/storage/base.py b/storage/base.py index 0da466e..c5b2b10 100644 --- a/storage/base.py +++ b/storage/base.py @@ -1,3 +1,8 @@ +import pickle + +from storage import path as pth + + class Storage: """Basic class for storage interface @@ -52,6 +57,7 @@ class Storage: async def makedirs(self, path, exist_ok=False): """ Create directory `path` + :param exist_ok: Not return error if dir exists :param path: directory to create :return: Path to new directory """ @@ -113,3 +119,25 @@ class Storage: :return: True if path is a directory """ pass + + +class Objects: + def __init__(self, storage): + self.storage = storage + + def save_object(self, object_name, object_instance): + """Save object into pickle file""" + with self.storage.open(pth.join("objects", object_name), "wb") as f: + pickler = pickle.Pickler(f) + pickler.dump(object_instance) + + def load_object(self, object_name): + """Load object from pickle file""" + if self.save_exists(object_name): + with self.storage.open(pth.join("objects", object_name), "rb") as f: + unpickler = pickle.Unpickler(f) + return unpickler.load() + + def save_exists(self, object_name): + """Check if pickle file exists""" + return self.storage.exists(pth.join("objects", object_name))