From 0ed718d6d49a8def12dedd19abd45c2517ae2be2 Mon Sep 17 00:00:00 2001 From: Louis Chauvet Date: Tue, 21 Apr 2020 10:46:36 +0200 Subject: [PATCH 01/15] Petit checkpoint --- Makefile => doc/Makefile | 0 make.bat => doc/make.bat | 0 {source => doc/source}/api/config.rst | 0 {source => doc/source}/api/main.rst | 0 {source => doc/source}/api/modules.rst | 0 {source => doc/source}/api/utils.emojis.rst | 0 {source => doc/source}/api/utils.rst | 0 {source => doc/source}/conf.py | 0 {source => doc/source}/index.rst | 0 .../source}/module_creation/index.rst | 0 .../source}/module_creation/intro.rst | 0 modules/newmember/version.json | 11 - modules/panic/version.json | 11 - modules/perdu/version.json | 11 - modules/purge/version.json | 11 - modules/readrules/version.json | 11 - modules/restart/version.json | 11 - modules/roles/version.json | 11 - modules/rtfgd/version.json | 11 - {modules => src/bot_base}/__init__.py | 0 .../roles.py => src/bot_base/bot_base.py | 0 {config => src/config}/__init__.py | 0 {config => src/config}/base.py | 0 .../config}/config_types/__init__.py | 0 .../config}/config_types/base_type.py | 0 {config => src/config}/config_types/bool.py | 0 {config => src/config}/config_types/color.py | 0 {config => src/config}/config_types/dict.py | 0 .../config_types/discord_types/__init__.py | 0 .../config_types/discord_types/channel.py | 0 .../config_types/discord_types/guild.py | 0 .../config_types/discord_types/role.py | 0 .../config_types/discord_types/user.py | 0 {config => src/config}/config_types/float.py | 0 {config => src/config}/config_types/int.py | 0 {config => src/config}/config_types/list.py | 0 {config => src/config}/config_types/str.py | 0 {config => src/config}/log_config.json | 0 errors.py => src/errors.py | 56 +- main.py => src/main.py | 998 +++++++++--------- {utils => src/modules}/__init__.py | 0 {modules => src/modules}/avalon/__init__.py | 0 src/modules/avalon/roles.py | 0 {modules => src/modules}/avalon/version.json | 20 +- {modules => src/modules}/base/__init__.py | 4 +- .../base/Base.py => src/modules/base/base.py | 7 +- .../modules/base/base_lua.py | 2 +- .../modules/base/base_python.py | 2 +- {modules => src/modules}/base/version.json | 20 +- {modules => src/modules}/clean/__init__.py | 0 {modules => src/modules}/clean/version.json | 20 +- {modules => src/modules}/errors/__init__.py | 0 .../modules/errors}/version.json | 26 +- {modules => src/modules}/help/__init__.py | 0 {modules => src/modules}/help/version.json | 20 +- {modules => src/modules}/modules/__init__.py | 282 ++--- {modules => src/modules}/modules/api.py | 0 .../modules/modules}/version.json | 26 +- .../modules}/newmember/__init__.py | 0 src/modules/newmember/version.json | 11 + {modules => src/modules}/panic/__init__.py | 0 src/modules/panic/version.json | 11 + {modules => src/modules}/perdu/__init__.py | 0 src/modules/perdu/version.json | 11 + {modules => src/modules}/purge/__init__.py | 0 src/modules/purge/version.json | 11 + .../modules}/readrules/__init__.py | 0 src/modules/readrules/version.json | 11 + {modules => src/modules}/restart/__init__.py | 0 src/modules/restart/version.json | 11 + {modules => src/modules}/roles/__init__.py | 0 src/modules/roles/version.json | 11 + {modules => src/modules}/rtfgd/__init__.py | 0 src/modules/rtfgd/version.json | 11 + pytest.ini => src/pytest.ini | 0 {storage => src/storage}/__init__.py | 0 {storage => src/storage}/jsonencoder.py | 0 {storage => src/storage}/objects.py | 0 src/utils/__init__.py | 0 {utils => src/utils}/emojis.py | 0 80 files changed, 830 insertions(+), 829 deletions(-) rename Makefile => doc/Makefile (100%) rename make.bat => doc/make.bat (100%) rename {source => doc/source}/api/config.rst (100%) rename {source => doc/source}/api/main.rst (100%) rename {source => doc/source}/api/modules.rst (100%) rename {source => doc/source}/api/utils.emojis.rst (100%) rename {source => doc/source}/api/utils.rst (100%) rename {source => doc/source}/conf.py (100%) rename {source => doc/source}/index.rst (100%) rename {source => doc/source}/module_creation/index.rst (100%) rename {source => doc/source}/module_creation/intro.rst (100%) delete mode 100644 modules/newmember/version.json delete mode 100644 modules/panic/version.json delete mode 100644 modules/perdu/version.json delete mode 100644 modules/purge/version.json delete mode 100644 modules/readrules/version.json delete mode 100644 modules/restart/version.json delete mode 100644 modules/roles/version.json delete mode 100644 modules/rtfgd/version.json rename {modules => src/bot_base}/__init__.py (100%) rename modules/avalon/roles.py => src/bot_base/bot_base.py (100%) rename {config => src/config}/__init__.py (100%) rename {config => src/config}/base.py (100%) rename {config => src/config}/config_types/__init__.py (100%) rename {config => src/config}/config_types/base_type.py (100%) rename {config => src/config}/config_types/bool.py (100%) rename {config => src/config}/config_types/color.py (100%) rename {config => src/config}/config_types/dict.py (100%) rename {config => src/config}/config_types/discord_types/__init__.py (100%) rename {config => src/config}/config_types/discord_types/channel.py (100%) rename {config => src/config}/config_types/discord_types/guild.py (100%) rename {config => src/config}/config_types/discord_types/role.py (100%) rename {config => src/config}/config_types/discord_types/user.py (100%) rename {config => src/config}/config_types/float.py (100%) rename {config => src/config}/config_types/int.py (100%) rename {config => src/config}/config_types/list.py (100%) rename {config => src/config}/config_types/str.py (100%) rename {config => src/config}/log_config.json (100%) rename errors.py => src/errors.py (94%) rename main.py => src/main.py (96%) rename {utils => src/modules}/__init__.py (100%) rename {modules => src/modules}/avalon/__init__.py (100%) create mode 100644 src/modules/avalon/roles.py rename {modules => src/modules}/avalon/version.json (92%) rename {modules => src/modules}/base/__init__.py (98%) rename modules/base/Base.py => src/modules/base/base.py (98%) rename modules/base/BaseLua.py => src/modules/base/base_lua.py (98%) rename modules/base/BasePython.py => src/modules/base/base_python.py (80%) rename {modules => src/modules}/base/version.json (92%) rename {modules => src/modules}/clean/__init__.py (100%) rename {modules => src/modules}/clean/version.json (92%) rename {modules => src/modules}/errors/__init__.py (100%) rename {modules/modules => src/modules/errors}/version.json (93%) rename {modules => src/modules}/help/__init__.py (100%) rename {modules => src/modules}/help/version.json (92%) rename {modules => src/modules}/modules/__init__.py (97%) rename {modules => src/modules}/modules/api.py (100%) rename {modules/errors => src/modules/modules}/version.json (93%) rename {modules => src/modules}/newmember/__init__.py (100%) create mode 100644 src/modules/newmember/version.json rename {modules => src/modules}/panic/__init__.py (100%) create mode 100644 src/modules/panic/version.json rename {modules => src/modules}/perdu/__init__.py (100%) create mode 100644 src/modules/perdu/version.json rename {modules => src/modules}/purge/__init__.py (100%) create mode 100644 src/modules/purge/version.json rename {modules => src/modules}/readrules/__init__.py (100%) create mode 100644 src/modules/readrules/version.json rename {modules => src/modules}/restart/__init__.py (100%) create mode 100644 src/modules/restart/version.json rename {modules => src/modules}/roles/__init__.py (100%) create mode 100644 src/modules/roles/version.json rename {modules => src/modules}/rtfgd/__init__.py (100%) create mode 100644 src/modules/rtfgd/version.json rename pytest.ini => src/pytest.ini (100%) rename {storage => src/storage}/__init__.py (100%) rename {storage => src/storage}/jsonencoder.py (100%) rename {storage => src/storage}/objects.py (100%) create mode 100644 src/utils/__init__.py rename {utils => src/utils}/emojis.py (100%) diff --git a/Makefile b/doc/Makefile similarity index 100% rename from Makefile rename to doc/Makefile diff --git a/make.bat b/doc/make.bat similarity index 100% rename from make.bat rename to doc/make.bat diff --git a/source/api/config.rst b/doc/source/api/config.rst similarity index 100% rename from source/api/config.rst rename to doc/source/api/config.rst diff --git a/source/api/main.rst b/doc/source/api/main.rst similarity index 100% rename from source/api/main.rst rename to doc/source/api/main.rst diff --git a/source/api/modules.rst b/doc/source/api/modules.rst similarity index 100% rename from source/api/modules.rst rename to doc/source/api/modules.rst diff --git a/source/api/utils.emojis.rst b/doc/source/api/utils.emojis.rst similarity index 100% rename from source/api/utils.emojis.rst rename to doc/source/api/utils.emojis.rst diff --git a/source/api/utils.rst b/doc/source/api/utils.rst similarity index 100% rename from source/api/utils.rst rename to doc/source/api/utils.rst diff --git a/source/conf.py b/doc/source/conf.py similarity index 100% rename from source/conf.py rename to doc/source/conf.py diff --git a/source/index.rst b/doc/source/index.rst similarity index 100% rename from source/index.rst rename to doc/source/index.rst diff --git a/source/module_creation/index.rst b/doc/source/module_creation/index.rst similarity index 100% rename from source/module_creation/index.rst rename to doc/source/module_creation/index.rst diff --git a/source/module_creation/intro.rst b/doc/source/module_creation/intro.rst similarity index 100% rename from source/module_creation/intro.rst rename to doc/source/module_creation/intro.rst diff --git a/modules/newmember/version.json b/modules/newmember/version.json deleted file mode 100644 index b27fd9a..0000000 --- a/modules/newmember/version.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "version":"0.1.0", - "type": "python", - "dependencies": { - - }, - "bot_version": { - "min": "0.1.0", - "max": "0.1.0" - } -} \ No newline at end of file diff --git a/modules/panic/version.json b/modules/panic/version.json deleted file mode 100644 index b27fd9a..0000000 --- a/modules/panic/version.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "version":"0.1.0", - "type": "python", - "dependencies": { - - }, - "bot_version": { - "min": "0.1.0", - "max": "0.1.0" - } -} \ No newline at end of file diff --git a/modules/perdu/version.json b/modules/perdu/version.json deleted file mode 100644 index b27fd9a..0000000 --- a/modules/perdu/version.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "version":"0.1.0", - "type": "python", - "dependencies": { - - }, - "bot_version": { - "min": "0.1.0", - "max": "0.1.0" - } -} \ No newline at end of file diff --git a/modules/purge/version.json b/modules/purge/version.json deleted file mode 100644 index b27fd9a..0000000 --- a/modules/purge/version.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "version":"0.1.0", - "type": "python", - "dependencies": { - - }, - "bot_version": { - "min": "0.1.0", - "max": "0.1.0" - } -} \ No newline at end of file diff --git a/modules/readrules/version.json b/modules/readrules/version.json deleted file mode 100644 index b27fd9a..0000000 --- a/modules/readrules/version.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "version":"0.1.0", - "type": "python", - "dependencies": { - - }, - "bot_version": { - "min": "0.1.0", - "max": "0.1.0" - } -} \ No newline at end of file diff --git a/modules/restart/version.json b/modules/restart/version.json deleted file mode 100644 index b27fd9a..0000000 --- a/modules/restart/version.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "version":"0.1.0", - "type": "python", - "dependencies": { - - }, - "bot_version": { - "min": "0.1.0", - "max": "0.1.0" - } -} \ No newline at end of file diff --git a/modules/roles/version.json b/modules/roles/version.json deleted file mode 100644 index b27fd9a..0000000 --- a/modules/roles/version.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "version":"0.1.0", - "type": "python", - "dependencies": { - - }, - "bot_version": { - "min": "0.1.0", - "max": "0.1.0" - } -} \ No newline at end of file diff --git a/modules/rtfgd/version.json b/modules/rtfgd/version.json deleted file mode 100644 index b27fd9a..0000000 --- a/modules/rtfgd/version.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "version":"0.1.0", - "type": "python", - "dependencies": { - - }, - "bot_version": { - "min": "0.1.0", - "max": "0.1.0" - } -} \ No newline at end of file diff --git a/modules/__init__.py b/src/bot_base/__init__.py similarity index 100% rename from modules/__init__.py rename to src/bot_base/__init__.py diff --git a/modules/avalon/roles.py b/src/bot_base/bot_base.py similarity index 100% rename from modules/avalon/roles.py rename to src/bot_base/bot_base.py diff --git a/config/__init__.py b/src/config/__init__.py similarity index 100% rename from config/__init__.py rename to src/config/__init__.py diff --git a/config/base.py b/src/config/base.py similarity index 100% rename from config/base.py rename to src/config/base.py diff --git a/config/config_types/__init__.py b/src/config/config_types/__init__.py similarity index 100% rename from config/config_types/__init__.py rename to src/config/config_types/__init__.py diff --git a/config/config_types/base_type.py b/src/config/config_types/base_type.py similarity index 100% rename from config/config_types/base_type.py rename to src/config/config_types/base_type.py diff --git a/config/config_types/bool.py b/src/config/config_types/bool.py similarity index 100% rename from config/config_types/bool.py rename to src/config/config_types/bool.py diff --git a/config/config_types/color.py b/src/config/config_types/color.py similarity index 100% rename from config/config_types/color.py rename to src/config/config_types/color.py diff --git a/config/config_types/dict.py b/src/config/config_types/dict.py similarity index 100% rename from config/config_types/dict.py rename to src/config/config_types/dict.py diff --git a/config/config_types/discord_types/__init__.py b/src/config/config_types/discord_types/__init__.py similarity index 100% rename from config/config_types/discord_types/__init__.py rename to src/config/config_types/discord_types/__init__.py diff --git a/config/config_types/discord_types/channel.py b/src/config/config_types/discord_types/channel.py similarity index 100% rename from config/config_types/discord_types/channel.py rename to src/config/config_types/discord_types/channel.py diff --git a/config/config_types/discord_types/guild.py b/src/config/config_types/discord_types/guild.py similarity index 100% rename from config/config_types/discord_types/guild.py rename to src/config/config_types/discord_types/guild.py diff --git a/config/config_types/discord_types/role.py b/src/config/config_types/discord_types/role.py similarity index 100% rename from config/config_types/discord_types/role.py rename to src/config/config_types/discord_types/role.py diff --git a/config/config_types/discord_types/user.py b/src/config/config_types/discord_types/user.py similarity index 100% rename from config/config_types/discord_types/user.py rename to src/config/config_types/discord_types/user.py diff --git a/config/config_types/float.py b/src/config/config_types/float.py similarity index 100% rename from config/config_types/float.py rename to src/config/config_types/float.py diff --git a/config/config_types/int.py b/src/config/config_types/int.py similarity index 100% rename from config/config_types/int.py rename to src/config/config_types/int.py diff --git a/config/config_types/list.py b/src/config/config_types/list.py similarity index 100% rename from config/config_types/list.py rename to src/config/config_types/list.py diff --git a/config/config_types/str.py b/src/config/config_types/str.py similarity index 100% rename from config/config_types/str.py rename to src/config/config_types/str.py diff --git a/config/log_config.json b/src/config/log_config.json similarity index 100% rename from config/log_config.json rename to src/config/log_config.json diff --git a/errors.py b/src/errors.py similarity index 94% rename from errors.py rename to src/errors.py index 6e93abf..4684fb1 100644 --- a/errors.py +++ b/src/errors.py @@ -1,28 +1,28 @@ -class LBIException(Exception): - """ - Base exception class for LBI - - All other exceptions are subclasses - """ - pass - - -class ModuleException(LBIException): - """ - Base exception class for all module errors - """ - pass - - -class ModuleNotInstalled(ModuleException): - """ - Raised when a module is not found in module directory - """ - pass - - -class IncompatibleModule(ModuleException): - """ - Raised when a module is not compatible with bot version - """ - pass +class LBIException(Exception): + """ + Base exception class for LBI + + All other exceptions are subclasses + """ + pass + + +class ModuleException(LBIException): + """ + Base exception class for all module errors + """ + pass + + +class ModuleNotInstalled(ModuleException): + """ + Raised when a module is not found in module directory + """ + pass + + +class IncompatibleModule(ModuleException): + """ + Raised when a module is not compatible with bot version + """ + pass diff --git a/main.py b/src/main.py similarity index 96% rename from main.py rename to src/main.py index 7fb32aa..8d9ee44 100644 --- a/main.py +++ b/src/main.py @@ -1,499 +1,499 @@ -#!/usr/bin/python3 -from __future__ import annotations - -import asyncio -import importlib -import json -import locale -import logging -import logging.config -import os -import sys -import traceback -from typing import Dict - -import discord -import humanize -from packaging.version import Version - -from config import Config, config_types -from config.config_types import factory -from errors import IncompatibleModule -from modules.base import base_supported_type - -__version__ = "0.1.0" - - -class Module: - name: str - - def __init__(self, name: str): - """ - Init module - - :param name: Module name - :type name: str - """ - 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: - """ - Check if module exists - - :return: True if module is present in modules folders - :rtype: bool - """ - if not os.path.isdir(os.path.join("modules", self.name)): - return False - return True - - @property - def complete(self) -> bool: - """ - Check if module is complete - - :return: True if module is compatible - :rtype: Boolean - """ - # Check if version.json exists - if not os.path.exists(os.path.join("modules", self.name, "version.json")): - return False - with open(os.path.join("modules", self.name, "version.json")) as file: - versions = json.load(file) - if "version" not in versions.keys(): - return False - if "dependencies" not in versions.keys(): - return False - if "bot_version" not in versions.keys(): - return False - if "type" not in versions.keys(): - return False - if versions["type"] not in base_supported_type: - return False - return True - - @property - def version(self) -> Version: - """ - Returns module version - - :return: current module version - :rtype: Version - """ - with open(os.path.join("modules", self.name, "version.json")) as file: - versions = json.load(file) - return Version(versions["version"]) - - @property - def bot_version(self) -> dict: - """ - returns the min and max version of the bot that is compatible with the module - - :return: Min and max version for bot - :rtype: dict - :raises IncompatibleModule: If bot_version is not properly formated (there must be min and max keys) - """ - with open(os.path.join("modules", self.name, "version.json")) as file: - versions = json.load(file) - try: - return {"min": Version(versions["bot_version"]["min"]), - "max": Version(versions["bot_version"]["max"])} - except KeyError: - raise IncompatibleModule(f"Module {self.name} is not compatible with bot (version.json does not " - f"contain bot_version.max or bot_version.min item)") - - @property - def dependencies(self) -> dict: - """ - return list of dependencies version - - :raise IncompatibleModule: If bot_version is not properly formated (there must be min and max keys for each dependencies) - :return: list of dependencies version - :rtype: dict - """ - with open(os.path.join("modules", self.name, "version.json")) as file: - versions = json.load(file) - try: - deps = {} - for name, dep in versions["dependencies"].items(): - dep_ver = {"min": Version(dep["min"]), - "max": Version(dep["max"])} - deps.update({name: dep_ver}) - return deps - except KeyError: - raise IncompatibleModule(f"Module {self.name} is not compatible with bot (version.json does not " - f"contain dependencies.modulename.max or dependencies.modulename.min item)") - - @property - def compatible(self) -> bool: - """ - Check if module is compatible with current installation - - :return: True if all dependencies are okays - :rtype: bool - """ - # Check bot version - bot_ver = Version(__version__) - if bot_ver < self.bot_version["min"]: - return False - if bot_ver > self.bot_version["max"]: - return False - for name, dep in self.dependencies.items(): - if name not in MODULES.keys(): - Module(name) - if MODULES[name].version < dep["min"]: - return False - if MODULES[name].version > dep["max"]: - return False - return True - - -MODULES: Dict[str, Module] = {} - - -def setup_logging(default_path='config/log_config.json', default_level=logging.INFO, env_key='LBI_LOG_CONFIG'): - """Setup logging configuration - """ - path = default_path - value = os.getenv(env_key, None) - if value: - path = value - if os.path.exists(path): - with open(path, 'rt') as f: - config = json.load(f) - logging.config.dictConfig(config) - else: - logging.basicConfig(level=default_level) - - -def modules_edit(func): - def wrapper(self, *args, **kwargs): - if self.reloading: - return func(self, *args, **kwargs) - else: - self.reloading = True - a = func(self, *args, **kwargs) - self.reloading = False - return a - - return wrapper - - -def event(func): - def wrapper(self, *args, **kwargs): - if self.reloading: - return lambda: None - else: - return func(self, *args, **kwargs) - - return wrapper - - -"""def async_event(func): - async def wrapper(self, *args, **kwargs): - if self.reloading: - return lambda: None - else: - return func(self, *args, **kwargs) - return wrapper""" - -setup_logging() - -log_discord = logging.getLogger('discord_types') -log_LBI = logging.getLogger('LBI') -log_communication = logging.getLogger('communication') - - -def load_modules_info(): - for mod in os.listdir("modules"): - Module(mod) - - -class LBI(discord.Client): - by_id: ClientById - base_path = "data" - debug = log_LBI.debug - info = log_LBI.info - warning = log_LBI.warning - warn = warning - error = log_LBI.error - critical = log_LBI.critical - - def __init__(self, config: Config = None, *args, **kwargs): - super().__init__(*args, **kwargs) - if config is None: - config = Config(path="data/config.toml", client=self) - self.reloading = False - self.by_id = ClientById(self) - self.ready = False - # Content: {"module_name": {"module": imported module, "class": initialized class}} - self.modules = {} - - self.config = config - self.config.register("modules", factory(config_types.List, factory(config_types.Str))) - self.config.register("prefix", factory(config_types.Str)) - self.config.register("admin_roles", factory(config_types.List, factory(config_types.discord_types.Role, self))) - self.config.register("admin_users", factory(config_types.List, factory(config_types.discord_types.User, self))) - self.config.register("main_guild", factory(config_types.discord_types.Guild, self)) - self.config.register("locale", factory(config_types.Str)) - - self.config.set({ - "modules": ["modules", "errors"], - "prefix": "%", - "admin_roles": [], - "admin_users": [], - "main_guild": None, - "locale": "fr_FR.UTF8", - }) - - locale.setlocale(locale.LC_TIME, self.config['locale']) - humanize.i18n.activate(self.config['locale']) - self.load_modules() - - @modules_edit - def load_modules(self): - self.info("Starts to load modules...") - e = {} - for module in self.config["modules"]: - e.update({module: self.load_module(module)}) - self.info("Finished to load all modules.") - return e - - @modules_edit - def load_module(self, module): - """ - - Status codes: - - 0: Module loaded - - 1: Module not in modules folder - - 2: Module incomplete - - 3: Module incompatible - - :param module: Module name - :return: Status code - """ - - # Check module compatibility - load_modules_info() - if not MODULES.get(module): - return 1 - if not MODULES[module].exists: - return 1 - if not MODULES[module].complete: - return 2 - if not MODULES[module].compatible: - return 3 - deps = MODULES[module].dependencies - for dep in deps.keys(): - if dep not in self.modules.keys(): - if dep != "base": - self.load_module(dep) - 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.config.save() - except AttributeError as e: - self.error("Module {module} doesn't have MainClass.".format(module=module)) - raise 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.BaseClassLua(self, path=f"modules/{module}/main") - self.modules.update({module: {"imported": imported, "initialized_class": initialized_class}}) - self.info(f"Module {module} successfully imported.") - initialized_class.dispatch("load") - if module not in self.config["modules"]: - self.config["modules"].append(module) - self.config.save() - return 0 - - @modules_edit - def unload_module(self, module): - self.info("Start unload module {module}...".format(module=module)) - try: - if module in self.config["modules"]: - self.config["modules"].remove(module) - self.config.save() - self.unload_all() - self.load_modules() - except KeyError as e: - self.error("Module {module} not loaded.").format(module=module) - return e - - @modules_edit - def reload(self): - del self.modules - self.load_modules() - - @modules_edit - def unload_all(self): - del self.modules - self.modules = {} - - @event - def dispatch(self, event, *args, **kwargs): - # Dispatch to handle wait_* commands - super().dispatch(event, *args, **kwargs) - # Dispatch to modules - for module in self.modules.values(): - module["initialized_class"].dispatch(event, *args, **kwargs) - - async def on_error(self, event_method, *args, **kwargs): - """Function called when error happend""" - # This event is special because it is call directly - self.error(traceback.format_exc()) - for module in self.modules.values(): - await module["initialized_class"].on_error(event_method, *args, **kwargs) - - -class ClientById: - client: LBI - - def __init__(self, client_): - self.client = client_ - - async def fetch_message(self, id_, *args, **kwargs): - """Find a message by id - - :param id_: Id of message to find - :type id_: int - - :raises discord_types.NotFound: This exception is raised when a message is not found (or not accessible by bot) - - :rtype: discord.Message - :return: discord_types.Message instance if message is found. - """ - msg = None - for channel in self.client.get_all_channels(): - try: - return await channel.fetch_message(id_, *args, **kwargs) - except discord.NotFound: - continue - if msg is None: - raise discord.NotFound(None, "Message not found") - - async def edit_message(self, id, *args, **kwargs): - """ - Edit message by id - - :param id: Id of the message to edit - :type id: int""" - message = await self.fetch_message(id) - return await message.edit(**kwargs) - - async def remove_reaction(self, id_message, *args, **kwargs): - """Remove reaction from message by id - - :param id_message: Id of message - :type id_message: int""" - message = await self.fetch_message(id_message) - return await message.remove_reaction(*args, **kwargs) - - async def send_message(self, id_, *args, **kwargs): - """Send message by channel id - - :param id_: Id of channel where to send message - :type id_: int""" - channel = self.client.get_channel(id_) - return channel.send(*args, **kwargs) - - def get_role(self, id_=None, name=None, check=None, guilds=None): - """Get role by id or with custom check""" - if guilds is None: - guilds = self.client.guilds - if id_ is not None: - for guild in guilds: - role = discord.utils.get(guild.roles, id=id_) - if role: - return role - if name is not None: - for guild in guilds: - role = discord.utils.get(guild.roles, name=name) - if role: - return role - if check is not None: - role = None - for guild in guilds: - for role_ in guild.roles: - if check(role_): - role = role_ - break - if role is not None: - break - return role - return None - - - - -class Communication(asyncio.Protocol): - debug = log_communication.debug - info = log_communication.info - warning = log_communication.warning - error = log_communication.error - critical = log_communication.critical - name = "Communication" - - def __init__(self, client): - self.client = client - self.transport = None - - def connection_made(self, transport): - print('%s: connection made' % self.name) - self.transport = transport - - def data_received(self, data): - print('%s: data received: %r' % (self.name, data)) - - def eof_received(self): - pass - - def connection_lost(self, exc): - print('%s: connection lost: %s' % (self.name, exc)) - -if __name__ == "__main__": - client1 = LBI(max_messages=500000) - communication = Communication(client1) - - - async def start_bot(): - await client1.start(os.environ.get("DISCORD_TOKEN")) - - - print(os.path.join("/tmp", os.path.dirname(os.path.realpath(__file__))) + ".sock") - - loop = asyncio.get_event_loop() - t = loop.create_unix_server(Communication, - path=os.path.join("/tmp", os.path.dirname(os.path.realpath(__file__)) + ".sock")) - if not sys.platform == "win32": - loop.run_until_complete(t) - - loop.create_task(start_bot()) - loop.run_forever() +#!/usr/bin/python3 +from __future__ import annotations + +import asyncio +import importlib +import json +import locale +import logging +import logging.config +import os +import sys +import traceback +from typing import Dict + +import discord +import humanize +from packaging.version import Version + +from config import Config, config_types +from config.config_types import factory +from errors import IncompatibleModule +from modules.base import base_supported_type + +__version__ = "0.1.0" + + +class Module: + name: str + + def __init__(self, name: str): + """ + Init module + + :param name: Module name + :type name: str + """ + 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: + """ + Check if module exists + + :return: True if module is present in modules folders + :rtype: bool + """ + if not os.path.isdir(os.path.join("modules", self.name)): + return False + return True + + @property + def complete(self) -> bool: + """ + Check if module is complete + + :return: True if module is compatible + :rtype: Boolean + """ + # Check if version.json exists + if not os.path.exists(os.path.join("modules", self.name, "version.json")): + return False + with open(os.path.join("modules", self.name, "version.json")) as file: + versions = json.load(file) + if "version" not in versions.keys(): + return False + if "dependencies" not in versions.keys(): + return False + if "bot_version" not in versions.keys(): + return False + if "type" not in versions.keys(): + return False + if versions["type"] not in base_supported_type: + return False + return True + + @property + def version(self) -> Version: + """ + Returns module version + + :return: current module version + :rtype: Version + """ + with open(os.path.join("modules", self.name, "version.json")) as file: + versions = json.load(file) + return Version(versions["version"]) + + @property + def bot_version(self) -> dict: + """ + returns the min and max version of the bot that is compatible with the module + + :return: Min and max version for bot + :rtype: dict + :raises IncompatibleModule: If bot_version is not properly formated (there must be min and max keys) + """ + with open(os.path.join("modules", self.name, "version.json")) as file: + versions = json.load(file) + try: + return {"min": Version(versions["bot_version"]["min"]), + "max": Version(versions["bot_version"]["max"])} + except KeyError: + raise IncompatibleModule(f"Module {self.name} is not compatible with bot (version.json does not " + f"contain bot_version.max or bot_version.min item)") + + @property + def dependencies(self) -> dict: + """ + return list of dependencies version + + :raise IncompatibleModule: If bot_version is not properly formated (there must be min and max keys for each dependencies) + :return: list of dependencies version + :rtype: dict + """ + with open(os.path.join("modules", self.name, "version.json")) as file: + versions = json.load(file) + try: + deps = {} + for name, dep in versions["dependencies"].items(): + dep_ver = {"min": Version(dep["min"]), + "max": Version(dep["max"])} + deps.update({name: dep_ver}) + return deps + except KeyError: + raise IncompatibleModule(f"Module {self.name} is not compatible with bot (version.json does not " + f"contain dependencies.modulename.max or dependencies.modulename.min item)") + + @property + def compatible(self) -> bool: + """ + Check if module is compatible with current installation + + :return: True if all dependencies are okays + :rtype: bool + """ + # Check bot version + bot_ver = Version(__version__) + if bot_ver < self.bot_version["min"]: + return False + if bot_ver > self.bot_version["max"]: + return False + for name, dep in self.dependencies.items(): + if name not in MODULES.keys(): + Module(name) + if MODULES[name].version < dep["min"]: + return False + if MODULES[name].version > dep["max"]: + return False + return True + + +MODULES: Dict[str, Module] = {} + + +def setup_logging(default_path='config/log_config.json', default_level=logging.INFO, env_key='LBI_LOG_CONFIG'): + """Setup logging configuration + """ + path = default_path + value = os.getenv(env_key, None) + if value: + path = value + if os.path.exists(path): + with open(path, 'rt') as f: + config = json.load(f) + logging.config.dictConfig(config) + else: + logging.basicConfig(level=default_level) + + +def modules_edit(func): + def wrapper(self, *args, **kwargs): + if self.reloading: + return func(self, *args, **kwargs) + else: + self.reloading = True + a = func(self, *args, **kwargs) + self.reloading = False + return a + + return wrapper + + +def event(func): + def wrapper(self, *args, **kwargs): + if self.reloading: + return lambda: None + else: + return func(self, *args, **kwargs) + + return wrapper + + +"""def async_event(func): + async def wrapper(self, *args, **kwargs): + if self.reloading: + return lambda: None + else: + return func(self, *args, **kwargs) + return wrapper""" + +setup_logging() + +log_discord = logging.getLogger('discord_types') +log_LBI = logging.getLogger('LBI') +log_communication = logging.getLogger('communication') + + +def load_modules_info(): + for mod in os.listdir("modules"): + Module(mod) + + +class LBI(discord.Client): + by_id: ClientById + base_path = "data" + debug = log_LBI.debug + info = log_LBI.info + warning = log_LBI.warning + warn = warning + error = log_LBI.error + critical = log_LBI.critical + + def __init__(self, config: Config = None, *args, **kwargs): + super().__init__(*args, **kwargs) + if config is None: + config = Config(path="data/config.toml", client=self) + self.reloading = False + self.by_id = ClientById(self) + self.ready = False + # Content: {"module_name": {"module": imported module, "class": initialized class}} + self.modules = {} + + self.config = config + self.config.register("modules", factory(config_types.List, factory(config_types.Str))) + self.config.register("prefix", factory(config_types.Str)) + self.config.register("admin_roles", factory(config_types.List, factory(config_types.discord_types.Role, self))) + self.config.register("admin_users", factory(config_types.List, factory(config_types.discord_types.User, self))) + self.config.register("main_guild", factory(config_types.discord_types.Guild, self)) + self.config.register("locale", factory(config_types.Str)) + + self.config.set({ + "modules": ["modules", "errors"], + "prefix": "%", + "admin_roles": [], + "admin_users": [], + "main_guild": None, + "locale": "fr_FR.UTF8", + }) + + locale.setlocale(locale.LC_TIME, self.config['locale']) + humanize.i18n.activate(self.config['locale']) + self.load_modules() + + @modules_edit + def load_modules(self): + self.info("Starts to load modules...") + e = {} + for module in self.config["modules"]: + e.update({module: self.load_module(module)}) + self.info("Finished to load all modules.") + return e + + @modules_edit + def load_module(self, module): + """ + + Status codes: + - 0: Module loaded + - 1: Module not in modules folder + - 2: Module incomplete + - 3: Module incompatible + + :param module: Module name + :return: Status code + """ + + # Check module compatibility + load_modules_info() + if not MODULES.get(module): + return 1 + if not MODULES[module].exists: + return 1 + if not MODULES[module].complete: + return 2 + if not MODULES[module].compatible: + return 3 + deps = MODULES[module].dependencies + for dep in deps.keys(): + if dep not in self.modules.keys(): + if dep != "base": + self.load_module(dep) + 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.config.save() + except AttributeError as e: + self.error("Module {module} doesn't have MainClass.".format(module=module)) + raise 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.BaseClassLua(self, path=f"modules/{module}/main") + self.modules.update({module: {"imported": imported, "initialized_class": initialized_class}}) + self.info(f"Module {module} successfully imported.") + initialized_class.dispatch("load") + if module not in self.config["modules"]: + self.config["modules"].append(module) + self.config.save() + return 0 + + @modules_edit + def unload_module(self, module): + self.info("Start unload module {module}...".format(module=module)) + try: + if module in self.config["modules"]: + self.config["modules"].remove(module) + self.config.save() + self.unload_all() + self.load_modules() + except KeyError as e: + self.error("Module {module} not loaded.").format(module=module) + return e + + @modules_edit + def reload(self): + del self.modules + self.load_modules() + + @modules_edit + def unload_all(self): + del self.modules + self.modules = {} + + @event + def dispatch(self, event, *args, **kwargs): + # Dispatch to handle wait_* commands + super().dispatch(event, *args, **kwargs) + # Dispatch to modules + for module in self.modules.values(): + module["initialized_class"].dispatch(event, *args, **kwargs) + + async def on_error(self, event_method, *args, **kwargs): + """Function called when error happend""" + # This event is special because it is call directly + self.error(traceback.format_exc()) + for module in self.modules.values(): + await module["initialized_class"].on_error(event_method, *args, **kwargs) + + +class ClientById: + client: LBI + + def __init__(self, client_): + self.client = client_ + + async def fetch_message(self, id_, *args, **kwargs): + """Find a message by id + + :param id_: Id of message to find + :type id_: int + + :raises discord_types.NotFound: This exception is raised when a message is not found (or not accessible by bot) + + :rtype: discord.Message + :return: discord_types.Message instance if message is found. + """ + msg = None + for channel in self.client.get_all_channels(): + try: + return await channel.fetch_message(id_, *args, **kwargs) + except discord.NotFound: + continue + if msg is None: + raise discord.NotFound(None, "Message not found") + + async def edit_message(self, id, *args, **kwargs): + """ + Edit message by id + + :param id: Id of the message to edit + :type id: int""" + message = await self.fetch_message(id) + return await message.edit(**kwargs) + + async def remove_reaction(self, id_message, *args, **kwargs): + """Remove reaction from message by id + + :param id_message: Id of message + :type id_message: int""" + message = await self.fetch_message(id_message) + return await message.remove_reaction(*args, **kwargs) + + async def send_message(self, id_, *args, **kwargs): + """Send message by channel id + + :param id_: Id of channel where to send message + :type id_: int""" + channel = self.client.get_channel(id_) + return channel.send(*args, **kwargs) + + def get_role(self, id_=None, name=None, check=None, guilds=None): + """Get role by id or with custom check""" + if guilds is None: + guilds = self.client.guilds + if id_ is not None: + for guild in guilds: + role = discord.utils.get(guild.roles, id=id_) + if role: + return role + if name is not None: + for guild in guilds: + role = discord.utils.get(guild.roles, name=name) + if role: + return role + if check is not None: + role = None + for guild in guilds: + for role_ in guild.roles: + if check(role_): + role = role_ + break + if role is not None: + break + return role + return None + + + + +class Communication(asyncio.Protocol): + debug = log_communication.debug + info = log_communication.info + warning = log_communication.warning + error = log_communication.error + critical = log_communication.critical + name = "Communication" + + def __init__(self, client): + self.client = client + self.transport = None + + def connection_made(self, transport): + print('%s: connection made' % self.name) + self.transport = transport + + def data_received(self, data): + print('%s: data received: %r' % (self.name, data)) + + def eof_received(self): + pass + + def connection_lost(self, exc): + print('%s: connection lost: %s' % (self.name, exc)) + +if __name__ == "__main__": + client1 = LBI(max_messages=500000) + communication = Communication(client1) + + + async def start_bot(): + await client1.start(os.environ.get("DISCORD_TOKEN")) + + + print(os.path.join("/tmp", os.path.dirname(os.path.realpath(__file__))) + ".sock") + + loop = asyncio.get_event_loop() + t = loop.create_unix_server(Communication, + path=os.path.join("/tmp", os.path.dirname(os.path.realpath(__file__)) + ".sock")) + if not sys.platform == "win32": + loop.run_until_complete(t) + + loop.create_task(start_bot()) + loop.run_forever() diff --git a/utils/__init__.py b/src/modules/__init__.py similarity index 100% rename from utils/__init__.py rename to src/modules/__init__.py diff --git a/modules/avalon/__init__.py b/src/modules/avalon/__init__.py similarity index 100% rename from modules/avalon/__init__.py rename to src/modules/avalon/__init__.py diff --git a/src/modules/avalon/roles.py b/src/modules/avalon/roles.py new file mode 100644 index 0000000..e69de29 diff --git a/modules/avalon/version.json b/src/modules/avalon/version.json similarity index 92% rename from modules/avalon/version.json rename to src/modules/avalon/version.json index b27fd9a..53fa16b 100644 --- a/modules/avalon/version.json +++ b/src/modules/avalon/version.json @@ -1,11 +1,11 @@ -{ - "version":"0.1.0", - "type": "python", - "dependencies": { - - }, - "bot_version": { - "min": "0.1.0", - "max": "0.1.0" - } +{ + "version":"0.1.0", + "type": "python", + "dependencies": { + + }, + "bot_version": { + "min": "0.1.0", + "max": "0.1.0" + } } \ No newline at end of file diff --git a/modules/base/__init__.py b/src/modules/base/__init__.py similarity index 98% rename from modules/base/__init__.py rename to src/modules/base/__init__.py index e5d1805..2e94b77 100644 --- a/modules/base/__init__.py +++ b/src/modules/base/__init__.py @@ -1,3 +1,3 @@ -from .BasePython import BaseClassPython -from .BaseLua import BaseClassLua +from .BasePython import BaseClassPython +from .BaseLua import BaseClassLua base_supported_type = ["python", "lua"] \ No newline at end of file diff --git a/modules/base/Base.py b/src/modules/base/base.py similarity index 98% rename from modules/base/Base.py rename to src/modules/base/base.py index c63bf32..d419638 100644 --- a/modules/base/Base.py +++ b/src/modules/base/base.py @@ -5,7 +5,8 @@ from typing import List, Union, Optional import discord -from config import Config, config_types +from config import Config +from config import config_types from config.config_types import factory from storage import Objects from utils import emojis @@ -35,9 +36,9 @@ class BaseClass: self.config.register("color", factory(config_types.Color)) self.config.register("auth_everyone", factory(config_types.Bool)) self.config.register("authorized_roles", - factory(config_types.List, factory(config_types.discord_types.Role, client))) + factory(config_types.List, factory(config.config_types.discord_types.Role, client))) self.config.register("authorized_users", - factory(config_types.List, factory(config_types.discord_types.User, client))) + factory(config_types.List, factory(config.config_types.discord_types.User, client))) self.config.register("command_text", factory(config_types.Str)) self.config.set({"help_active": True, "color": 0x000000, diff --git a/modules/base/BaseLua.py b/src/modules/base/base_lua.py similarity index 98% rename from modules/base/BaseLua.py rename to src/modules/base/base_lua.py index 51c57cd..c5ce17d 100644 --- a/modules/base/BaseLua.py +++ b/src/modules/base/base_lua.py @@ -4,7 +4,7 @@ import asyncio import discord import lupa -from modules.base.Base import BaseClass +from modules import BaseClass class BaseClassLua(BaseClass): diff --git a/modules/base/BasePython.py b/src/modules/base/base_python.py similarity index 80% rename from modules/base/BasePython.py rename to src/modules/base/base_python.py index fbb52a8..5cb7d7f 100644 --- a/modules/base/BasePython.py +++ b/src/modules/base/base_python.py @@ -1,6 +1,6 @@ """Base class for module, never use directly !!!""" -from modules.base.Base import BaseClass +from .Base import BaseClass class BaseClassPython(BaseClass): diff --git a/modules/base/version.json b/src/modules/base/version.json similarity index 92% rename from modules/base/version.json rename to src/modules/base/version.json index b27fd9a..53fa16b 100644 --- a/modules/base/version.json +++ b/src/modules/base/version.json @@ -1,11 +1,11 @@ -{ - "version":"0.1.0", - "type": "python", - "dependencies": { - - }, - "bot_version": { - "min": "0.1.0", - "max": "0.1.0" - } +{ + "version":"0.1.0", + "type": "python", + "dependencies": { + + }, + "bot_version": { + "min": "0.1.0", + "max": "0.1.0" + } } \ No newline at end of file diff --git a/modules/clean/__init__.py b/src/modules/clean/__init__.py similarity index 100% rename from modules/clean/__init__.py rename to src/modules/clean/__init__.py diff --git a/modules/clean/version.json b/src/modules/clean/version.json similarity index 92% rename from modules/clean/version.json rename to src/modules/clean/version.json index b27fd9a..53fa16b 100644 --- a/modules/clean/version.json +++ b/src/modules/clean/version.json @@ -1,11 +1,11 @@ -{ - "version":"0.1.0", - "type": "python", - "dependencies": { - - }, - "bot_version": { - "min": "0.1.0", - "max": "0.1.0" - } +{ + "version":"0.1.0", + "type": "python", + "dependencies": { + + }, + "bot_version": { + "min": "0.1.0", + "max": "0.1.0" + } } \ No newline at end of file diff --git a/modules/errors/__init__.py b/src/modules/errors/__init__.py similarity index 100% rename from modules/errors/__init__.py rename to src/modules/errors/__init__.py diff --git a/modules/modules/version.json b/src/modules/errors/version.json similarity index 93% rename from modules/modules/version.json rename to src/modules/errors/version.json index 4cb3a98..85a57f9 100644 --- a/modules/modules/version.json +++ b/src/modules/errors/version.json @@ -1,14 +1,14 @@ -{ - "version": "0.1.0", - "type": "python", - "dependencies": { - "base": { - "min": "0.1.0", - "max": "0.1.0" - } - }, - "bot_version": { - "min": "0.1.0", - "max": "0.1.0" - } +{ + "version": "0.1.0", + "type": "python", + "dependencies": { + "base": { + "min": "0.1.0", + "max": "0.1.0" + } + }, + "bot_version": { + "min": "0.1.0", + "max": "0.1.0" + } } \ No newline at end of file diff --git a/modules/help/__init__.py b/src/modules/help/__init__.py similarity index 100% rename from modules/help/__init__.py rename to src/modules/help/__init__.py diff --git a/modules/help/version.json b/src/modules/help/version.json similarity index 92% rename from modules/help/version.json rename to src/modules/help/version.json index b27fd9a..53fa16b 100644 --- a/modules/help/version.json +++ b/src/modules/help/version.json @@ -1,11 +1,11 @@ -{ - "version":"0.1.0", - "type": "python", - "dependencies": { - - }, - "bot_version": { - "min": "0.1.0", - "max": "0.1.0" - } +{ + "version":"0.1.0", + "type": "python", + "dependencies": { + + }, + "bot_version": { + "min": "0.1.0", + "max": "0.1.0" + } } \ No newline at end of file diff --git a/modules/modules/__init__.py b/src/modules/modules/__init__.py similarity index 97% rename from modules/modules/__init__.py rename to src/modules/modules/__init__.py index 31144c1..e5b646f 100644 --- a/modules/modules/__init__.py +++ b/src/modules/modules/__init__.py @@ -1,141 +1,141 @@ -import os - -import discord -from aiohttp import ClientConnectorError - -from modules.base import BaseClassPython -from modules.modules.api import Api - - -class MainClass(BaseClassPython): - name = "modules" - help = { - "description": "Manage bot modules.", - "commands": { - "`{prefix}{command} list`": "List of available modules.", - "`{prefix}{command} enable `": "Enable module ``.", - "`{prefix}{command} disable `": "Disable module ``.", - "`{prefix}{command} reload `": "Reload module ``", - "`{prefix}{command} web_list`": "List all available modules from repository", - # "`{prefix}{command} web_source`": "List all source repositories", - # "`{prefix}{command} web_source remove `": "Remove url from repository list", - # "`{prefix}{command} web_source add `": "Add url to repository list", - } - } - - def __init__(self, client): - super().__init__(client) - os.makedirs("modules", exist_ok=True) - self.api = Api() - - @staticmethod - def get_all_modules(): - all_items = os.listdir("modules") - modules = [] - for item in all_items: - if item not in ["__init__.py", "base", "__pycache__"]: - if os.path.isfile(os.path.join("modules", item)): - modules.append(item[:-3]) - else: - modules.append(item) - return set(modules) - - async def com_enable(self, message, args, kwargs): - args = args[1:] - if len(args) == 0: - await message.channel.send("You must specify at least one module.") - return - if len(args) == 1 and args[0] == "*": - for module in self.get_all_modules(): - e = self.client.load_module(module) - if e: - await message.channel.send("An error occurred during the loading of the module {module}." - .format(module=module)) - await self.com_list(message, args, kwargs) - return - for arg in args: - e = self.client.load_module(arg) - if e == 1: - await message.channel.send(f"Module {arg} not exists.") - if e == 2: - await message.channel.send(f"Module {arg} is incompatible.") - elif e: - await message.channel.send(f"An error occurred during the loading of the module {arg}: {e}.") - await self.com_list(message, args, kwargs) - - async def com_reload(self, message, args, kwargs): - args = args[1:] - if len(args) == 0: - await message.channel.send("You must specify at least one module.") - return - if len(args) == 1 and args[0] == "*": - for module in self.get_all_modules(): - e = self.client.unload_module(module) - if e: - await message.channel.send(f"An error occurred during the unloading of the module {module}.") - e = self.client.load_module(module) - if e: - await message.channel.send(f"An error occurred during the loading of the module {module}.") - await self.com_list(message, args, kwargs) - return - for arg in args: - e = self.client.unload_module(arg) - if e: - await message.channel.send(f"An error occurred during the unloading of the module {arg}.") - e = self.client.load_module(arg) - if e: - await message.channel.send(f"An error occurred during the loading of the module {arg}.") - await self.com_list(message, [], []) - - async def com_disable(self, message, args, kwargs): - args = args[1:] - if len(args) == 0: - await message.channel.send("You must specify at least one module.") - return - if len(args) == 1 and args[0] == "*": - for module in self.get_all_modules(): - e = self.client.unload_module(module) - if e: - await message.channel.send(f"An error occurred during the loading of the module {module}.") - await self.com_list(message, args, kwargs) - return - for arg in args: - e = self.client.unload_module(arg) - if e: - await message.channel.send(f"An error occurred during the loading of the module {arg}: {e}.") - await self.com_list(message, [], []) - - async def com_list(self, message, args, kwargs): - list_files = self.get_all_modules() - activated = set(self.client.config["modules"]) - if len(activated): - activated_string = "\n+ " + "\n+ ".join(activated) - else: - activated_string = "" - if len(activated) != len(list_files): - deactivated_string = "\n- " + "\n- ".join(list_files.difference(activated)) - else: - deactivated_string = "" - embed = discord.Embed(title="[Modules] - Liste des modules", - description="```diff{activated}{deactivated}```".format( - activated=activated_string, - deactivated=deactivated_string) - ) - await message.channel.send(embed=embed) - - async def com_web_list(self, message, args, kwargs): - try: - modules = await self.api.list() - except ClientConnectorError: - await message.channel.send("Connection impossible au serveur.") - return - text = "" - for module, versions in modules.items(): - text += module + " - " + ", ".join(versions) - await message.channel.send(text) - - async def com_web_dl(self, message, args, kwargs): - try: - await self.api.download(args[1], args[2]) - except ClientConnectorError: - await message.channel.send("Connection impossible au serveur.") +import os + +import discord +from aiohttp import ClientConnectorError + +from modules.base import BaseClassPython +from modules.modules.api import Api + + +class MainClass(BaseClassPython): + name = "modules" + help = { + "description": "Manage bot modules.", + "commands": { + "`{prefix}{command} list`": "List of available modules.", + "`{prefix}{command} enable `": "Enable module ``.", + "`{prefix}{command} disable `": "Disable module ``.", + "`{prefix}{command} reload `": "Reload module ``", + "`{prefix}{command} web_list`": "List all available modules from repository", + # "`{prefix}{command} web_source`": "List all source repositories", + # "`{prefix}{command} web_source remove `": "Remove url from repository list", + # "`{prefix}{command} web_source add `": "Add url to repository list", + } + } + + def __init__(self, client): + super().__init__(client) + os.makedirs("modules", exist_ok=True) + self.api = Api() + + @staticmethod + def get_all_modules(): + all_items = os.listdir("modules") + modules = [] + for item in all_items: + if item not in ["__init__.py", "base", "__pycache__"]: + if os.path.isfile(os.path.join("modules", item)): + modules.append(item[:-3]) + else: + modules.append(item) + return set(modules) + + async def com_enable(self, message, args, kwargs): + args = args[1:] + if len(args) == 0: + await message.channel.send("You must specify at least one module.") + return + if len(args) == 1 and args[0] == "*": + for module in self.get_all_modules(): + e = self.client.load_module(module) + if e: + await message.channel.send("An error occurred during the loading of the module {module}." + .format(module=module)) + await self.com_list(message, args, kwargs) + return + for arg in args: + e = self.client.load_module(arg) + if e == 1: + await message.channel.send(f"Module {arg} not exists.") + if e == 2: + await message.channel.send(f"Module {arg} is incompatible.") + elif e: + await message.channel.send(f"An error occurred during the loading of the module {arg}: {e}.") + await self.com_list(message, args, kwargs) + + async def com_reload(self, message, args, kwargs): + args = args[1:] + if len(args) == 0: + await message.channel.send("You must specify at least one module.") + return + if len(args) == 1 and args[0] == "*": + for module in self.get_all_modules(): + e = self.client.unload_module(module) + if e: + await message.channel.send(f"An error occurred during the unloading of the module {module}.") + e = self.client.load_module(module) + if e: + await message.channel.send(f"An error occurred during the loading of the module {module}.") + await self.com_list(message, args, kwargs) + return + for arg in args: + e = self.client.unload_module(arg) + if e: + await message.channel.send(f"An error occurred during the unloading of the module {arg}.") + e = self.client.load_module(arg) + if e: + await message.channel.send(f"An error occurred during the loading of the module {arg}.") + await self.com_list(message, [], []) + + async def com_disable(self, message, args, kwargs): + args = args[1:] + if len(args) == 0: + await message.channel.send("You must specify at least one module.") + return + if len(args) == 1 and args[0] == "*": + for module in self.get_all_modules(): + e = self.client.unload_module(module) + if e: + await message.channel.send(f"An error occurred during the loading of the module {module}.") + await self.com_list(message, args, kwargs) + return + for arg in args: + e = self.client.unload_module(arg) + if e: + await message.channel.send(f"An error occurred during the loading of the module {arg}: {e}.") + await self.com_list(message, [], []) + + async def com_list(self, message, args, kwargs): + list_files = self.get_all_modules() + activated = set(self.client.config["modules"]) + if len(activated): + activated_string = "\n+ " + "\n+ ".join(activated) + else: + activated_string = "" + if len(activated) != len(list_files): + deactivated_string = "\n- " + "\n- ".join(list_files.difference(activated)) + else: + deactivated_string = "" + embed = discord.Embed(title="[Modules] - Liste des modules", + description="```diff{activated}{deactivated}```".format( + activated=activated_string, + deactivated=deactivated_string) + ) + await message.channel.send(embed=embed) + + async def com_web_list(self, message, args, kwargs): + try: + modules = await self.api.list() + except ClientConnectorError: + await message.channel.send("Connection impossible au serveur.") + return + text = "" + for module, versions in modules.items(): + text += module + " - " + ", ".join(versions) + await message.channel.send(text) + + async def com_web_dl(self, message, args, kwargs): + try: + await self.api.download(args[1], args[2]) + except ClientConnectorError: + await message.channel.send("Connection impossible au serveur.") diff --git a/modules/modules/api.py b/src/modules/modules/api.py similarity index 100% rename from modules/modules/api.py rename to src/modules/modules/api.py diff --git a/modules/errors/version.json b/src/modules/modules/version.json similarity index 93% rename from modules/errors/version.json rename to src/modules/modules/version.json index 4cb3a98..85a57f9 100644 --- a/modules/errors/version.json +++ b/src/modules/modules/version.json @@ -1,14 +1,14 @@ -{ - "version": "0.1.0", - "type": "python", - "dependencies": { - "base": { - "min": "0.1.0", - "max": "0.1.0" - } - }, - "bot_version": { - "min": "0.1.0", - "max": "0.1.0" - } +{ + "version": "0.1.0", + "type": "python", + "dependencies": { + "base": { + "min": "0.1.0", + "max": "0.1.0" + } + }, + "bot_version": { + "min": "0.1.0", + "max": "0.1.0" + } } \ No newline at end of file diff --git a/modules/newmember/__init__.py b/src/modules/newmember/__init__.py similarity index 100% rename from modules/newmember/__init__.py rename to src/modules/newmember/__init__.py diff --git a/src/modules/newmember/version.json b/src/modules/newmember/version.json new file mode 100644 index 0000000..53fa16b --- /dev/null +++ b/src/modules/newmember/version.json @@ -0,0 +1,11 @@ +{ + "version":"0.1.0", + "type": "python", + "dependencies": { + + }, + "bot_version": { + "min": "0.1.0", + "max": "0.1.0" + } +} \ No newline at end of file diff --git a/modules/panic/__init__.py b/src/modules/panic/__init__.py similarity index 100% rename from modules/panic/__init__.py rename to src/modules/panic/__init__.py diff --git a/src/modules/panic/version.json b/src/modules/panic/version.json new file mode 100644 index 0000000..53fa16b --- /dev/null +++ b/src/modules/panic/version.json @@ -0,0 +1,11 @@ +{ + "version":"0.1.0", + "type": "python", + "dependencies": { + + }, + "bot_version": { + "min": "0.1.0", + "max": "0.1.0" + } +} \ No newline at end of file diff --git a/modules/perdu/__init__.py b/src/modules/perdu/__init__.py similarity index 100% rename from modules/perdu/__init__.py rename to src/modules/perdu/__init__.py diff --git a/src/modules/perdu/version.json b/src/modules/perdu/version.json new file mode 100644 index 0000000..53fa16b --- /dev/null +++ b/src/modules/perdu/version.json @@ -0,0 +1,11 @@ +{ + "version":"0.1.0", + "type": "python", + "dependencies": { + + }, + "bot_version": { + "min": "0.1.0", + "max": "0.1.0" + } +} \ No newline at end of file diff --git a/modules/purge/__init__.py b/src/modules/purge/__init__.py similarity index 100% rename from modules/purge/__init__.py rename to src/modules/purge/__init__.py diff --git a/src/modules/purge/version.json b/src/modules/purge/version.json new file mode 100644 index 0000000..53fa16b --- /dev/null +++ b/src/modules/purge/version.json @@ -0,0 +1,11 @@ +{ + "version":"0.1.0", + "type": "python", + "dependencies": { + + }, + "bot_version": { + "min": "0.1.0", + "max": "0.1.0" + } +} \ No newline at end of file diff --git a/modules/readrules/__init__.py b/src/modules/readrules/__init__.py similarity index 100% rename from modules/readrules/__init__.py rename to src/modules/readrules/__init__.py diff --git a/src/modules/readrules/version.json b/src/modules/readrules/version.json new file mode 100644 index 0000000..53fa16b --- /dev/null +++ b/src/modules/readrules/version.json @@ -0,0 +1,11 @@ +{ + "version":"0.1.0", + "type": "python", + "dependencies": { + + }, + "bot_version": { + "min": "0.1.0", + "max": "0.1.0" + } +} \ No newline at end of file diff --git a/modules/restart/__init__.py b/src/modules/restart/__init__.py similarity index 100% rename from modules/restart/__init__.py rename to src/modules/restart/__init__.py diff --git a/src/modules/restart/version.json b/src/modules/restart/version.json new file mode 100644 index 0000000..53fa16b --- /dev/null +++ b/src/modules/restart/version.json @@ -0,0 +1,11 @@ +{ + "version":"0.1.0", + "type": "python", + "dependencies": { + + }, + "bot_version": { + "min": "0.1.0", + "max": "0.1.0" + } +} \ No newline at end of file diff --git a/modules/roles/__init__.py b/src/modules/roles/__init__.py similarity index 100% rename from modules/roles/__init__.py rename to src/modules/roles/__init__.py diff --git a/src/modules/roles/version.json b/src/modules/roles/version.json new file mode 100644 index 0000000..53fa16b --- /dev/null +++ b/src/modules/roles/version.json @@ -0,0 +1,11 @@ +{ + "version":"0.1.0", + "type": "python", + "dependencies": { + + }, + "bot_version": { + "min": "0.1.0", + "max": "0.1.0" + } +} \ No newline at end of file diff --git a/modules/rtfgd/__init__.py b/src/modules/rtfgd/__init__.py similarity index 100% rename from modules/rtfgd/__init__.py rename to src/modules/rtfgd/__init__.py diff --git a/src/modules/rtfgd/version.json b/src/modules/rtfgd/version.json new file mode 100644 index 0000000..53fa16b --- /dev/null +++ b/src/modules/rtfgd/version.json @@ -0,0 +1,11 @@ +{ + "version":"0.1.0", + "type": "python", + "dependencies": { + + }, + "bot_version": { + "min": "0.1.0", + "max": "0.1.0" + } +} \ No newline at end of file diff --git a/pytest.ini b/src/pytest.ini similarity index 100% rename from pytest.ini rename to src/pytest.ini diff --git a/storage/__init__.py b/src/storage/__init__.py similarity index 100% rename from storage/__init__.py rename to src/storage/__init__.py diff --git a/storage/jsonencoder.py b/src/storage/jsonencoder.py similarity index 100% rename from storage/jsonencoder.py rename to src/storage/jsonencoder.py diff --git a/storage/objects.py b/src/storage/objects.py similarity index 100% rename from storage/objects.py rename to src/storage/objects.py diff --git a/src/utils/__init__.py b/src/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/utils/emojis.py b/src/utils/emojis.py similarity index 100% rename from utils/emojis.py rename to src/utils/emojis.py From a590a4cba1165d322cccce491da1e59282361dab Mon Sep 17 00:00:00 2001 From: Louis Chauvet Date: Tue, 21 Apr 2020 15:11:06 +0200 Subject: [PATCH 02/15] =?UTF-8?q?[bot-base]=20Gros=20retravail=20des=20mod?= =?UTF-8?q?ules=20[modules]=20Suppression=20[doc]=20Mise=20=C3=A0=20jour?= =?UTF-8?q?=20du=20path=20[config]=20Mise=20=C3=A0=20jour=20de=20la=20doc?= =?UTF-8?q?=20[storage]=20Un=20module=20python=20plus=20propre=20[main]=20?= =?UTF-8?q?Un=20main=20ne=20doit=20faire=20que=20le=20role=20du=20main=20[?= =?UTF-8?q?errors]=20Ajout=20des=20erreurs=20n=C3=A9cessaires=20pour=20le?= =?UTF-8?q?=20chargement=20d'un=20module=20[utils]=20Un=20module=20python?= =?UTF-8?q?=20plus=20propre=20[scripts]=20Mise=20=C3=A0=20jour=20des=20scr?= =?UTF-8?q?ipts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- doc/source/conf.py | 2 +- scripts/build-docs.sh | 2 + scripts/run-tests.sh | 4 +- src/bot_base/bot_base.py | 147 ++++++ src/config/__init__.py | 5 +- src/config/config_types/__init__.py | 22 +- src/config/config_types/bool.py | 2 +- src/config/config_types/color.py | 2 +- src/config/config_types/dict.py | 2 +- .../config_types/discord_types/__init__.py | 2 +- .../config_types/discord_types/guild.py | 13 +- src/config/config_types/float.py | 2 +- src/config/config_types/int.py | 2 +- src/config/config_types/list.py | 2 +- src/config/config_types/str.py | 2 +- src/errors.py | 22 +- src/main.py | 472 +----------------- src/modules/avalon/__init__.py | 56 --- src/modules/avalon/version.json | 11 - src/modules/base/__init__.py | 3 - src/modules/base/base.py | 276 ---------- src/modules/base/base_lua.py | 73 --- src/modules/base/base_python.py | 8 - src/modules/base/version.json | 11 - src/modules/clean/__init__.py | 17 - src/modules/clean/version.json | 11 - src/modules/errors/__init__.py | 120 ----- src/modules/errors/version.json | 14 - src/modules/help/__init__.py | 35 -- src/modules/help/version.json | 11 - src/modules/modules/__init__.py | 141 ------ src/modules/modules/api.py | 40 -- src/modules/modules/version.json | 14 - src/modules/newmember/__init__.py | 31 -- src/modules/newmember/version.json | 11 - src/modules/panic/__init__.py | 44 -- src/modules/panic/version.json | 11 - src/modules/perdu/__init__.py | 221 -------- src/modules/perdu/version.json | 11 - src/modules/purge/__init__.py | 35 -- src/modules/purge/version.json | 11 - src/modules/readrules/__init__.py | 38 -- src/modules/readrules/version.json | 11 - src/modules/restart/__init__.py | 19 - src/modules/restart/version.json | 11 - src/modules/roles/__init__.py | 129 ----- src/modules/roles/version.json | 11 - src/modules/rtfgd/__init__.py | 25 - src/modules/rtfgd/version.json | 11 - src/storage/__init__.py | 3 + src/storage/objects.py | 2 +- src/utils/__init__.py | 3 + 52 files changed, 197 insertions(+), 1987 deletions(-) delete mode 100644 src/modules/avalon/__init__.py delete mode 100644 src/modules/avalon/version.json delete mode 100644 src/modules/base/__init__.py delete mode 100644 src/modules/base/base.py delete mode 100644 src/modules/base/base_lua.py delete mode 100644 src/modules/base/base_python.py delete mode 100644 src/modules/base/version.json delete mode 100644 src/modules/clean/__init__.py delete mode 100644 src/modules/clean/version.json delete mode 100644 src/modules/errors/__init__.py delete mode 100644 src/modules/errors/version.json delete mode 100644 src/modules/help/__init__.py delete mode 100644 src/modules/help/version.json delete mode 100644 src/modules/modules/__init__.py delete mode 100644 src/modules/modules/api.py delete mode 100644 src/modules/modules/version.json delete mode 100644 src/modules/newmember/__init__.py delete mode 100644 src/modules/newmember/version.json delete mode 100644 src/modules/panic/__init__.py delete mode 100644 src/modules/panic/version.json delete mode 100644 src/modules/perdu/__init__.py delete mode 100644 src/modules/perdu/version.json delete mode 100644 src/modules/purge/__init__.py delete mode 100644 src/modules/purge/version.json delete mode 100644 src/modules/readrules/__init__.py delete mode 100644 src/modules/readrules/version.json delete mode 100644 src/modules/restart/__init__.py delete mode 100644 src/modules/restart/version.json delete mode 100644 src/modules/roles/__init__.py delete mode 100644 src/modules/roles/version.json delete mode 100644 src/modules/rtfgd/__init__.py delete mode 100644 src/modules/rtfgd/version.json diff --git a/doc/source/conf.py b/doc/source/conf.py index 9bd00d1..c2c81fb 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -13,7 +13,7 @@ import os import sys -sys.path.insert(0, os.path.abspath('../')) +sys.path.insert(0, os.path.abspath('../../')) # -- Project information ----------------------------------------------------- diff --git a/scripts/build-docs.sh b/scripts/build-docs.sh index 138ea02..90781fd 100644 --- a/scripts/build-docs.sh +++ b/scripts/build-docs.sh @@ -1,2 +1,4 @@ +cd doc # Build html doc pipenv run make html +cd ../ \ No newline at end of file diff --git a/scripts/run-tests.sh b/scripts/run-tests.sh index 5594408..9224bf6 100644 --- a/scripts/run-tests.sh +++ b/scripts/run-tests.sh @@ -4,10 +4,12 @@ set -e -cd "${0%/*}/.." +cd "${0%/*}/../src" echo "Running tests" # Run test and ignore warnings pipenv run pytest -p no:warnings +cd "../" + diff --git a/src/bot_base/bot_base.py b/src/bot_base/bot_base.py index e69de29..bec9df5 100644 --- a/src/bot_base/bot_base.py +++ b/src/bot_base/bot_base.py @@ -0,0 +1,147 @@ +from __future__ import annotations + +import importlib +import inspect +import logging +import os +import sys + +import discord +import toml +from packaging.specifiers import SpecifierSet + +from config import Config, config_types +from config.config_types import factory +from errors import IncompatibleModuleError + +__version__ = "0.1.0" +MINIMAL_INFOS = ["version", "bot_version"] + + +class BotBase(discord.Client): + log = None + + def __init__(self, data_folder: str = "data", modules_folder: str = "modules", *args, **kwargs): + super().__init__(*args, **kwargs) + # Create folders + os.makedirs(modules_folder, exist_ok=True) + os.makedirs(data_folder, exist_ok=True) + # Add module folder to search path + # TODO: Vérifier que ca ne casse rien + sys.path.insert(0, modules_folder) + # Setup logging + self.log = logging.getLogger('bot_base') + # Content: {"module_name": {"module": imported module, "class": initialized class}} + self.modules = {} + + # Setup config + self.config = Config(path=os.path.join(data_folder, "config.toml")) + self.config.register("modules", factory(config_types.List, factory(config_types.Str))) + self.config.register("prefix", factory(config_types.Str)) + self.config.register("admin_roles", factory(config_types.List, factory(config_types.discord_types.Role, self))) + self.config.register("admin_users", factory(config_types.List, factory(config_types.discord_types.User, self))) + self.config.register("main_guild", factory(config_types.discord_types.Guild, self)) + self.config.register("locale", factory(config_types.Str)) + self.config.register("data_folder", factory(config_types.Str)) + self.config.register("modules_folder", factory(config_types.Str)) + + self.config.set({ + "modules": [], + "prefix": "%", + "admin_roles": [], + "admin_users": [], + "main_guild": None, + "locale": "fr_FR.UTF8", + "data_folder": data_folder, + "modules_folder": modules_folder, + }) + + self.config.load() + self.load_module("test_module") + + async def on_ready(self): + self.info("Bot ready.") + + def load_module(self, module): + # Check if module exists + if not os.path.isdir(os.path.join(self.config["modules_folder"], module)): + raise ModuleNotFoundError(f"Module {module} not found in modules folder ({self.config['modules_folder']}.)") + if not os.path.isfile(os.path.join(self.config["modules_folder"], module, "infos.toml")): + raise IncompatibleModuleError(f"Module {module} is incompatible: no infos.toml found.") + # Check infos.toml integrity + with open(os.path.join(self.config["modules_folder"], module, "infos.toml")) as f: + infos = toml.load(f) + for key in MINIMAL_INFOS: + if key not in infos.keys(): + raise IncompatibleModuleError(f"Missing information for module {module}: missing {key}.") + # Check bot_version + bot_version_specifier = SpecifierSet(infos["bot_version"]) + if __version__ not in bot_version_specifier: + raise IncompatibleModuleError(f"Module {module} is not compatible with your current bot version " + f"(need {infos['bot_version']} and you have {__version__}).") + # Check if module have __main_class__ + imported = importlib.import_module(module) + try: + main_class = imported.__main_class__ + except AttributeError: + raise IncompatibleModuleError(f"Module {module} does not provide __main_class__.") + # Check if __main_class__ is a class + if not inspect.isclass(main_class): + raise IncompatibleModuleError(f"Module {module} contains __main_class__ but it is not a type.") + try: + main_class = main_class(self) + except TypeError: + # Module don't need client reference + main_class = main_class() + # Check if __main_class__ have __dispatch__ attribute + try: + dispatch = main_class.__dispatch__ + except AttributeError: + raise IncompatibleModuleError(f"Module {module} mainclass ({main_class}) does not provide __dispatch__" + f" attribute)") + # Check if __dispatch__ is function + if not inspect.isfunction(dispatch): + raise IncompatibleModuleError(f"Module {module} mainclass ({main_class}) provides __dispatch__, but it is " + f"not a function ({dispatch}).") + # Check if __dispatch__ can have variable positional and keyword aguments (to avoid future error on each event) + sig = inspect.signature(dispatch) + args_present, kwargs_present = False, False + for p in sig.parameters.values(): + if p.kind == p.VAR_POSITIONAL: + args_present = True + elif p.kind == p.VAR_KEYWORD: + kwargs_present = True + if not args_present: + raise IncompatibleModuleError( + f"Module {module} mainclass ({main_class}) provide __dispatch__ function, but " + f"this function doesn't accept variable positionnal arguments.") + if not kwargs_present: + raise IncompatibleModuleError( + f"Module {module} mainclass ({main_class}) provide __dispatch__ function, but " + f"this function doesn't accept variable keywords arguments.") + # Module is compatible! + # Add module to loaded modules + + self.modules.update({ + module: { + "imported": imported, + "initialized_class": main_class, + "dispatch": dispatch, + } + }) + + # Logging + def info(self, *args, **kwargs): + if self.log: + self.log.info(*args, **kwargs) + self.dispatch("on_log_info", *args, **kwargs) + + def error(self, *args, **kwargs): + if self.log: + self.log.error(*args, **kwargs) + self.dispatch("on_log_error", *args, **kwargs) + + def warning(self, *args, **kwargs): + if self.log: + self.log.warning(*args, **kwargs) + self.dispatch("on_log_warning", *args, **kwargs) diff --git a/src/config/__init__.py b/src/config/__init__.py index 1f266ee..43d647a 100644 --- a/src/config/__init__.py +++ b/src/config/__init__.py @@ -1,3 +1,4 @@ -from config.base import Config +from . import config_types +from .base import Config -__all__ = ["Config"] +__all__ = ["Config", "config_types"] diff --git a/src/config/config_types/__init__.py b/src/config/config_types/__init__.py index 309b02a..b4d860c 100644 --- a/src/config/config_types/__init__.py +++ b/src/config/config_types/__init__.py @@ -1,16 +1,16 @@ from typing import Type -import config.config_types.discord_types -from config.config_types.base_type import BaseType -from config.config_types.bool import Bool -from config.config_types.color import Color -from config.config_types.dict import Dict -from config.config_types.float import Float -from config.config_types.int import Int -from config.config_types.list import List -from config.config_types.str import Str +from . import discord_types +from .base_type import BaseType +from .bool import Bool +from .color import Color +from .dict import Dict +from .float import Float +from .int import Int +from .list import List +from .str import Str -__all__ = ['factory', "BaseType", 'Dict', 'Float', 'Int', 'List', 'Str', 'discord_types', 'Bool', 'Color'] +__all__ = ['factory', 'Dict', 'Float', 'Int', 'List', 'Str', 'discord_types', 'Bool', 'Color'] class Meta(type): @@ -32,7 +32,7 @@ def factory(type: Type[BaseType], *args, **kwargs): >>> factory(Int, min=0, max=10) - :param type: Type to create + :param Type[BaseType] type: Type to create :return: New type """ diff --git a/src/config/config_types/bool.py b/src/config/config_types/bool.py index 8cd1d79..670bcf0 100644 --- a/src/config/config_types/bool.py +++ b/src/config/config_types/bool.py @@ -1,6 +1,6 @@ import typing -from config.config_types.base_type import BaseType +from .base_type import BaseType class Bool(BaseType): diff --git a/src/config/config_types/color.py b/src/config/config_types/color.py index c7392af..c107c67 100644 --- a/src/config/config_types/color.py +++ b/src/config/config_types/color.py @@ -1,6 +1,6 @@ import typing -from config.config_types.base_type import BaseType +from .base_type import BaseType class Color(BaseType): diff --git a/src/config/config_types/dict.py b/src/config/config_types/dict.py index d9e84d3..37fc1db 100644 --- a/src/config/config_types/dict.py +++ b/src/config/config_types/dict.py @@ -1,6 +1,6 @@ import typing -from config.config_types.base_type import BaseType +from .base_type import BaseType class Dict(BaseType): diff --git a/src/config/config_types/discord_types/__init__.py b/src/config/config_types/discord_types/__init__.py index 0f66960..d1a2ac0 100644 --- a/src/config/config_types/discord_types/__init__.py +++ b/src/config/config_types/discord_types/__init__.py @@ -1,6 +1,6 @@ from config.config_types.discord_types.channel import Channel from config.config_types.discord_types.guild import Guild -from config.config_types.discord_types.user import User from config.config_types.discord_types.role import Role +from config.config_types.discord_types.user import User __all__ = ['Channel', "Guild", "User", "Role"] \ No newline at end of file diff --git a/src/config/config_types/discord_types/guild.py b/src/config/config_types/discord_types/guild.py index 3e65214..e38c654 100644 --- a/src/config/config_types/discord_types/guild.py +++ b/src/config/config_types/discord_types/guild.py @@ -6,22 +6,23 @@ import discord from config.config_types.base_type import BaseType -LBI = typing.TypeVar('LBI') +if typing.TYPE_CHECKING: + from bot_base import BotBase class Guild(BaseType): - #: :class:`LBI`: Client instance for checking - client: LBI + #: :class:`BotBase`: Client instance for checking + client: BotBase #: :class:`typing.Optional` [:class:`int`]: Current guild id value: typing.Optional[int] #: :class:`typing.Optional` [:class:`discord.Guild`]: Current guild instance guild_instance: typing.Optional[discord.Guild] - def __init__(self, client: LBI) -> None: + def __init__(self, client: BotBase) -> None: """ Base Guild type for config. - :param LBI client: Client instance + :param BotBase client: Client instance :Basic usage: @@ -55,7 +56,7 @@ class Guild(BaseType): if isinstance(value, discord.Guild): id = value.id if not self.client.is_ready(): - self.client.warn("No check for guild `value` because client is not initialized!") + self.client.warning("No check for guild `value` because client is not initialized!") return True if self.client.get_guild(id): return True diff --git a/src/config/config_types/float.py b/src/config/config_types/float.py index 325a655..e4ecb75 100644 --- a/src/config/config_types/float.py +++ b/src/config/config_types/float.py @@ -1,6 +1,6 @@ import typing -from config.config_types.base_type import BaseType +from .base_type import BaseType class Float(BaseType): diff --git a/src/config/config_types/int.py b/src/config/config_types/int.py index 2a5a64a..58d2dab 100644 --- a/src/config/config_types/int.py +++ b/src/config/config_types/int.py @@ -1,6 +1,6 @@ import typing -from config.config_types.base_type import BaseType +from .base_type import BaseType class Int(BaseType): diff --git a/src/config/config_types/list.py b/src/config/config_types/list.py index 70fa392..d2142d2 100644 --- a/src/config/config_types/list.py +++ b/src/config/config_types/list.py @@ -1,6 +1,6 @@ import typing -from config.config_types.base_type import BaseType +from .base_type import BaseType class List(BaseType): diff --git a/src/config/config_types/str.py b/src/config/config_types/str.py index be699da..50e835b 100644 --- a/src/config/config_types/str.py +++ b/src/config/config_types/str.py @@ -1,4 +1,4 @@ -from config.config_types.base_type import BaseType +from .base_type import BaseType class Str(BaseType): diff --git a/src/errors.py b/src/errors.py index 4684fb1..ccbef48 100644 --- a/src/errors.py +++ b/src/errors.py @@ -1,28 +1,14 @@ -class LBIException(Exception): - """ - Base exception class for LBI - - All other exceptions are subclasses - """ +class BotBaseException(Exception): pass -class ModuleException(LBIException): - """ - Base exception class for all module errors - """ +class ModuleException(BotBaseException): pass -class ModuleNotInstalled(ModuleException): - """ - Raised when a module is not found in module directory - """ +class ModuleNotFound(ModuleException): pass -class IncompatibleModule(ModuleException): - """ - Raised when a module is not compatible with bot version - """ +class IncompatibleModuleError(ModuleException): pass diff --git a/src/main.py b/src/main.py index 8d9ee44..47ddb8a 100644 --- a/src/main.py +++ b/src/main.py @@ -1,173 +1,13 @@ #!/usr/bin/python3 -from __future__ import annotations - import asyncio -import importlib import json -import locale import logging -import logging.config import os -import sys -import traceback -from typing import Dict -import discord -import humanize -from packaging.version import Version - -from config import Config, config_types -from config.config_types import factory -from errors import IncompatibleModule -from modules.base import base_supported_type - -__version__ = "0.1.0" +from bot_base.bot_base import BotBase -class Module: - name: str - - def __init__(self, name: str): - """ - Init module - - :param name: Module name - :type name: str - """ - 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: - """ - Check if module exists - - :return: True if module is present in modules folders - :rtype: bool - """ - if not os.path.isdir(os.path.join("modules", self.name)): - return False - return True - - @property - def complete(self) -> bool: - """ - Check if module is complete - - :return: True if module is compatible - :rtype: Boolean - """ - # Check if version.json exists - if not os.path.exists(os.path.join("modules", self.name, "version.json")): - return False - with open(os.path.join("modules", self.name, "version.json")) as file: - versions = json.load(file) - if "version" not in versions.keys(): - return False - if "dependencies" not in versions.keys(): - return False - if "bot_version" not in versions.keys(): - return False - if "type" not in versions.keys(): - return False - if versions["type"] not in base_supported_type: - return False - return True - - @property - def version(self) -> Version: - """ - Returns module version - - :return: current module version - :rtype: Version - """ - with open(os.path.join("modules", self.name, "version.json")) as file: - versions = json.load(file) - return Version(versions["version"]) - - @property - def bot_version(self) -> dict: - """ - returns the min and max version of the bot that is compatible with the module - - :return: Min and max version for bot - :rtype: dict - :raises IncompatibleModule: If bot_version is not properly formated (there must be min and max keys) - """ - with open(os.path.join("modules", self.name, "version.json")) as file: - versions = json.load(file) - try: - return {"min": Version(versions["bot_version"]["min"]), - "max": Version(versions["bot_version"]["max"])} - except KeyError: - raise IncompatibleModule(f"Module {self.name} is not compatible with bot (version.json does not " - f"contain bot_version.max or bot_version.min item)") - - @property - def dependencies(self) -> dict: - """ - return list of dependencies version - - :raise IncompatibleModule: If bot_version is not properly formated (there must be min and max keys for each dependencies) - :return: list of dependencies version - :rtype: dict - """ - with open(os.path.join("modules", self.name, "version.json")) as file: - versions = json.load(file) - try: - deps = {} - for name, dep in versions["dependencies"].items(): - dep_ver = {"min": Version(dep["min"]), - "max": Version(dep["max"])} - deps.update({name: dep_ver}) - return deps - except KeyError: - raise IncompatibleModule(f"Module {self.name} is not compatible with bot (version.json does not " - f"contain dependencies.modulename.max or dependencies.modulename.min item)") - - @property - def compatible(self) -> bool: - """ - Check if module is compatible with current installation - - :return: True if all dependencies are okays - :rtype: bool - """ - # Check bot version - bot_ver = Version(__version__) - if bot_ver < self.bot_version["min"]: - return False - if bot_ver > self.bot_version["max"]: - return False - for name, dep in self.dependencies.items(): - if name not in MODULES.keys(): - Module(name) - if MODULES[name].version < dep["min"]: - return False - if MODULES[name].version > dep["max"]: - return False - return True - - -MODULES: Dict[str, Module] = {} - - -def setup_logging(default_path='config/log_config.json', default_level=logging.INFO, env_key='LBI_LOG_CONFIG'): +def setup_logging(default_path='data/log_config.json', default_level=logging.INFO, env_key='LBI_LOG_CONFIG'): """Setup logging configuration """ path = default_path @@ -182,318 +22,14 @@ def setup_logging(default_path='config/log_config.json', default_level=logging.I logging.basicConfig(level=default_level) -def modules_edit(func): - def wrapper(self, *args, **kwargs): - if self.reloading: - return func(self, *args, **kwargs) - else: - self.reloading = True - a = func(self, *args, **kwargs) - self.reloading = False - return a - - return wrapper - - -def event(func): - def wrapper(self, *args, **kwargs): - if self.reloading: - return lambda: None - else: - return func(self, *args, **kwargs) - - return wrapper - - -"""def async_event(func): - async def wrapper(self, *args, **kwargs): - if self.reloading: - return lambda: None - else: - return func(self, *args, **kwargs) - return wrapper""" - setup_logging() -log_discord = logging.getLogger('discord_types') -log_LBI = logging.getLogger('LBI') -log_communication = logging.getLogger('communication') - - -def load_modules_info(): - for mod in os.listdir("modules"): - Module(mod) - - -class LBI(discord.Client): - by_id: ClientById - base_path = "data" - debug = log_LBI.debug - info = log_LBI.info - warning = log_LBI.warning - warn = warning - error = log_LBI.error - critical = log_LBI.critical - - def __init__(self, config: Config = None, *args, **kwargs): - super().__init__(*args, **kwargs) - if config is None: - config = Config(path="data/config.toml", client=self) - self.reloading = False - self.by_id = ClientById(self) - self.ready = False - # Content: {"module_name": {"module": imported module, "class": initialized class}} - self.modules = {} - - self.config = config - self.config.register("modules", factory(config_types.List, factory(config_types.Str))) - self.config.register("prefix", factory(config_types.Str)) - self.config.register("admin_roles", factory(config_types.List, factory(config_types.discord_types.Role, self))) - self.config.register("admin_users", factory(config_types.List, factory(config_types.discord_types.User, self))) - self.config.register("main_guild", factory(config_types.discord_types.Guild, self)) - self.config.register("locale", factory(config_types.Str)) - - self.config.set({ - "modules": ["modules", "errors"], - "prefix": "%", - "admin_roles": [], - "admin_users": [], - "main_guild": None, - "locale": "fr_FR.UTF8", - }) - - locale.setlocale(locale.LC_TIME, self.config['locale']) - humanize.i18n.activate(self.config['locale']) - self.load_modules() - - @modules_edit - def load_modules(self): - self.info("Starts to load modules...") - e = {} - for module in self.config["modules"]: - e.update({module: self.load_module(module)}) - self.info("Finished to load all modules.") - return e - - @modules_edit - def load_module(self, module): - """ - - Status codes: - - 0: Module loaded - - 1: Module not in modules folder - - 2: Module incomplete - - 3: Module incompatible - - :param module: Module name - :return: Status code - """ - - # Check module compatibility - load_modules_info() - if not MODULES.get(module): - return 1 - if not MODULES[module].exists: - return 1 - if not MODULES[module].complete: - return 2 - if not MODULES[module].compatible: - return 3 - deps = MODULES[module].dependencies - for dep in deps.keys(): - if dep not in self.modules.keys(): - if dep != "base": - self.load_module(dep) - 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.config.save() - except AttributeError as e: - self.error("Module {module} doesn't have MainClass.".format(module=module)) - raise 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.BaseClassLua(self, path=f"modules/{module}/main") - self.modules.update({module: {"imported": imported, "initialized_class": initialized_class}}) - self.info(f"Module {module} successfully imported.") - initialized_class.dispatch("load") - if module not in self.config["modules"]: - self.config["modules"].append(module) - self.config.save() - return 0 - - @modules_edit - def unload_module(self, module): - self.info("Start unload module {module}...".format(module=module)) - try: - if module in self.config["modules"]: - self.config["modules"].remove(module) - self.config.save() - self.unload_all() - self.load_modules() - except KeyError as e: - self.error("Module {module} not loaded.").format(module=module) - return e - - @modules_edit - def reload(self): - del self.modules - self.load_modules() - - @modules_edit - def unload_all(self): - del self.modules - self.modules = {} - - @event - def dispatch(self, event, *args, **kwargs): - # Dispatch to handle wait_* commands - super().dispatch(event, *args, **kwargs) - # Dispatch to modules - for module in self.modules.values(): - module["initialized_class"].dispatch(event, *args, **kwargs) - - async def on_error(self, event_method, *args, **kwargs): - """Function called when error happend""" - # This event is special because it is call directly - self.error(traceback.format_exc()) - for module in self.modules.values(): - await module["initialized_class"].on_error(event_method, *args, **kwargs) - - -class ClientById: - client: LBI - - def __init__(self, client_): - self.client = client_ - - async def fetch_message(self, id_, *args, **kwargs): - """Find a message by id - - :param id_: Id of message to find - :type id_: int - - :raises discord_types.NotFound: This exception is raised when a message is not found (or not accessible by bot) - - :rtype: discord.Message - :return: discord_types.Message instance if message is found. - """ - msg = None - for channel in self.client.get_all_channels(): - try: - return await channel.fetch_message(id_, *args, **kwargs) - except discord.NotFound: - continue - if msg is None: - raise discord.NotFound(None, "Message not found") - - async def edit_message(self, id, *args, **kwargs): - """ - Edit message by id - - :param id: Id of the message to edit - :type id: int""" - message = await self.fetch_message(id) - return await message.edit(**kwargs) - - async def remove_reaction(self, id_message, *args, **kwargs): - """Remove reaction from message by id - - :param id_message: Id of message - :type id_message: int""" - message = await self.fetch_message(id_message) - return await message.remove_reaction(*args, **kwargs) - - async def send_message(self, id_, *args, **kwargs): - """Send message by channel id - - :param id_: Id of channel where to send message - :type id_: int""" - channel = self.client.get_channel(id_) - return channel.send(*args, **kwargs) - - def get_role(self, id_=None, name=None, check=None, guilds=None): - """Get role by id or with custom check""" - if guilds is None: - guilds = self.client.guilds - if id_ is not None: - for guild in guilds: - role = discord.utils.get(guild.roles, id=id_) - if role: - return role - if name is not None: - for guild in guilds: - role = discord.utils.get(guild.roles, name=name) - if role: - return role - if check is not None: - role = None - for guild in guilds: - for role_ in guild.roles: - if check(role_): - role = role_ - break - if role is not None: - break - return role - return None - - - - -class Communication(asyncio.Protocol): - debug = log_communication.debug - info = log_communication.info - warning = log_communication.warning - error = log_communication.error - critical = log_communication.critical - name = "Communication" - - def __init__(self, client): - self.client = client - self.transport = None - - def connection_made(self, transport): - print('%s: connection made' % self.name) - self.transport = transport - - def data_received(self, data): - print('%s: data received: %r' % (self.name, data)) - - def eof_received(self): - pass - - def connection_lost(self, exc): - print('%s: connection lost: %s' % (self.name, exc)) - if __name__ == "__main__": - client1 = LBI(max_messages=500000) - communication = Communication(client1) - + client = BotBase(max_messages=500000) async def start_bot(): - await client1.start(os.environ.get("DISCORD_TOKEN")) - - - print(os.path.join("/tmp", os.path.dirname(os.path.realpath(__file__))) + ".sock") + await client.start(os.environ.get("DISCORD_TOKEN")) loop = asyncio.get_event_loop() - t = loop.create_unix_server(Communication, - path=os.path.join("/tmp", os.path.dirname(os.path.realpath(__file__)) + ".sock")) - if not sys.platform == "win32": - loop.run_until_complete(t) - loop.create_task(start_bot()) loop.run_forever() diff --git a/src/modules/avalon/__init__.py b/src/modules/avalon/__init__.py deleted file mode 100644 index fe433a3..0000000 --- a/src/modules/avalon/__init__.py +++ /dev/null @@ -1,56 +0,0 @@ -import datetime - -import discord - -import utils.emojis -from modules.base import BaseClassPython - - -class MainClass(BaseClassPython): - name = "Avalon" - help = { - "description": "Maître du jeu Avalon.", - "commands": { - "`{prefix}{command} join`": "", - "`{prefix}{command} quit`": "", - "`{prefix}{command} players list`": "", - "`{prefix}{command} players kick (/<@mention>)`": "", - "`{prefix}{command} roles setup`": "", - "`{prefix}{command} roles list`": "", - } - } - help_active = True - command_text = "perdu" - color = 0xff6ba6 - - def __init__(self, client): - super().__init__(client) - self.config.set({"spectate_channel": 0, - "illustrations":{"merlin":"", - "perceval":"", - "gentil":"", - "assassin":"", - "mordred":"", - "morgane":"", - "oberon":"", - "mechant":""}, - "couleurs":{"merlin":"", - "perceval":0, - "gentil":0, - "assassin":0, - "mordred":0, - "morgane":0, - "oberon":0, - "mechant":0, - "test":15}, - "test":{"merlin":"", - "perceval":0, - "gentil":0, - "assassin":0, - "mordred":0, - "morgane":0, - "oberon":0, - "mechant":0, - "test":15} - }) - diff --git a/src/modules/avalon/version.json b/src/modules/avalon/version.json deleted file mode 100644 index 53fa16b..0000000 --- a/src/modules/avalon/version.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "version":"0.1.0", - "type": "python", - "dependencies": { - - }, - "bot_version": { - "min": "0.1.0", - "max": "0.1.0" - } -} \ No newline at end of file diff --git a/src/modules/base/__init__.py b/src/modules/base/__init__.py deleted file mode 100644 index 2e94b77..0000000 --- a/src/modules/base/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .BasePython import BaseClassPython -from .BaseLua import BaseClassLua -base_supported_type = ["python", "lua"] \ No newline at end of file diff --git a/src/modules/base/base.py b/src/modules/base/base.py deleted file mode 100644 index d419638..0000000 --- a/src/modules/base/base.py +++ /dev/null @@ -1,276 +0,0 @@ -"""Base class for module, never use directly !!!""" -import asyncio -import os -from typing import List, Union, Optional - -import discord - -from config import Config -from config import config_types -from config.config_types import factory -from storage import Objects -from utils import emojis - - -class BaseClass: - """Base class for all modules, Override it to make submodules""" - name = "" - help = { - "description": "", - "commands": { - - } - } - - 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: LBI""" - self.client = client - self.objects = Objects(path=os.path.join("data", self.name.lower())) - self.config = Config(path=os.path.join("data", self.name.lower(), "config.toml")) - self.config.register("help_active", factory(config_types.Bool)) - self.config.register("color", factory(config_types.Color)) - self.config.register("auth_everyone", factory(config_types.Bool)) - self.config.register("authorized_roles", - factory(config_types.List, factory(config.config_types.discord_types.Role, client))) - self.config.register("authorized_users", - factory(config_types.List, factory(config.config_types.discord_types.User, client))) - self.config.register("command_text", factory(config_types.Str)) - self.config.set({"help_active": True, - "color": 0x000000, - "auth_everyone": False, - "authorized_roles": [], - "authorized_users": [], - "command_text": self.name.lower()}) - self.config.load() - - 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.config["color"] - ) - for command, description in self.help["commands"].items(): - embed.add_field( - name=command.format(prefix=self.client.config['prefix'], command=self.config["command_text"]), - value="-> " + description.format(prefix=self.client.config['prefix'], - command=self.config["command_text"]), - inline=False) - await channel.send(embed=embed) - - def auth(self, user: discord.User, role_list: List[int] = None, user_list: List[int] = None, - guild: int = None): - """ - Return True if user is an owner of the bot or in authorized_users or he have a role in authorized_roles. - - :param user: User to check - :param user_list: List of authorized users, if not specified use self.authorized_users - :param role_list: list of authorized roles, if not specified use self.authorized_roles - :param guild: Specific guild to search role - :type user_list: List[Int] - :type role_list: List[Int] - :type guild: Int - :type user: discord.User - """ - if self.config["auth_everyone"]: - return True - if user_list is None: - user_list = self.config["authorized_users"] + self.client.config['admin_users'] - if user.id in user_list: - return True - if role_list is None: - role_list = self.config["authorized_roles"] + self.client.config['admin_roles'] - if guild is None: - guilds = self.client.guilds - else: - guilds = [guild] - for guild in 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 - return False - - async def parse_command(self, message): - """Parse a command_text from received message and execute function - Parse message like `{prefix}{command_text} subcommand` and call class method `com_{subcommand}`. - - :param message: message to parse - :type message: discord.Message""" - command = self.client.config["prefix"] + (self.config["command_text"] if self.config["command_text"] else "") - if message.content.startswith(command): - content = message.content.split(" ", 1)[1 if " " in message.content else 0] - sub_command, args, kwargs = self._parse_command_content(content) - sub_command = "com_" + sub_command - if self.auth(message.author): - if sub_command in dir(self): - await self.__getattribute__(sub_command)(message, args, kwargs) - else: - await self.command(message, args, kwargs) - else: - await self.unauthorized(message) - - @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: tuple[str, list, list]""" - if not len(content.split()): - return "", [], [] - # Sub_command - sub_command = content.split()[0] - args_ = [sub_command] - kwargs = [] - if len(content.split()) > 1: - # Remove subcommand - content = content.lstrip(sub_command) - # Take the other part of command_text - content = content.lstrip().replace("\"", "\"\"") - # Splitting around quotes - quotes = [element.split("\" ") for element in content.split(" \"")] - # Split all sub chains but raw 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: discord.Message): - """Override this function to deactivate command_text parsing""" - if message.author.bot: - return - 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""" - await self.send_help(message.channel) - - async def com_help(self, message, args, kwargs): - await self.send_help(message.channel) - - async def unauthorized(self, message): - await message.channel.send("Vous n'êtes pas autorisé à effectuer cette commande") - - 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.client._run_event(coro, method, *args, **kwargs), loop=self.client.loop) - - async def on_error(self, event_method, *args, **kwargs): - pass - - async def choice(self, message: discord.Message, choices: List[Union[discord.Emoji, discord.PartialEmoji, str]], - validation: bool = False, - validation_emote: Union[discord.Emoji, discord.PartialEmoji, str] = emojis.WHITE_CHECK_MARK, - minimal_choices: int = 1, - maximal_choices: Optional[int] = None, - timeout: Optional[float] = None, - user: Optional[discord.User] = None, - unique: bool = False): - final_choices: List[Union[discord.Emoji, discord.PartialEmoji, str]] = [] - validation_step = False - for emoji in choices: - await message.add_reaction(emoji) - - def check_add(reaction, u): - nonlocal validation_step, final_choices - if (not user.bot) and (user is None or u.id == user.id): - if validation_step and reaction.emoji == validation_emote: - return True - if reaction in choices: - if not unique or reaction.emoji not in final_choices: - final_choices.append(reaction.emoji) - if maximal_choices is not None and len(final_choices) > maximal_choices: - validation_step = False - asyncio.ensure_future(message.remove_reaction(validation_emote, self.client.user)) - try: - asyncio.get_running_loop().run_until_complete(message.clear_reaction(validation_emote)) - except discord.errors.Forbidden: - pass - return False - if len(final_choices) >= minimal_choices: - if validation: - asyncio.get_running_loop().run_until_complete(message.add_reaction(validation_emote)) - validation_step = True - return False - else: - return True - return False - - def check_remove(reaction: discord.Reaction, u): - nonlocal validation_step, final_choices - if (not user.bot) and (user is None or u.id == user.id): - if reaction.emoji in choices: - if not unique or reaction.count != 0: - final_choices.remove(reaction.emoji) - if len(final_choices) < minimal_choices: - if validation_step: - asyncio.ensure_future(message.remove_reaction(validation_emote, self.client.user)) - try: - asyncio.get_running_loop().run_until_complete(message.clear_reaction(validation_emote)) - except discord.errors.Forbidden: - pass - validation_step = False - return False - if (maximal_choices is None or len(final_choices) <= maximal_choices) and len( - final_choices) >= minimal_choices: - if validation: - asyncio.get_running_loop().run_until_complete(message.add_reaction(validation_emote)) - validation_step = True - return False - else: - return True - return False - - done, pending = await asyncio.wait([ - self.client.wait_for('reaction_add', timeout=timeout, check=check_add), - self.client.wait_for('reaction_remove', timeout=timeout, check=check_remove)], - return_when=asyncio.FIRST_COMPLETED) - return final_choices diff --git a/src/modules/base/base_lua.py b/src/modules/base/base_lua.py deleted file mode 100644 index c5ce17d..0000000 --- a/src/modules/base/base_lua.py +++ /dev/null @@ -1,73 +0,0 @@ -"""Base class for module, never use directly !!!""" -import asyncio - -import discord -import lupa - -from modules import BaseClass - - -class BaseClassLua(BaseClass): - """Base class for all modules, Override it to make submodules""" - name = "" - help = { - "description": "", - "commands": { - - } - } - help_active = False - color = 0x000000 - command_text = None - authorized_users = [] - authorized_roles = [] - command_text = "" - - 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""" - super().__init__(client) - # Get lua globals - self.lua = lupa.LuaRuntime(unpack_returned_tuples=True) - self.luaMethods = self.lua.require(path) - - def call(self, method, *args, **kwargs): - # Try to run lua method then python one - if self.luaMethods[method] is not None: - async def coro(*args, **kwargs): - self.luaMethods[method](self, asyncio.ensure_future, discord, *args, *kwargs) - asyncio.ensure_future(self.client._run_event(coro, method, *args, **kwargs), loop=self.client.loop) - try: - coro = getattr(self, method) - except AttributeError: - pass - else: - asyncio.ensure_future(self.client._run_event(coro, method, *args, **kwargs), loop=self.client.loop) - - def dispatch(self, event, *args, **kwargs): - method = "on_"+event - self.call(method, *args, **kwargs) - - 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 self.auth(message.author): - self.call(sub_command, args, kwargs) - else: - await self.unauthorized(message) - diff --git a/src/modules/base/base_python.py b/src/modules/base/base_python.py deleted file mode 100644 index 5cb7d7f..0000000 --- a/src/modules/base/base_python.py +++ /dev/null @@ -1,8 +0,0 @@ -"""Base class for module, never use directly !!!""" - -from .Base import BaseClass - - -class BaseClassPython(BaseClass): - """Base class for all modules, Override it to make submodules""" - pass diff --git a/src/modules/base/version.json b/src/modules/base/version.json deleted file mode 100644 index 53fa16b..0000000 --- a/src/modules/base/version.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "version":"0.1.0", - "type": "python", - "dependencies": { - - }, - "bot_version": { - "min": "0.1.0", - "max": "0.1.0" - } -} \ No newline at end of file diff --git a/src/modules/clean/__init__.py b/src/modules/clean/__init__.py deleted file mode 100644 index fbe208a..0000000 --- a/src/modules/clean/__init__.py +++ /dev/null @@ -1,17 +0,0 @@ -from modules.base import BaseClassPython - -class MainClass(BaseClassPython): - name = "clean" - help = { - "description": "Supprime des messages", - "commands": { - "`{prefix}{command}`": "Supprime tous les messages du bot dans le salon" - } - } - - async def command(self, message, args, kwargs): - def is_me(m): - return m.author == self.client.user - - deleted = await message.channel.purge(limit=10000000, check=is_me) - await message.channel.send('Deleted {} message(s)'.format(len(deleted))) diff --git a/src/modules/clean/version.json b/src/modules/clean/version.json deleted file mode 100644 index 53fa16b..0000000 --- a/src/modules/clean/version.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "version":"0.1.0", - "type": "python", - "dependencies": { - - }, - "bot_version": { - "min": "0.1.0", - "max": "0.1.0" - } -} \ No newline at end of file diff --git a/src/modules/errors/__init__.py b/src/modules/errors/__init__.py deleted file mode 100644 index 751bec3..0000000 --- a/src/modules/errors/__init__.py +++ /dev/null @@ -1,120 +0,0 @@ -import asyncio -import random -import traceback - -import discord -from discord import Message - -from config import config_types -from config.config_types import factory -from modules.base import BaseClassPython - - -class MainClass(BaseClassPython): - name = "errors" - authorized_users = [] - authorized_roles = [] - help = { - "description": "Montre toutes les erreurs du bot dans discord_types.", - "commands": { - "`{prefix}{command}`": "Renvoie une erreur de test.", - } - } - - def __init__(self, client): - super().__init__(client) - self.config.register("dev_chan", - factory(config_types.List, factory(config_types.discord_types.Channel, client))) - self.config.register("memes", factory(config_types.List, factory(config_types.Str))) - self.config.register("icon", factory(config_types.Str)) - self.config.set({"dev_chan": [], "memes": [""], "icon": ""}) - self.errorsList = None - - async def on_load(self): - if self.objects.save_exists('errorsList'): - self.errorsList = self.objects.load_object('errorsList') - else: - self.errorsList = [] - - async def on_ready(self): - for i in range(len(self.errorsList)): - try: - msg_id = self.errorsList.pop(0) - channel = self.client.get_channel(msg_id["channel_id"]) - to_delete = await channel.fetch_message(msg_id["msg_id"]) - await to_delete.delete() - except: - raise - self.objects.save_object('errorsList', self.errorsList) - - async def command(self, message, args, kwargs): - raise Exception("KERNEL PANIC!!!") - - async def on_error(self, event, *args, **kwargs): - """Send error message""" - # Search first channel instance found in arg, then search in kwargs - channel = None - for arg in args: - if type(arg) == Message: - channel = arg.channel - break - if type(arg) == discord.TextChannel: - channel = arg - break - if channel is None: - for _, v in kwargs.items(): - if type(v) == discord.Message: - channel = v.channel - break - if type(v) == discord.TextChannel: - channel = v - break # Create embed - embed = discord.Embed( - title="[Erreur] Aïe :/", - description="```python\n{0}```".format(traceback.format_exc()), - color=self.config["color"]) - embed.set_image(url=random.choice(self.config["memes"])) - message_list = None - - # Send message to dev channels - for chanid in self.config["dev_chan"]: - try: - await self.client.get_channel(chanid).send( - embed=embed.set_footer(text="Ce message ne s'autodétruira pas.", icon_url=self.config["icon"])) - except BaseException as e: - raise e - # Send message to current channel if exists - if channel is not None: - message = await channel.send(embed=embed.set_footer(text="Ce message va s'autodétruire dans une minute", - icon_url=self.config["icon"])) - msg_id = {"channel_id": message.channel.id, "msg_id": message.id} - self.errorsList.append(msg_id) - # Save message in errorsList now to keep them if a reboot happend during next 60 seconds - self.objects.save_object('errorsList', self.errorsList) - - # Wait 60 seconds and delete message - # await asyncio.sleep(60) - try: - # channel = self.client.get_channel(msg_id["channel_id"]) - # delete_message = await channel.fetch_message(msg_id["msg_id"]) - # await delete_message.delete() - await message.add_reaction("🗑️") - - try: - reaction, user = await self.client.wait_for('reaction_add', - timeout=60.0, - check=lambda r, u: - r.emoji == "🗑️" and not u.bot and self.auth(u)) - except asyncio.TimeoutError: - await message.delete() - else: - await reaction.message.delete() - except: - raise - finally: - try: - self.errorsList.remove(msg_id) - except ValueError: - pass - # Save now to avoid deleting unkown message - self.objects.save_object('errorsList', self.errorsList) diff --git a/src/modules/errors/version.json b/src/modules/errors/version.json deleted file mode 100644 index 85a57f9..0000000 --- a/src/modules/errors/version.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "version": "0.1.0", - "type": "python", - "dependencies": { - "base": { - "min": "0.1.0", - "max": "0.1.0" - } - }, - "bot_version": { - "min": "0.1.0", - "max": "0.1.0" - } -} \ No newline at end of file diff --git a/src/modules/help/__init__.py b/src/modules/help/__init__.py deleted file mode 100644 index f15f014..0000000 --- a/src/modules/help/__init__.py +++ /dev/null @@ -1,35 +0,0 @@ -import discord - -from modules.base import BaseClassPython - - -class MainClass(BaseClassPython): - name = "Aide" - help = { - "description": "Module d'aide", - "commands": { - "`{prefix}{command} list`": "Affiche une liste des modules ainsi qu'une desription", - "`{prefix}{command} `": "Affiche l'aide sépcifique d'un module"# , - # "`{prefix}{command} all`": "Affiche l'aide de tous les modules" - } - } - - async def com_list(self, message, args, kwargs): - embed = discord.Embed(title="[Aide] - Liste des modules", color=self.config.color) - for moduleName in list(self.client.modules.keys()): - if self.client.modules[moduleName]["initialized_class"].config.help_active: - embed.add_field( - name=moduleName.capitalize(), - value=self.client.modules[moduleName]["initialized_class"].help["description"]) - await message.channel.send(embed=embed) - - # async def com_all(self, message, args, kwargs): - # for name, module in self.client.modules.items(): - # await module["initialized_class"].send_help(message.channel) - - async def command(self, message, args, kwargs): - if len(args) and args[0] in self.client.modules.keys() and self.client.modules[args[0]][ - "initialized_class"].config.help_active: - await self.client.modules[args[0]]["initialized_class"].send_help(message.channel) - else : - await self.send_help(message.channel) \ No newline at end of file diff --git a/src/modules/help/version.json b/src/modules/help/version.json deleted file mode 100644 index 53fa16b..0000000 --- a/src/modules/help/version.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "version":"0.1.0", - "type": "python", - "dependencies": { - - }, - "bot_version": { - "min": "0.1.0", - "max": "0.1.0" - } -} \ No newline at end of file diff --git a/src/modules/modules/__init__.py b/src/modules/modules/__init__.py deleted file mode 100644 index e5b646f..0000000 --- a/src/modules/modules/__init__.py +++ /dev/null @@ -1,141 +0,0 @@ -import os - -import discord -from aiohttp import ClientConnectorError - -from modules.base import BaseClassPython -from modules.modules.api import Api - - -class MainClass(BaseClassPython): - name = "modules" - help = { - "description": "Manage bot modules.", - "commands": { - "`{prefix}{command} list`": "List of available modules.", - "`{prefix}{command} enable `": "Enable module ``.", - "`{prefix}{command} disable `": "Disable module ``.", - "`{prefix}{command} reload `": "Reload module ``", - "`{prefix}{command} web_list`": "List all available modules from repository", - # "`{prefix}{command} web_source`": "List all source repositories", - # "`{prefix}{command} web_source remove `": "Remove url from repository list", - # "`{prefix}{command} web_source add `": "Add url to repository list", - } - } - - def __init__(self, client): - super().__init__(client) - os.makedirs("modules", exist_ok=True) - self.api = Api() - - @staticmethod - def get_all_modules(): - all_items = os.listdir("modules") - modules = [] - for item in all_items: - if item not in ["__init__.py", "base", "__pycache__"]: - if os.path.isfile(os.path.join("modules", item)): - modules.append(item[:-3]) - else: - modules.append(item) - return set(modules) - - async def com_enable(self, message, args, kwargs): - args = args[1:] - if len(args) == 0: - await message.channel.send("You must specify at least one module.") - return - if len(args) == 1 and args[0] == "*": - for module in self.get_all_modules(): - e = self.client.load_module(module) - if e: - await message.channel.send("An error occurred during the loading of the module {module}." - .format(module=module)) - await self.com_list(message, args, kwargs) - return - for arg in args: - e = self.client.load_module(arg) - if e == 1: - await message.channel.send(f"Module {arg} not exists.") - if e == 2: - await message.channel.send(f"Module {arg} is incompatible.") - elif e: - await message.channel.send(f"An error occurred during the loading of the module {arg}: {e}.") - await self.com_list(message, args, kwargs) - - async def com_reload(self, message, args, kwargs): - args = args[1:] - if len(args) == 0: - await message.channel.send("You must specify at least one module.") - return - if len(args) == 1 and args[0] == "*": - for module in self.get_all_modules(): - e = self.client.unload_module(module) - if e: - await message.channel.send(f"An error occurred during the unloading of the module {module}.") - e = self.client.load_module(module) - if e: - await message.channel.send(f"An error occurred during the loading of the module {module}.") - await self.com_list(message, args, kwargs) - return - for arg in args: - e = self.client.unload_module(arg) - if e: - await message.channel.send(f"An error occurred during the unloading of the module {arg}.") - e = self.client.load_module(arg) - if e: - await message.channel.send(f"An error occurred during the loading of the module {arg}.") - await self.com_list(message, [], []) - - async def com_disable(self, message, args, kwargs): - args = args[1:] - if len(args) == 0: - await message.channel.send("You must specify at least one module.") - return - if len(args) == 1 and args[0] == "*": - for module in self.get_all_modules(): - e = self.client.unload_module(module) - if e: - await message.channel.send(f"An error occurred during the loading of the module {module}.") - await self.com_list(message, args, kwargs) - return - for arg in args: - e = self.client.unload_module(arg) - if e: - await message.channel.send(f"An error occurred during the loading of the module {arg}: {e}.") - await self.com_list(message, [], []) - - async def com_list(self, message, args, kwargs): - list_files = self.get_all_modules() - activated = set(self.client.config["modules"]) - if len(activated): - activated_string = "\n+ " + "\n+ ".join(activated) - else: - activated_string = "" - if len(activated) != len(list_files): - deactivated_string = "\n- " + "\n- ".join(list_files.difference(activated)) - else: - deactivated_string = "" - embed = discord.Embed(title="[Modules] - Liste des modules", - description="```diff{activated}{deactivated}```".format( - activated=activated_string, - deactivated=deactivated_string) - ) - await message.channel.send(embed=embed) - - async def com_web_list(self, message, args, kwargs): - try: - modules = await self.api.list() - except ClientConnectorError: - await message.channel.send("Connection impossible au serveur.") - return - text = "" - for module, versions in modules.items(): - text += module + " - " + ", ".join(versions) - await message.channel.send(text) - - async def com_web_dl(self, message, args, kwargs): - try: - await self.api.download(args[1], args[2]) - except ClientConnectorError: - await message.channel.send("Connection impossible au serveur.") diff --git a/src/modules/modules/api.py b/src/modules/modules/api.py deleted file mode 100644 index ef087ed..0000000 --- a/src/modules/modules/api.py +++ /dev/null @@ -1,40 +0,0 @@ -import os -import shutil - -import aiohttp -import aiofiles -import zipfile - -class Api: - def __init__(self, host="localhost:5000"): - self.host = host - self.basepath = "http://"+host+"/api/current" - - async def _get(self, endpoint): - if endpoint[0] != "/": - endpoint = "/" + endpoint - async with aiohttp.ClientSession() as session: - async with session.get(self.basepath+endpoint) as response: - return await response.json() - - async def _download(self, endpoint, filename="temp"): - if endpoint[0] != "/": - endpoint = "/" + endpoint - async with aiohttp.ClientSession() as session: - async with session.get(self.basepath+endpoint) as resp: - f = await aiofiles.open(filename, mode='wb') - await f.write(await resp.read()) - await f.close() - - async def list(self): - return await self._get("modules/") - - async def download(self, module, version): - await self._download("modules/"+module+"/"+version, filename="temp.zip") - # TODO: Supprimer le dossier ici - try: - shutil.rmtree(os.path.join("modules", module)) - except: - print('Error while deleting directory') - with zipfile.ZipFile('temp.zip', "r") as z: - z.extractall(os.path.join("modules", module)) diff --git a/src/modules/modules/version.json b/src/modules/modules/version.json deleted file mode 100644 index 85a57f9..0000000 --- a/src/modules/modules/version.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "version": "0.1.0", - "type": "python", - "dependencies": { - "base": { - "min": "0.1.0", - "max": "0.1.0" - } - }, - "bot_version": { - "min": "0.1.0", - "max": "0.1.0" - } -} \ No newline at end of file diff --git a/src/modules/newmember/__init__.py b/src/modules/newmember/__init__.py deleted file mode 100644 index 9ad8e18..0000000 --- a/src/modules/newmember/__init__.py +++ /dev/null @@ -1,31 +0,0 @@ -from modules.base import BaseClassPython - - -class MainClass(BaseClassPython): - name = "NewMember" - help = { - "description": "Module d'accueil", - "commands": { - } - } - - def __init__(self, client): - super().__init__(client) - self.config.set({"new_role": 0, - "motd": "Bienvenue !"}) - - async def on_ready(self): - guild = self.client.get_guild(self.client.config.main_guild) - for i, member in enumerate(guild.members): - if len(member.roles) == 1: - await member.add_roles(self.client.id.get_role(id_=self.config.new_role, - guilds=[self.client.get_guild( - self.client.config.main_guild)])) - if i % 50 == 0: - self.client.log(f"Attribution des roles automatique manqués... {i}/{len(guild.members)}") - - async def on_member_join(self, member): - await member.add_roles(self.client.id.get_role(id_=self.config.new_role, - guilds=[self.client.get_guild( - self.client.config.main_guild)])) - await member.send(self.config.motd) diff --git a/src/modules/newmember/version.json b/src/modules/newmember/version.json deleted file mode 100644 index 53fa16b..0000000 --- a/src/modules/newmember/version.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "version":"0.1.0", - "type": "python", - "dependencies": { - - }, - "bot_version": { - "min": "0.1.0", - "max": "0.1.0" - } -} \ No newline at end of file diff --git a/src/modules/panic/__init__.py b/src/modules/panic/__init__.py deleted file mode 100644 index da8c649..0000000 --- a/src/modules/panic/__init__.py +++ /dev/null @@ -1,44 +0,0 @@ -import time - -import discord - -from modules.base import BaseClassPython - - -class MainClass(BaseClassPython): - name = "Panic" - help = { - "description": "Dans quel état est Nikola Tesla", - "commands": { - "`{prefix}{command}`": "Donne l'état actuel de Nikola Tesla", - } - } - - async def command(self, message, args, kwargs): - temperature = 0 - with open("/sys/class/thermal/thermal_zone0/temp") as f: - temperature = int(f.read().rstrip("\n")) / 1000 - with open("/proc/cpuinfo") as f: - cpu_count = f.read().count('\n\n') - embed = discord.Embed(title="[Panic] - Infos", color=self.config.color) - with open("/proc/loadavg") as f: - load_average = ["**" + str(round((val / cpu_count) * 100, 1)) + '%**' for val in - map(float, f.read().split(' ')[0:3])] - with open("/proc/uptime") as f: - uptime = time.gmtime(float(f.read().split(' ')[0])) - uptime = "**" + str(int(time.strftime('%-m', uptime)) - 1) + "** mois, **" + str( - int(time.strftime('%-d', uptime)) - 1) + "** jours, " + time.strftime( - '**%H** heures, **%M** minutes, **%S** secondes.', uptime) - embed.add_field( - name="Température", - value="Nikola est à **{temperature}°C**".format(temperature=temperature)) - - embed.add_field( - name="Charge moyenne", - value=f"{self.client.name} est en moyenne, utilisé à :\n sur une minute : %s\n sur cinq minutes : %s\n sur quinze minutes : %s" % tuple( - load_average)) - - embed.add_field( - name="Temps d'éveil", - value=f"{self.client.name} est éveillé depuis {uptime}".format(uptime=uptime)) - await message.channel.send(embed=embed) diff --git a/src/modules/panic/version.json b/src/modules/panic/version.json deleted file mode 100644 index 53fa16b..0000000 --- a/src/modules/panic/version.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "version":"0.1.0", - "type": "python", - "dependencies": { - - }, - "bot_version": { - "min": "0.1.0", - "max": "0.1.0" - } -} \ No newline at end of file diff --git a/src/modules/perdu/__init__.py b/src/modules/perdu/__init__.py deleted file mode 100644 index 2954150..0000000 --- a/src/modules/perdu/__init__.py +++ /dev/null @@ -1,221 +0,0 @@ -import datetime -import time - -import discord -import humanize -import matplotlib.pyplot as np - -import config -import utils.emojis -from config.config_types import factory -from modules.base import BaseClassPython - - -class MainClass(BaseClassPython): - name = "Perdu" - help = { - "description": "Module donnant les statistiques sur les perdants", - "commands": { - "`{prefix}{command}`": "Donne le classement des perdants de la semaine", - "`{prefix}{command} all`": "Donne le classement des perdants depuis toujours", - "`{prefix}{command} `": "Donne le classement des perdants sur la durée spécifiée", - "`{prefix}{command} stats [@mention]`": "Donne les statistiques d'un perdant.", - "`{prefix}{command} stats history": "Affiche un graphique avec le nombre de pertes." - } - } - - def __init__(self, client): - super().__init__(client) - self.config.set({"channel": 0, "lost_role": 0, "min_delta": datetime.timedelta(minutes=26).total_seconds()}) - self.config.register("channel", factory(config.config_types.Channel, self.client)) - self.history = {} - - async def on_ready(self): - await self.fill_history() - - async def on_message(self, message: discord.Message): - # Fill history - if message.author.bot: - return - if message.channel.id == self.config.channel: - if message.author.id not in self.history.keys(): - # Add new user if not found - self.history.update( - {message.author.id: ([(message.created_at, datetime.timedelta(seconds=0)), ])} - ) - else: - # Update user and precompute timedelta - delta = message.created_at - self.history[message.author.id][-1][0] - if delta.total_seconds() >= self.config.min_delta: - self.history[message.author.id].append((message.created_at, delta)) - await self.parse_command(message) - - async def fill_history(self): - self.history = {} - async for message in self.client.get_channel(self.config.channel).history(limit=None): - if message.author.id not in self.history.keys(): - # Add new user if not found - self.history.update({message.author.id: ([(message.created_at, datetime.timedelta(seconds=0)), ])}) - else: - # Update user and precompute timedelta - delta = self.history[message.author.id][-1][0] - message.created_at - if delta.total_seconds() >= self.config.min_delta: - self.history[message.author.id].append((message.created_at, delta)) - for user in self.history.keys(): - self.history[user].sort(key=lambda x: x[0]) - - def get_top(self, top=10, since=datetime.datetime(year=1, month=1, day=1), with_user=None, only_users=None): - """Return [(userid, [(date, delta), (date,delta), ...]), ... ]""" - # Extract only messages after until - if only_users is not None: - # Extract data for only_users - messages = [] - for user in only_users: - try: - if self.history[user][-1][0] >= since: - messages.append((user, [message for message in self.history[user] if message[0] > since])) - except KeyError: - pass - messages.sort(key=lambda x: len(x[1]), reverse=True) - return messages - if with_user is None: - with_user = [] - # Extract TOP top users, and with_users data - messages = [] - for user in self.history.keys(): - if self.history[user][-1][0] >= since: - messages.append((user, [message for message in self.history[user] if message[0] > since])) - messages.sort(key=lambda x: len(x[1]), reverse=True) - # Extract top-ten - saved_messages = messages[:min(top, len(messages))] - # Add with_user - saved_messages.extend([message for message in messages if message[0] in with_user]) - return saved_messages - - async def com_fill(self, message: discord.Message, args, kwargs): - if self.auth(message.author): - async with message.channel.typing(): - await self.fill_history() - await message.channel.send("Fait.") - - async def com_all(self, message: discord.Message, args, kwargs): - # Get all stats - top = self.get_top() - intervales = [sum(list(zip(*top[i][1]))[1], datetime.timedelta(0)) / len(top[i][1]) for i in range(len(top))] - embed_description = "\n".join( - f"{utils.emojis.write_with_number(i)} : <@{top[i][0]}> a **perdu {len(top[i][1])} fois** depuis la" - f" création du salon à en moyenne **" - f"{(str(intervales[i].days) + ' jours et' if intervales[i].days else '')} " - f"{str(int(intervales[i].total_seconds() % (24 * 3600) // 3600)) + ':' if intervales[i].total_seconds() > 3600 else ''}" - f"{int((intervales[i].total_seconds() % 3600) // 60)} " - f"{'heures' if intervales[i].total_seconds() > 3600 else 'minutes'} d'intervalle.**" - for i in range(len(top)) - )[:2000] - await message.channel.send(embed=discord.Embed(title="G-Perdu - Tableau des scores", - description=embed_description, - color=self.config.color)) - - async def com_stats(self, message: discord.Message, args, kwargs): - # TODO: Finir sum - async with message.channel.typing(): - if not ((not False or (not False or not ("sum" in args))) or not True): - if message.mentions: - top = self.get_top(only_users=[mention.id for mention in message.mentions] + [message.author.id]) - else: - # TOP 5 + auteur - top = self.get_top(top=5, with_user=[message.author.id]) - dates = [] - new_top = {} - for t in top: - for date, _ in t[1]: - dates.append(date) - dates.sort() - dates.append(datetime.datetime.today() + datetime.timedelta(days=1)) - for t in top: - user = t[0] - new_top.update({user: ([dates[0]], [0])}) - i = 0 - for date, _ in t[1]: - while date < dates[i]: - new_top[user][0].append(dates[i]) - new_top[user][1].append(new_top[user][1][-1]) - i += 1 - new_top[user][0].append(date) - new_top[user][1].append(new_top[user][1][-1] + 1) - - to_plot = [t[1][1:] for t in new_top.values()] - np.xlabel("Temps", fontsize=30) - np.ylabel("Score", fontsize=30) - np.title("Évolution du nombre de perdu au cours du temps.", fontsize=40) - np.legend() - file_name = f"/tmp/{time.time()}.png" - np.savefig(file_name, bbox_inches='tight') - await message.channel.send(file=discord.File(file_name)) - - if "history" in args: - since = datetime.datetime(year=1, month=1, day=1) - debut_message = "la création du salon" - top = 5 - if "s" in [k[0] for k in kwargs]: - try: - d = [k[1] for k in kwargs if k[0] == "s"][0] - since = datetime.datetime.now() - datetime.timedelta(days=float(d)) - debut_message = humanize.naturalday(since.date(), format='le %d %b') - except ValueError: - pass - if "t" in [k[0] for k in kwargs]: - try: - top = int([k[1] for k in kwargs if k[0] == "t"][0]) - except ValueError: - pass - # Si mention, alors uniquement les mentions - if message.mentions: - top = self.get_top(since=since, - only_users=[mention.id for mention in message.mentions]) - else: - # TOP 5 + auteur - top = self.get_top(since=since, top=top, with_user=[message.author.id]) - new_top = {} - for t in top: - c = 0 - counts = [] - dates = [] - for date, _ in t[1]: - c += 1 - counts.append(c) - dates.append(date) - new_top.update({t[0]: (dates, counts)}) - np.figure(num=None, figsize=(25, 15), dpi=120, facecolor='w', edgecolor='k') - for user, (dates, counts) in new_top.items(): - np.plot_date(dates, counts, linestyle='-', label=str(self.client.get_user(user).name)) - np.xlabel("Temps", fontsize=30) - np.ylabel("Score", fontsize=30) - np.legend(fontsize=20) - np.title(f"Évolution du nombre de perdu au cours du temps depuis {debut_message}.", fontsize=35) - file_name = f"/tmp/{time.time()}.png" - np.savefig(file_name, bbox_inches='tight') - await message.channel.send(file=discord.File(file_name)) - - async def command(self, message, args, kwargs): - if message.mentions: - await self.com_stats(message, args, kwargs) - since = datetime.datetime.now() - datetime.timedelta(days=7) - if args[0]: - try: - since = datetime.datetime.now() - datetime.timedelta(days=float(args[0])) - except ValueError: - pass - top = self.get_top(10, since) - intervales = [sum(list(zip(*top[i][1]))[1], datetime.timedelta(0)) / len(top[i][1]) for i in range(len(top))] - embed_description = "\n".join( - f"{utils.emojis.write_with_number(i)} : <@{top[i][0]}> a **perdu {len(top[i][1])} fois** depuis " - f"{humanize.naturalday(since.date(), format='le %d %b')} à en moyenne **" - f"{(str(intervales[i].days) + ' jours et' if intervales[i].days else '')} " - f"{str(int(intervales[i].total_seconds() % (24 * 3600) // 3600)) + ':' if intervales[i].total_seconds() > 3600 else ''}" - f"{int((intervales[i].total_seconds() % 3600) // 60)} " - f"{'heures' if intervales[i].total_seconds() > 3600 else 'minutes'} d'intervalle.**" - for i in range(len(top)) - )[:2000] - await message.channel.send(embed=discord.Embed(title="G-Perdu - Tableau des scores", - description=embed_description, - color=self.config.color)) diff --git a/src/modules/perdu/version.json b/src/modules/perdu/version.json deleted file mode 100644 index 53fa16b..0000000 --- a/src/modules/perdu/version.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "version":"0.1.0", - "type": "python", - "dependencies": { - - }, - "bot_version": { - "min": "0.1.0", - "max": "0.1.0" - } -} \ No newline at end of file diff --git a/src/modules/purge/__init__.py b/src/modules/purge/__init__.py deleted file mode 100644 index 2061a57..0000000 --- a/src/modules/purge/__init__.py +++ /dev/null @@ -1,35 +0,0 @@ -from modules.base import BaseClassPython - -class MainClass(BaseClassPython): - name = "Purge" - help = { - "description": "Suppression de messages en block.", - "commands": { - "`{prefix}{command} `": "Supprime tous les messages du salon jusqu'au message spécifié", - } - } - - async def command(self, message, args, kwargs): - message_id = None - try: - message_id = int(args[0]) - except ValueError: - pass - if len(args) and message_id is not None: - messages_list=[] - done=False - async for current in message.channel.history(limit=None): - if int(current.id) == message_id: - done = True - break - elif message.id != current.id: - messages_list.append(current) - if done: - chunks = [messages_list[x:x+99] for x in range(0, len(messages_list), 99)] - for chunk in chunks: - await message.channel.delete_messages(chunk) - await message.channel.send(f"**{len(messages_list)}** messages supprimés.") - else: - await message.channel.send("Le message spécifié n'a pas été trouvé.") - else: - await message.channel.send("Arguments invalides.") diff --git a/src/modules/purge/version.json b/src/modules/purge/version.json deleted file mode 100644 index 53fa16b..0000000 --- a/src/modules/purge/version.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "version":"0.1.0", - "type": "python", - "dependencies": { - - }, - "bot_version": { - "min": "0.1.0", - "max": "0.1.0" - } -} \ No newline at end of file diff --git a/src/modules/readrules/__init__.py b/src/modules/readrules/__init__.py deleted file mode 100644 index 3e17a0d..0000000 --- a/src/modules/readrules/__init__.py +++ /dev/null @@ -1,38 +0,0 @@ -from modules.base import BaseClassPython - - -class MainClass(BaseClassPython): - name = "ReadRules" - color = 0xff071f - help_active = False - help = { - "description": "Module d'accueil", - "commands": { - } - } - - def __init__(self, client): - super().__init__(client) - self.config.set({"accepted_role": 0, - "new_role": 0, - "listen_chan": 0, - "log_chan": 0, - "passwords": [], - "succes_pm": "Félicitations, vous savez lire les règles!", - "succes": "{user} a désormais accepté."}) - - async def on_message(self, message): - if message.author.bot: - return - if message.channel.id == self.config.listen_chan: - if message.content.lower() in self.config.passwords: - new_role = self.client.id.get_role(id_=self.config.new_role, guilds=[message.channel.guild]) - if new_role in message.author.roles: - await message.author.remove_roles(new_role) - await message.author.add_roles(self.client.id.get_role(id_=self.config.accepted_role, - guild=[message.channel.guild])) - await message.author.send(self.config.succes_pm) - await message.channel.guild.get_channel(self.config.log_chan).send( - self.config.succes.format(user=message.author.mention)) - else: - await message.author.send(f"Le mot de passe que vous avez entré est incorrect : `{message.content}`.\nNous vous prions de lire le règlement afin d'accéder au serveur complet.") diff --git a/src/modules/readrules/version.json b/src/modules/readrules/version.json deleted file mode 100644 index 53fa16b..0000000 --- a/src/modules/readrules/version.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "version":"0.1.0", - "type": "python", - "dependencies": { - - }, - "bot_version": { - "min": "0.1.0", - "max": "0.1.0" - } -} \ No newline at end of file diff --git a/src/modules/restart/__init__.py b/src/modules/restart/__init__.py deleted file mode 100644 index 367b71b..0000000 --- a/src/modules/restart/__init__.py +++ /dev/null @@ -1,19 +0,0 @@ -import sys - -from modules.base import BaseClassPython - - -class MainClass(BaseClassPython): - name = "Restart" - help = { - "description": "Module gérant les redémarrages de Nikola Tesla", - "commands": { - "`{prefix}{command}`": "Redémarre le bot.", - } - } - - async def command(self, message, args, kwargs): - await message.channel.send(f"{message.author.mention}, Le bot va redémarrer.") - await self.client.logout() - # TODO: Faut vraiment faire mieux - sys.exit(0) diff --git a/src/modules/restart/version.json b/src/modules/restart/version.json deleted file mode 100644 index 53fa16b..0000000 --- a/src/modules/restart/version.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "version":"0.1.0", - "type": "python", - "dependencies": { - - }, - "bot_version": { - "min": "0.1.0", - "max": "0.1.0" - } -} \ No newline at end of file diff --git a/src/modules/roles/__init__.py b/src/modules/roles/__init__.py deleted file mode 100644 index 067d070..0000000 --- a/src/modules/roles/__init__.py +++ /dev/null @@ -1,129 +0,0 @@ -import discord - -from modules.base import BaseClassPython - - -class RoleAttributionError(Exception): - pass - - -class AlreadyHasRoleError(RoleAttributionError): - pass - - -class AlreadyRemovedRole(RoleAttributionError): - pass - - -class UnavailableRoleError(RoleAttributionError): - pass - - -class MainClass(BaseClassPython): - name = "Roles" - help = { - "description": "Module gérant l'attribution des roles", - "commands": { - "`{prefix}{command} list`": "Liste les roles", - "`{prefix}{command} add [role] ...`": "S'attribuer le(s) rôle(s) ([role]...)", - "`{prefix}{command} remove [role] ...`": "Se désattribuer le(s) rôle(s) ([role]...)", - "`{prefix}{command} toggle [role] ...`": "S'attribuer (ou désattribuer) le(s) rôle(s) ([role]...)", - "`{prefix}{command} [role] ...`": "Alias de `{prefix}{command} toggle`", - } - } - - def __init__(self, client): - super().__init__(client) - self.config.set({"roles": {}}) - - async def com_list(self, message, args, kwargs): - response = discord.Embed(title="Roles disponibles", color=self.config.color) - for id_ in self.config.roles.keys(): - role = message.guild.get_role(id_=int(id_)) - if role is not None: - response.add_field(name=role.name, value=f"-> `{self.config.roles[id_]}`", inline=True) - await message.channel.send(embed=response) - - async def com_add(self, message, args, kwargs): - if len(args) <= 1: - await message.channel.send("Il manque des arguments à la commande") - for role in args[1:]: - try: - await self.try_add_role(message.author, role) - except discord.errors.Forbidden: - await message.channel.send(f"Je n'ai pas la permission de modifier le role {role}.") - except AlreadyHasRoleError: - await message.channel.send(f"Vous avez déjà le role {role}.") - except UnavailableRoleError: - await message.channel.send(f"Le role {role} n'est pas une role disponible à l'autoattribution.") - - async def com_remove(self, message, args, kwargs): - if len(args) <= 1: - await message.channel.send("Il manque des arguments à la commande") - for role in args[1:]: - try: - await self.try_remove_role(message.author, role) - except discord.errors.Forbidden: - await message.channel.send(f"Je n'ai pas la permission de modifier le role {role}.") - except AlreadyRemovedRole: - await message.channel.send(f"Vous n'avez pas le role {role}.") - except UnavailableRoleError: - await message.channel.send(f"Le role {role} n'est pas une role disponible à l'autoattribution.") - - async def com_toggle(self, message, args, kwargs): - if len(args) <= 1: - await message.channel.send("Il manque des arguments à la commande") - for role in args[1:]: - try: - await self.try_toggle_role(message.author, role) - except discord.errors.Forbidden: - await message.channel.send(f"Je n'ai pas la permission de modifier le role {role}.") - except AlreadyHasRoleError: - await message.channel.send(f"Vous avez déjà le role {role}.") - except AlreadyRemovedRole: - await message.channel.send(f"Vous n'avez pas le role {role}.") - except UnavailableRoleError: - await message.channel.send(f"Le role {role} n'est pas une role disponible à l'autoattribution.") - - async def command(self, message, args, kwargs): - if len(args) < 1: - await message.channel.send("Il manque des arguments à la commande") - for role in args: - try: - await self.try_toggle_role(message.author, role) - except discord.errors.Forbidden: - await message.channel.send(f"Je n'ai pas la permission de modifier le role {role}.") - except AlreadyHasRoleError: - await message.channel.send(f"Vous avez déjà le role {role}.") - except AlreadyRemovedRole: - await message.channel.send(f"Vous n'avez pas le role {role}.") - except UnavailableRoleError: - await message.channel.send(f"Le role {role} n'est pas une role disponible à l'autoattribution.") - - def get_member(self, user): - return self.client.get_guild(self.client.config.main_guild).get_member(user.id) - - def get_role(self, role): - role = self.client.id.get_role(name=role, guilds=[self.client.get_guild(self.client.config.main_guild)], - check=lambda x: x.name.lower() == role.lower()) - if role is None or str(role.id) not in self.config.roles.keys(): - raise UnavailableRoleError() - return role - - async def try_toggle_role(self, user, role): - if self.get_role(role) in self.get_member(user).roles: - await self.try_remove_role(user, role) - else: - await self.try_add_role(user, role) - - async def try_add_role(self, user, role): - role = self.get_role(role) - if role in user.roles: - raise AlreadyHasRoleError() - await self.get_member(user).add_roles(role, reason="Auto-attribution") - - async def try_remove_role(self, user, role): - role = self.get_role(role) - if role not in user.roles: - raise AlreadyRemovedRole() - await self.get_member(user).remove_roles(role, reason="Auto-désattribution") diff --git a/src/modules/roles/version.json b/src/modules/roles/version.json deleted file mode 100644 index 53fa16b..0000000 --- a/src/modules/roles/version.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "version":"0.1.0", - "type": "python", - "dependencies": { - - }, - "bot_version": { - "min": "0.1.0", - "max": "0.1.0" - } -} \ No newline at end of file diff --git a/src/modules/rtfgd/__init__.py b/src/modules/rtfgd/__init__.py deleted file mode 100644 index 7723561..0000000 --- a/src/modules/rtfgd/__init__.py +++ /dev/null @@ -1,25 +0,0 @@ -import random - -import discord - -from modules.base import BaseClassPython - - -class MainClass(BaseClassPython): - name = "rtfgd" - help = { - "description": "Read the fucking google doc", - "commands": { - "{prefix}{command} ": "Demande gentilment de lire le google doc" - } - } - - def __init__(self, client): - super().__init__(client) - self.config.set({"memes": []}) - - async def command(self, message, args, kwargs): - await message.channel.send( - " ".join(member.mention for member in message.mentions), - embed=discord.Embed(title="Read da fu**ing GOOGLE DOCS ! (╯°□°)╯︵ ┻━┻", - color=self.config.color).set_image(url=random.choice(self.config.memes))) diff --git a/src/modules/rtfgd/version.json b/src/modules/rtfgd/version.json deleted file mode 100644 index 53fa16b..0000000 --- a/src/modules/rtfgd/version.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "version":"0.1.0", - "type": "python", - "dependencies": { - - }, - "bot_version": { - "min": "0.1.0", - "max": "0.1.0" - } -} \ No newline at end of file diff --git a/src/storage/__init__.py b/src/storage/__init__.py index 3366166..abb0116 100644 --- a/src/storage/__init__.py +++ b/src/storage/__init__.py @@ -1 +1,4 @@ +from .jsonencoder import Encoder from .objects import Objects + +__all__ = ["Objects", "Encoder"] diff --git a/src/storage/objects.py b/src/storage/objects.py index 0f696fa..02dfd39 100644 --- a/src/storage/objects.py +++ b/src/storage/objects.py @@ -1,7 +1,7 @@ import json import os -from storage import jsonencoder +from . import jsonencoder class Objects: diff --git a/src/utils/__init__.py b/src/utils/__init__.py index e69de29..d97a733 100644 --- a/src/utils/__init__.py +++ b/src/utils/__init__.py @@ -0,0 +1,3 @@ +from . import emojis + +__all__ = ["emojis"] From 10e1b2333ccc8916d95e91a6ba7ed0706bffcee2 Mon Sep 17 00:00:00 2001 From: Louis Chauvet Date: Tue, 21 Apr 2020 17:35:15 +0200 Subject: [PATCH 03/15] [bot-base] Visiblement ca a l'air de marcher correctement --- src/bot_base/bot_base.py | 17 +++++++++++++++-- src/modules/__init__.py | 0 src/modules/avalon/roles.py | 0 3 files changed, 15 insertions(+), 2 deletions(-) delete mode 100644 src/modules/__init__.py delete mode 100644 src/modules/avalon/roles.py diff --git a/src/bot_base/bot_base.py b/src/bot_base/bot_base.py index bec9df5..eac8fe6 100644 --- a/src/bot_base/bot_base.py +++ b/src/bot_base/bot_base.py @@ -62,7 +62,14 @@ class BotBase(discord.Client): async def on_ready(self): self.info("Bot ready.") - def load_module(self, module): + def load_module(self, module: str) -> None: + """ + Try to load module + + :raise ModuleNotFoundError: If module is not in module folder + :raise IncompatibleModuleError: If module is incompatible + :param str module: module to load + """ # Check if module exists if not os.path.isdir(os.path.join(self.config["modules_folder"], module)): raise ModuleNotFoundError(f"Module {module} not found in modules folder ({self.config['modules_folder']}.)") @@ -100,7 +107,7 @@ class BotBase(discord.Client): raise IncompatibleModuleError(f"Module {module} mainclass ({main_class}) does not provide __dispatch__" f" attribute)") # Check if __dispatch__ is function - if not inspect.isfunction(dispatch): + if not inspect.isfunction(imported.__main_class__.__dispatch__): raise IncompatibleModuleError(f"Module {module} mainclass ({main_class}) provides __dispatch__, but it is " f"not a function ({dispatch}).") # Check if __dispatch__ can have variable positional and keyword aguments (to avoid future error on each event) @@ -130,6 +137,12 @@ class BotBase(discord.Client): } }) + def dispatch(self, event, *args, **kwargs): + """Dispatch event""" + super().dispatch(event, *args, **kwargs) + for module in self.modules.values(): + module["dispatch"](event, *args, **kwargs) + # Logging def info(self, *args, **kwargs): if self.log: diff --git a/src/modules/__init__.py b/src/modules/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/modules/avalon/roles.py b/src/modules/avalon/roles.py deleted file mode 100644 index e69de29..0000000 From 74bf2ebec1845cd0fddf119350b0817fb1346bd4 Mon Sep 17 00:00:00 2001 From: Louis Chauvet Date: Tue, 21 Apr 2020 23:19:16 +0200 Subject: [PATCH 04/15] =?UTF-8?q?[bot-base]=20Ajout=20de=20la=20gestion=20?= =?UTF-8?q?des=20d=C3=A9pendances?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + Pipfile | 2 +- Pipfile.lock | 27 +------ src/bot_base/bot_base.py | 171 +++++++++++++++++++++++++-------------- src/errors.py | 2 +- src/main.py | 2 +- 6 files changed, 118 insertions(+), 87 deletions(-) diff --git a/.gitignore b/.gitignore index 86f81e7..bdd67a5 100644 --- a/.gitignore +++ b/.gitignore @@ -74,3 +74,4 @@ data/* .env _build +/src/datas/ diff --git a/Pipfile b/Pipfile index 2587526..0c56937 100644 --- a/Pipfile +++ b/Pipfile @@ -20,4 +20,4 @@ sphinx-rtd-theme = "*" [dev-packages] [requires] -python_version = "3.7" +python_version = "3.8" diff --git a/Pipfile.lock b/Pipfile.lock index 18b8580..4b54f5d 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,11 +1,11 @@ { "_meta": { "hash": { - "sha256": "83d48f54ad14a40d4bc3b81715c5ef5bb4e604b11a7da3e9ad4db62826ff37ae" + "sha256": "d52eccc06eb777b155f756b9aadf63b98a045a979bd4882fa0ea63278245a435" }, "pipfile-spec": 6, "requires": { - "python_version": "3.7" + "python_version": "3.8" }, "sources": [ { @@ -177,14 +177,6 @@ ], "version": "==1.2.0" }, - "importlib-metadata": { - "hashes": [ - "sha256:2a688cbaa90e0cc587f1df48bdc97a6eadccdcd9c35fb3f976a09e3b5016d90f", - "sha256:34513a8a0c4962bc66d35b359558fd8a5e10cd472d37aec5f66858addef32c1e" - ], - "markers": "python_version < '3.8'", - "version": "==1.6.0" - }, "jinja2": { "hashes": [ "sha256:89aab215427ef59c34ad58735269eb58b1a5808103067f7bb9d5836c651b3bb0", @@ -477,14 +469,6 @@ "index": "pypi", "version": "==3.0.2" }, - "sphinx-autodoc-typehints": { - "hashes": [ - "sha256:27c9e6ef4f4451766ab8d08b2d8520933b97beb21c913f3df9ab2e59b56e6c6c", - "sha256:a6b3180167479aca2c4d1ed3b5cb044a70a76cccd6b38662d39288ebd9f0dff0" - ], - "index": "pypi", - "version": "==1.10.3" - }, "sphinx-rtd-theme": { "hashes": [ "sha256:00cf895504a7895ee433807c62094cf1e95f065843bf3acd17037c3e9a2becd4", @@ -605,13 +589,6 @@ "sha256:e15199cdb423316e15f108f51249e44eb156ae5dba232cb73be555324a1d49c2" ], "version": "==1.4.2" - }, - "zipp": { - "hashes": [ - "sha256:aa36550ff0c0b7ef7fa639055d797116ee891440eac1a56f378e2d3179e0320b", - "sha256:c599e4d75c98f6798c509911d08a22e6c021d074469042177c8c86fb92eefd96" - ], - "version": "==3.1.0" } }, "develop": {} diff --git a/src/bot_base/bot_base.py b/src/bot_base/bot_base.py index eac8fe6..edfa08b 100644 --- a/src/bot_base/bot_base.py +++ b/src/bot_base/bot_base.py @@ -12,7 +12,7 @@ from packaging.specifiers import SpecifierSet from config import Config, config_types from config.config_types import factory -from errors import IncompatibleModuleError +import errors __version__ = "0.1.0" MINIMAL_INFOS = ["version", "bot_version"] @@ -57,10 +57,21 @@ class BotBase(discord.Client): }) self.config.load() - self.load_module("test_module") async def on_ready(self): self.info("Bot ready.") + try: + self.load_modules() + except errors.ModuleException as e: + self.loop.stop() + raise e + + def load_modules(self): + self.info("Load modules...") + for module in self.config["modules"]: + if module not in self.modules.keys(): + self.load_module(module) + self.info("Modules loaded.") def load_module(self, module: str) -> None: """ @@ -70,72 +81,114 @@ class BotBase(discord.Client): :raise IncompatibleModuleError: If module is incompatible :param str module: module to load """ + self.info(f"Attempt to load module {module}...") # Check if module exists if not os.path.isdir(os.path.join(self.config["modules_folder"], module)): - raise ModuleNotFoundError(f"Module {module} not found in modules folder ({self.config['modules_folder']}.)") + self.warning(f"Attempt to load unknown module {module}.") + raise errors.ModuleNotFoundError(f"Module {module} not found in modules folder ({self.config['modules_folder']}.)") if not os.path.isfile(os.path.join(self.config["modules_folder"], module, "infos.toml")): - raise IncompatibleModuleError(f"Module {module} is incompatible: no infos.toml found.") + self.warning(f"Attempt to load incompatible module {module}: no infos.toml found") + raise errors.IncompatibleModuleError(f"Module {module} is incompatible: no infos.toml found.") # Check infos.toml integrity with open(os.path.join(self.config["modules_folder"], module, "infos.toml")) as f: infos = toml.load(f) for key in MINIMAL_INFOS: if key not in infos.keys(): - raise IncompatibleModuleError(f"Missing information for module {module}: missing {key}.") - # Check bot_version + self.warning(f"Attempt to load incompatible module {module}: missing information {key}") + raise errors.IncompatibleModuleError(f"Missing information for module {module}: missing {key}.") + # Check bot_version bot_version_specifier = SpecifierSet(infos["bot_version"]) if __version__ not in bot_version_specifier: - raise IncompatibleModuleError(f"Module {module} is not compatible with your current bot version " + self.warning(f"Attempt to load incompatible module {module}: need bot version {infos['bot_version']} " + f"and you have {__version__}") + raise errors.IncompatibleModuleError(f"Module {module} is not compatible with your current bot version " f"(need {infos['bot_version']} and you have {__version__}).") - # Check if module have __main_class__ - imported = importlib.import_module(module) - try: - main_class = imported.__main_class__ - except AttributeError: - raise IncompatibleModuleError(f"Module {module} does not provide __main_class__.") - # Check if __main_class__ is a class - if not inspect.isclass(main_class): - raise IncompatibleModuleError(f"Module {module} contains __main_class__ but it is not a type.") - try: - main_class = main_class(self) - except TypeError: - # Module don't need client reference - main_class = main_class() - # Check if __main_class__ have __dispatch__ attribute - try: - dispatch = main_class.__dispatch__ - except AttributeError: - raise IncompatibleModuleError(f"Module {module} mainclass ({main_class}) does not provide __dispatch__" - f" attribute)") - # Check if __dispatch__ is function - if not inspect.isfunction(imported.__main_class__.__dispatch__): - raise IncompatibleModuleError(f"Module {module} mainclass ({main_class}) provides __dispatch__, but it is " - f"not a function ({dispatch}).") - # Check if __dispatch__ can have variable positional and keyword aguments (to avoid future error on each event) - sig = inspect.signature(dispatch) - args_present, kwargs_present = False, False - for p in sig.parameters.values(): - if p.kind == p.VAR_POSITIONAL: - args_present = True - elif p.kind == p.VAR_KEYWORD: - kwargs_present = True - if not args_present: - raise IncompatibleModuleError( - f"Module {module} mainclass ({main_class}) provide __dispatch__ function, but " - f"this function doesn't accept variable positionnal arguments.") - if not kwargs_present: - raise IncompatibleModuleError( - f"Module {module} mainclass ({main_class}) provide __dispatch__ function, but " - f"this function doesn't accept variable keywords arguments.") - # Module is compatible! - # Add module to loaded modules + # Check dependencies + if infos.get("dependencies"): + for dep, version in infos["dependencies"].items(): + if not dep in self.modules.keys(): + self.load_module(dep) + dep_version_specifier = SpecifierSet(version) + if self.modules[dep]["infos"]["version"] not in dep_version_specifier: + self.warning(f"Attempt to load incompatible module {module}: (require {dep} ({version}) " + f"and you have {dep} ({self.modules[dep]['infos']['version']})") + raise errors.IncompatibleModuleError(f"Module {module} is not compatible with your current install " + f"(require {dep} ({version}) and you have {dep} " + f"({self.modules[dep]['infos']['version']})") - self.modules.update({ - module: { - "imported": imported, - "initialized_class": main_class, - "dispatch": dispatch, - } - }) + # Check if module is meta + if infos.get("metamodule", False) == False: + # Check if module have __main_class__ + imported = importlib.import_module(module) + try: + main_class = imported.__main_class__ + except AttributeError: + self.warning(f"Attempt to load incompatible module {module}: no __main_class__ found") + raise errors.IncompatibleModuleError(f"Module {module} does not provide __main_class__.") + # Check if __main_class__ is a class + if not inspect.isclass(main_class): + self.warning(f"Attempt to load incompatible module {module}: __main_class__ is not a type") + raise errors.IncompatibleModuleError(f"Module {module} contains __main_class__ but it is not a type.") + try: + main_class = main_class(self) + except TypeError: + # Module don't need client reference + main_class = main_class() + # Check if __main_class__ have __dispatch__ attribute + try: + dispatch = main_class.__dispatch__ + except AttributeError: + self.warning(f"Attempt to load incompatible module {module}: __dispatch_ not found") + raise errors.IncompatibleModuleError(f"Module {module} mainclass ({main_class}) does not provide __dispatch__" + f" attribute)") + # Check if __dispatch__ is function + if not inspect.isfunction(imported.__main_class__.__dispatch__): + self.warning(f"Attempt to load incompatible module {module}: __dispatch__ is not a function") + raise errors.IncompatibleModuleError( + f"Module {module} mainclass ({main_class}) provides __dispatch__, but it is " + f"not a function ({dispatch}).") + # Check if __dispatch__ can have variable positional and keyword aguments (to avoid future error on each event) + sig = inspect.signature(dispatch) + args_present, kwargs_present = False, False + for p in sig.parameters.values(): + if p.kind == p.VAR_POSITIONAL: + args_present = True + elif p.kind == p.VAR_KEYWORD: + kwargs_present = True + if not args_present: + self.warning(f"Attempt to load incompatible module {module}: __dispatch__ doesn't accept variable " + f"positional arguments") + raise errors.IncompatibleModuleError( + f"Module {module} mainclass ({main_class}) provide __dispatch__ function, but " + f"this function doesn't accept variable positional arguments.") + if not kwargs_present: + self.warning(f"Attempt to load incompatible module {module}: __dispatch__ doesn't accept variable " + f"keywords arguments.") + raise errors.IncompatibleModuleError( + f"Module {module} mainclass ({main_class}) provide __dispatch__ function, but " + f"this function doesn't accept variable keywords arguments.") + # Module is compatible! + # Add module to loaded modules + self.info(f"Add modules {module} to current modules.") + self.modules.update({ + module: { + "infos": infos, + "imported": imported, + "initialized_class": main_class, + "dispatch": dispatch, + } + }) + else: # Module is metamodule + self.info(f"Add modules {module} to current modules") + self.modules.update({ + module: { + "infos": infos, + "dispatch": lambda *x, **y: None + } + }) + if module not in self.config["modules"]: + self.config.set({"modules": self.config["modules"] + [module]}) + self.config.save() def dispatch(self, event, *args, **kwargs): """Dispatch event""" @@ -147,14 +200,14 @@ class BotBase(discord.Client): def info(self, *args, **kwargs): if self.log: self.log.info(*args, **kwargs) - self.dispatch("on_log_info", *args, **kwargs) + self.dispatch("log_info", *args, **kwargs) def error(self, *args, **kwargs): if self.log: self.log.error(*args, **kwargs) - self.dispatch("on_log_error", *args, **kwargs) + self.dispatch("log_error", *args, **kwargs) def warning(self, *args, **kwargs): if self.log: self.log.warning(*args, **kwargs) - self.dispatch("on_log_warning", *args, **kwargs) + self.dispatch("log_warning", *args, **kwargs) diff --git a/src/errors.py b/src/errors.py index ccbef48..e880d59 100644 --- a/src/errors.py +++ b/src/errors.py @@ -6,7 +6,7 @@ class ModuleException(BotBaseException): pass -class ModuleNotFound(ModuleException): +class ModuleNotFoundError(ModuleException): pass diff --git a/src/main.py b/src/main.py index 47ddb8a..c9564fc 100644 --- a/src/main.py +++ b/src/main.py @@ -25,7 +25,7 @@ def setup_logging(default_path='data/log_config.json', default_level=logging.INF setup_logging() if __name__ == "__main__": - client = BotBase(max_messages=500000) + client = BotBase(max_messages=500000, data_folder="datas", modules_folder=os.environ.get("LOCAL_MODULES", "modules")) async def start_bot(): await client.start(os.environ.get("DISCORD_TOKEN")) From 3d60083f2f066fffacf5ddb0ab27001e31537eca Mon Sep 17 00:00:00 2001 From: Louis Chauvet Date: Tue, 21 Apr 2020 23:20:05 +0200 Subject: [PATCH 05/15] =?UTF-8?q?[scripts]=20Marqu=C3=A9s=20comme=20ex?= =?UTF-8?q?=C3=A9cutables?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/build-docs.sh | 0 scripts/install-hooks.sh | 0 scripts/pre-commit.sh | 0 scripts/run-tests.sh | 0 4 files changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 scripts/build-docs.sh mode change 100644 => 100755 scripts/install-hooks.sh mode change 100644 => 100755 scripts/pre-commit.sh mode change 100644 => 100755 scripts/run-tests.sh diff --git a/scripts/build-docs.sh b/scripts/build-docs.sh old mode 100644 new mode 100755 diff --git a/scripts/install-hooks.sh b/scripts/install-hooks.sh old mode 100644 new mode 100755 diff --git a/scripts/pre-commit.sh b/scripts/pre-commit.sh old mode 100644 new mode 100755 diff --git a/scripts/run-tests.sh b/scripts/run-tests.sh old mode 100644 new mode 100755 From 1966b69d5357ca23c9b91dc6d84d35610e763c0b Mon Sep 17 00:00:00 2001 From: Louis Chauvet Date: Tue, 21 Apr 2020 23:22:57 +0200 Subject: [PATCH 06/15] [doc] La doc peut maintenant compiler --- .gitignore | 3 ++- doc/source/conf.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index bdd67a5..d2c50bc 100644 --- a/.gitignore +++ b/.gitignore @@ -73,5 +73,6 @@ data/* .env -_build +/doc/build /src/datas/ +/src/doctest_config.toml diff --git a/doc/source/conf.py b/doc/source/conf.py index c2c81fb..102e02c 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -13,7 +13,7 @@ import os import sys -sys.path.insert(0, os.path.abspath('../../')) +sys.path.insert(0, os.path.abspath('../../src')) # -- Project information ----------------------------------------------------- From 75f524c509660def217fc6421b2523538356397e Mon Sep 17 00:00:00 2001 From: Louis Chauvet Date: Fri, 24 Apr 2020 12:10:33 +0200 Subject: [PATCH 07/15] [config] Suppression des nones dans le toml, sauvegarde de la config au chargement pour ajouter les nouveaux parametres [bot-base] Gestion des erreurs --- src/bot_base/bot_base.py | 32 ++++++++----- src/config/base.py | 13 +++--- .../config_types/discord_types/channel.py | 45 ++++++++++++++----- .../config_types/discord_types/guild.py | 7 +-- 4 files changed, 66 insertions(+), 31 deletions(-) diff --git a/src/bot_base/bot_base.py b/src/bot_base/bot_base.py index edfa08b..0cb14e7 100644 --- a/src/bot_base/bot_base.py +++ b/src/bot_base/bot_base.py @@ -5,6 +5,7 @@ import inspect import logging import os import sys +import traceback import discord import toml @@ -85,7 +86,8 @@ class BotBase(discord.Client): # Check if module exists if not os.path.isdir(os.path.join(self.config["modules_folder"], module)): self.warning(f"Attempt to load unknown module {module}.") - raise errors.ModuleNotFoundError(f"Module {module} not found in modules folder ({self.config['modules_folder']}.)") + raise errors.ModuleNotFoundError( + f"Module {module} not found in modules folder ({self.config['modules_folder']}.)") if not os.path.isfile(os.path.join(self.config["modules_folder"], module, "infos.toml")): self.warning(f"Attempt to load incompatible module {module}: no infos.toml found") raise errors.IncompatibleModuleError(f"Module {module} is incompatible: no infos.toml found.") @@ -102,7 +104,7 @@ class BotBase(discord.Client): self.warning(f"Attempt to load incompatible module {module}: need bot version {infos['bot_version']} " f"and you have {__version__}") raise errors.IncompatibleModuleError(f"Module {module} is not compatible with your current bot version " - f"(need {infos['bot_version']} and you have {__version__}).") + f"(need {infos['bot_version']} and you have {__version__}).") # Check dependencies if infos.get("dependencies"): for dep, version in infos["dependencies"].items(): @@ -113,13 +115,17 @@ class BotBase(discord.Client): self.warning(f"Attempt to load incompatible module {module}: (require {dep} ({version}) " f"and you have {dep} ({self.modules[dep]['infos']['version']})") raise errors.IncompatibleModuleError(f"Module {module} is not compatible with your current install " - f"(require {dep} ({version}) and you have {dep} " - f"({self.modules[dep]['infos']['version']})") + f"(require {dep} ({version}) and you have {dep} " + f"({self.modules[dep]['infos']['version']})") # Check if module is meta if infos.get("metamodule", False) == False: # Check if module have __main_class__ - imported = importlib.import_module(module) + try: + imported = importlib.import_module(module) + except Exception as e: + self.warning(f"Attempt to load incompatible module {module}: failed import") + raise e try: main_class = imported.__main_class__ except AttributeError: @@ -139,8 +145,9 @@ class BotBase(discord.Client): dispatch = main_class.__dispatch__ except AttributeError: self.warning(f"Attempt to load incompatible module {module}: __dispatch_ not found") - raise errors.IncompatibleModuleError(f"Module {module} mainclass ({main_class}) does not provide __dispatch__" - f" attribute)") + raise errors.IncompatibleModuleError( + f"Module {module} mainclass ({main_class}) does not provide __dispatch__" + f" attribute)") # Check if __dispatch__ is function if not inspect.isfunction(imported.__main_class__.__dispatch__): self.warning(f"Attempt to load incompatible module {module}: __dispatch__ is not a function") @@ -178,7 +185,7 @@ class BotBase(discord.Client): "dispatch": dispatch, } }) - else: # Module is metamodule + else: # Module is metamodule self.info(f"Add modules {module} to current modules") self.modules.update({ module: { @@ -196,16 +203,19 @@ class BotBase(discord.Client): for module in self.modules.values(): module["dispatch"](event, *args, **kwargs) + async def on_error(self, event_method, exc, *args, **kwargs): + self.error(f"Error in {event_method}: \n{exc}") + # Logging def info(self, *args, **kwargs): if self.log: self.log.info(*args, **kwargs) self.dispatch("log_info", *args, **kwargs) - def error(self, *args, **kwargs): + def error(self, e, *args, **kwargs): if self.log: - self.log.error(*args, **kwargs) - self.dispatch("log_error", *args, **kwargs) + self.log.error(e, *args, **kwargs) + self.dispatch("log_error", e, *args, **kwargs) def warning(self, *args, **kwargs): if self.log: diff --git a/src/config/base.py b/src/config/base.py index f70a636..2afecf0 100644 --- a/src/config/base.py +++ b/src/config/base.py @@ -1,5 +1,6 @@ from __future__ import annotations +import os import typing import toml @@ -75,8 +76,9 @@ class Config: >>> config = Config("doctest_config.toml") >>> config.register("my_parameter", factory(Int)) >>> config.set({"my_parameter": 3}) - >>> config.save() + >>> config.save() #doctest: +SKIP """ + os.makedirs(os.path.dirname(self.path), exist_ok=True) with open(self.path, 'w') as file: toml.dump({k: v.to_save() for k, v in self.fields.items()}, file) @@ -90,11 +92,11 @@ class Config: >>> config = Config("doctest_config.toml") >>> config.register("my_parameter", factory(Int)) >>> config.set({"my_parameter": 3}) - >>> config.save() + >>> config.save() #doctest: +SKIP >>> new_config = Config("doctest_config.toml") >>> new_config.register("my_parameter", factory(Int)) - >>> new_config.load() - >>> new_config["my_parameter"] + >>> new_config.load() #doctest: +SKIP + >>> new_config["my_parameter"] #doctest: +SKIP 3 :return: None @@ -103,7 +105,8 @@ class Config: with open(self.path, 'r') as file: self.set(toml.load(file)) except FileNotFoundError: - self.save() + pass + self.save() def __getitem__(self, item: str) -> typing.Any: """ diff --git a/src/config/config_types/discord_types/channel.py b/src/config/config_types/discord_types/channel.py index edffe0c..f7cd831 100644 --- a/src/config/config_types/discord_types/channel.py +++ b/src/config/config_types/discord_types/channel.py @@ -1,36 +1,57 @@ from __future__ import annotations -from typing import TYPE_CHECKING +import typing + +import discord from config.config_types.base_type import BaseType -if TYPE_CHECKING: - from main import LBI +if typing.TYPE_CHECKING: + from bot_base import BotBase class Channel(BaseType): - client: LBI + client: BotBase def __init__(self, client): - self.value = None + self.value = 0 + self.channel_instance = None self.client = client def check_value(self, value): + id = value + if isinstance(value, discord.Guild): + id = value.id + if not self.client.is_ready(): + self.client.warning(f"No check for channel {value} because client is not initialized!") + return True + if self.client.get_channel(id): + return True return True def set(self, value): - if self.check_value(value): - self.value = value - return - raise ValueError("Tentative de définir une valeur incompatible") + if not self.check_value(value): + raise ValueError("Tentative de définir une valeur incompatible") + self.value = value + self._update() def get(self): - return self.value + self._update() + return self.channel_instance or self.value def to_save(self): - return self.value + return self.value or 0 def load(self, value): + if self.check_value(value): raise ValueError("Tentative de charger une donnée incompatible.") - self.value = value + self.set(value) + self._update() + + def _update(self): + if self.client.is_ready() and self.channel_instance is None: + self.channel_instance = self.client.get_channel(self.value) + else: + self.channel_instance = None + diff --git a/src/config/config_types/discord_types/guild.py b/src/config/config_types/discord_types/guild.py index e38c654..9c9544d 100644 --- a/src/config/config_types/discord_types/guild.py +++ b/src/config/config_types/discord_types/guild.py @@ -29,7 +29,7 @@ class Guild(BaseType): >>> Guild(client) #doctest: +SKIP """ - self.value = None + self.value = 0 self.guild_instance = None self.client = client @@ -56,7 +56,7 @@ class Guild(BaseType): if isinstance(value, discord.Guild): id = value.id if not self.client.is_ready(): - self.client.warning("No check for guild `value` because client is not initialized!") + self.client.warning(f"No check for guild {value} because client is not initialized!") return True if self.client.get_guild(id): return True @@ -119,7 +119,7 @@ class Guild(BaseType): :return: Current id :rtype: Optional[int] """ - return self.value + return self.value or 0 def load(self, value): """ @@ -140,6 +140,7 @@ class Guild(BaseType): if self.check_value(value): raise ValueError("Tentative de charger une donnée incompatible.") self.set(value) + self._update() def __repr__(self): return f'' From e48f74e59213c43b9da94424332e811e5ef61f32 Mon Sep 17 00:00:00 2001 From: Louis Chauvet Date: Fri, 24 Apr 2020 22:02:32 +0200 Subject: [PATCH 08/15] =?UTF-8?q?[bot-base]=20Ajout=20de=20la=20gestion=20?= =?UTF-8?q?des=20erreurs=20et=20des=20warns=20[config]=20Modification=20de?= =?UTF-8?q?=20Channel=20pour=20=C3=A9viter=20les=20Nones=20inutiles?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/bot_base/bot_base.py | 6 +++--- src/config/config_types/discord_types/channel.py | 3 ++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/bot_base/bot_base.py b/src/bot_base/bot_base.py index 0cb14e7..41ff1db 100644 --- a/src/bot_base/bot_base.py +++ b/src/bot_base/bot_base.py @@ -207,10 +207,10 @@ class BotBase(discord.Client): self.error(f"Error in {event_method}: \n{exc}") # Logging - def info(self, *args, **kwargs): + def info(self, info, *args, **kwargs): if self.log: - self.log.info(*args, **kwargs) - self.dispatch("log_info", *args, **kwargs) + self.log.info(info, *args, **kwargs) + self.dispatch("log_info", info, *args, **kwargs) def error(self, e, *args, **kwargs): if self.log: diff --git a/src/config/config_types/discord_types/channel.py b/src/config/config_types/discord_types/channel.py index f7cd831..238457d 100644 --- a/src/config/config_types/discord_types/channel.py +++ b/src/config/config_types/discord_types/channel.py @@ -36,7 +36,8 @@ class Channel(BaseType): self._update() def get(self): - self._update() + if self.channel_instance is None: + self._update() return self.channel_instance or self.value def to_save(self): From 018907f86b9e0965f7c61eb81e59e97dcf9e5085 Mon Sep 17 00:00:00 2001 From: Louis Chauvet Date: Fri, 24 Apr 2020 22:04:30 +0200 Subject: [PATCH 09/15] =?UTF-8?q?oups,=20oubli=C3=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/bot_base/bot_base.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/bot_base/bot_base.py b/src/bot_base/bot_base.py index 41ff1db..78ae836 100644 --- a/src/bot_base/bot_base.py +++ b/src/bot_base/bot_base.py @@ -217,7 +217,7 @@ class BotBase(discord.Client): self.log.error(e, *args, **kwargs) self.dispatch("log_error", e, *args, **kwargs) - def warning(self, *args, **kwargs): + def warning(self, warning, *args, **kwargs): if self.log: - self.log.warning(*args, **kwargs) - self.dispatch("log_warning", *args, **kwargs) + self.log.warning(warning, *args, **kwargs) + self.dispatch("log_warning", warning, *args, **kwargs) From f6b5faf80861012dead59320ac275d6ca5584564 Mon Sep 17 00:00:00 2001 From: Louis Chauvet Date: Fri, 24 Apr 2020 22:29:43 +0200 Subject: [PATCH 10/15] =?UTF-8?q?Suppression=20de=20toutes=20les=20r=C3=A9?= =?UTF-8?q?f=C3=A9rences=20=C3=A0=20LBI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/config/config_types/discord_types/role.py | 10 ++++------ src/config/config_types/discord_types/user.py | 8 ++++---- src/config/log_config.json | 2 +- src/main.py | 2 +- 4 files changed, 10 insertions(+), 12 deletions(-) diff --git a/src/config/config_types/discord_types/role.py b/src/config/config_types/discord_types/role.py index 3d600fb..a468b57 100644 --- a/src/config/config_types/discord_types/role.py +++ b/src/config/config_types/discord_types/role.py @@ -1,15 +1,13 @@ from __future__ import annotations - -from typing import TYPE_CHECKING +import typing from config.config_types.base_type import BaseType - -if TYPE_CHECKING: - from main import LBI +if typing.TYPE_CHECKING: + from bot_base import BotBase class Role(BaseType): - client: LBI + client: BotBase def __init__(self, client): self.value = None diff --git a/src/config/config_types/discord_types/user.py b/src/config/config_types/discord_types/user.py index 2c53a87..1b98fad 100644 --- a/src/config/config_types/discord_types/user.py +++ b/src/config/config_types/discord_types/user.py @@ -1,15 +1,15 @@ from __future__ import annotations -from typing import TYPE_CHECKING +import typing from config.config_types.base_type import BaseType -if TYPE_CHECKING: - from main import LBI +if typing.TYPE_CHECKING: + from bot_base import BotBase class User(BaseType): - client: LBI + client: BotBase def __init__(self, client): self.value = None diff --git a/src/config/log_config.json b/src/config/log_config.json index e0954b0..6ef8d66 100644 --- a/src/config/log_config.json +++ b/src/config/log_config.json @@ -41,7 +41,7 @@ "level":"ERROR", "handlers":["console", "info_file_handler", "error_file_handler"] }, - "LBI": { + "bot-base": { "level":"DEBUG", "handlers":["console", "info_file_handler", "error_file_handler"] } diff --git a/src/main.py b/src/main.py index c9564fc..ca82e93 100644 --- a/src/main.py +++ b/src/main.py @@ -7,7 +7,7 @@ import os from bot_base.bot_base import BotBase -def setup_logging(default_path='data/log_config.json', default_level=logging.INFO, env_key='LBI_LOG_CONFIG'): +def setup_logging(default_path='data/log_config.json', default_level=logging.INFO, env_key='BOT_BASE_LOG_CONFIG'): """Setup logging configuration """ path = default_path From 97c1756add893fe080643219a7cdf6f99d007a0d Mon Sep 17 00:00:00 2001 From: Louis Chauvet Date: Fri, 24 Apr 2020 22:33:42 +0200 Subject: [PATCH 11/15] Passage aux erreurs en anglais --- src/config/config_types/bool.py | 6 ++---- src/config/config_types/color.py | 4 ++-- src/config/config_types/dict.py | 2 +- src/config/config_types/discord_types/channel.py | 4 ++-- src/config/config_types/discord_types/guild.py | 4 ++-- src/config/config_types/discord_types/role.py | 4 ++-- src/config/config_types/discord_types/user.py | 4 ++-- src/config/config_types/float.py | 4 ++-- src/config/config_types/int.py | 4 ++-- src/config/config_types/list.py | 4 ++-- src/config/config_types/str.py | 4 ++-- 11 files changed, 21 insertions(+), 23 deletions(-) diff --git a/src/config/config_types/bool.py b/src/config/config_types/bool.py index 670bcf0..420c608 100644 --- a/src/config/config_types/bool.py +++ b/src/config/config_types/bool.py @@ -22,8 +22,6 @@ class Bool(BaseType): """ Check if value is a correct bool - Check if value is int, and if applicable, between ``min`` and ``max`` or in ``values`` - :Basic usage: >>> my_bool = Bool() @@ -74,7 +72,7 @@ class Bool(BaseType): :param bool value: Value to set """ if not self.check_value(value): - raise ValueError("Tentative de définir une valeur incompatible") + raise ValueError("Attempt to set incompatible value.") self.value = bool(value) def get(self) -> typing.Optional[bool]: @@ -122,7 +120,7 @@ class Bool(BaseType): :param bool value: Value to load """ if not self.check_value(value): - raise ValueError("Tentative de charger une donnée incompatible.") + raise ValueError("Attempt to load incompatible value.") self.value = value def __repr__(self): diff --git a/src/config/config_types/color.py b/src/config/config_types/color.py index c107c67..1a5dae8 100644 --- a/src/config/config_types/color.py +++ b/src/config/config_types/color.py @@ -62,7 +62,7 @@ class Color(BaseType): :param int value: Value to set """ if not self.check_value(value): - raise ValueError("Tentative de définir une valeur incompatible") + raise ValueError("Attempt to set incompatible value.") self.value = int(value) def get(self) -> typing.Optional[int]: @@ -110,7 +110,7 @@ class Color(BaseType): :param int value: Value to load """ if not self.check_value(value): - raise ValueError("Tentative de charger une donnée incompatible.") + raise ValueError("Attempt to load incompatible value.") self.value = value def __repr__(self): diff --git a/src/config/config_types/dict.py b/src/config/config_types/dict.py index 37fc1db..0d91d4b 100644 --- a/src/config/config_types/dict.py +++ b/src/config/config_types/dict.py @@ -76,7 +76,7 @@ class Dict(BaseType): """ new_dict = dict() if not self.check_value(value): - raise ValueError("Tentative de définir une valeur incompatible") + raise ValueError("Attempt to set incompatible value.") for k, v in value.items(): new_key = self.type_key() new_key.set(k) diff --git a/src/config/config_types/discord_types/channel.py b/src/config/config_types/discord_types/channel.py index 238457d..b29c7fc 100644 --- a/src/config/config_types/discord_types/channel.py +++ b/src/config/config_types/discord_types/channel.py @@ -31,7 +31,7 @@ class Channel(BaseType): def set(self, value): if not self.check_value(value): - raise ValueError("Tentative de définir une valeur incompatible") + raise ValueError("Attempt to set incompatible value.") self.value = value self._update() @@ -46,7 +46,7 @@ class Channel(BaseType): def load(self, value): if self.check_value(value): - raise ValueError("Tentative de charger une donnée incompatible.") + raise ValueError("Attempt to load incompatible value.") self.set(value) self._update() diff --git a/src/config/config_types/discord_types/guild.py b/src/config/config_types/discord_types/guild.py index 9c9544d..164a97f 100644 --- a/src/config/config_types/discord_types/guild.py +++ b/src/config/config_types/discord_types/guild.py @@ -79,7 +79,7 @@ class Guild(BaseType): :type value: Union[int, discord.Guild] """ if not self.check_value(value): - raise ValueError("Tentative de définir une valeur incompatible") + raise ValueError("Attempt to set incompatible value.") self.value = value self._update() @@ -138,7 +138,7 @@ class Guild(BaseType): :type value: Union[int, discord.Guild] """ if self.check_value(value): - raise ValueError("Tentative de charger une donnée incompatible.") + raise ValueError("Attempt to load incompatible value.") self.set(value) self._update() diff --git a/src/config/config_types/discord_types/role.py b/src/config/config_types/discord_types/role.py index a468b57..6c12d2b 100644 --- a/src/config/config_types/discord_types/role.py +++ b/src/config/config_types/discord_types/role.py @@ -20,7 +20,7 @@ class Role(BaseType): if self.check_value(value): self.value = value return - raise ValueError("Tentative de définir une valeur incompatible") + raise ValueError("Attempt to set incompatible value.") def get(self): return self.value @@ -30,5 +30,5 @@ class Role(BaseType): def load(self, value): if self.check_value(value): - raise ValueError("Tentative de charger une donnée incompatible.") + raise ValueError("Attempt to load incompatible value.") self.value = value diff --git a/src/config/config_types/discord_types/user.py b/src/config/config_types/discord_types/user.py index 1b98fad..420d6db 100644 --- a/src/config/config_types/discord_types/user.py +++ b/src/config/config_types/discord_types/user.py @@ -22,7 +22,7 @@ class User(BaseType): if self.check_value(value): self.value = value return - raise ValueError("Tentative de définir une valeur incompatible") + raise ValueError("Attempt to set incompatible value.") def get(self): return self.value @@ -32,5 +32,5 @@ class User(BaseType): def load(self, value): if self.check_value(value): - raise ValueError("Tentative de charger une donnée incompatible.") + raise ValueError("Attempt to load incompatible value.") self.value = value \ No newline at end of file diff --git a/src/config/config_types/float.py b/src/config/config_types/float.py index e4ecb75..42de8cd 100644 --- a/src/config/config_types/float.py +++ b/src/config/config_types/float.py @@ -98,7 +98,7 @@ class Float(BaseType): :param float value: Value to set """ if not self.check_value(value): - raise ValueError("Tentative de définir une valeur incompatible") + raise ValueError("Attempt to set incompatible value.") self.value = float(value) def get(self) -> float: @@ -146,7 +146,7 @@ class Float(BaseType): :param float value: Value to load """ if not self.check_value(value): - raise ValueError("Tentative de charger une donnée incompatible.") + raise ValueError("Attempt to load incompatible value.") self.value = value def __repr__(self): diff --git a/src/config/config_types/int.py b/src/config/config_types/int.py index 58d2dab..85c5f0e 100644 --- a/src/config/config_types/int.py +++ b/src/config/config_types/int.py @@ -117,7 +117,7 @@ class Int(BaseType): :param int value: Value to set """ if not self.check_value(value): - raise ValueError("Tentative de définir une valeur incompatible") + raise ValueError("Attempt to set incompatible value.") self.value = int(value) def get(self) -> typing.Optional[int]: @@ -165,7 +165,7 @@ class Int(BaseType): :param int value: Value to load """ if not self.check_value(value): - raise ValueError("Tentative de charger une donnée incompatible.") + raise ValueError("Attempt to load incompatible value.") self.value = value def __repr__(self): diff --git a/src/config/config_types/list.py b/src/config/config_types/list.py index d2142d2..e838d61 100644 --- a/src/config/config_types/list.py +++ b/src/config/config_types/list.py @@ -70,7 +70,7 @@ class List(BaseType): :param typing.List[typing.Any] value: Value to set """ if not self.check_value(value): - raise ValueError('Tentative de définir une valeur incompatible') + raise ValueError('Attempt to set incompatible value.') new_liste = [] for v in value: new_element = self.type_() @@ -128,7 +128,7 @@ class List(BaseType): :param typing.List[typing.Any] value: Value to load """ if not self.check_value(value): - raise ValueError("Tentative de charger une donnée incompatible.") + raise ValueError("Attempt to load incompatible value.") for v in value: new_object = self.type_() new_object.load(v) diff --git a/src/config/config_types/str.py b/src/config/config_types/str.py index 50e835b..61e73d5 100644 --- a/src/config/config_types/str.py +++ b/src/config/config_types/str.py @@ -52,7 +52,7 @@ class Str(BaseType): :return: None """ if not self.check_value(value): - raise ValueError("Tentative de définir une valeur incompatible") + raise ValueError("Attempt to set incompatible value.") self.value = str(value) def get(self) -> str: @@ -103,7 +103,7 @@ class Str(BaseType): '34' """ if not self.check_value(value): - raise ValueError("Tentative de charger une donnée incompatible.") + raise ValueError("Attempt to load incompatible value.") self.value = value def __repr__(self): From 34caac1585887e585ad42a8daf76f27117e295ef Mon Sep 17 00:00:00 2001 From: Louis Chauvet Date: Fri, 24 Apr 2020 22:52:33 +0200 Subject: [PATCH 12/15] =?UTF-8?q?Suppression=20des=20d=C3=A9pendances=20in?= =?UTF-8?q?utiles?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Pipfile | 10 +- Pipfile.lock | 458 ++++++++++++++++++++------------------------------- 2 files changed, 182 insertions(+), 286 deletions(-) diff --git a/Pipfile b/Pipfile index 0c56937..8fb42aa 100644 --- a/Pipfile +++ b/Pipfile @@ -5,19 +5,13 @@ name = "pypi" [packages] packaging = "*" -aiohttp = "*" -aiofiles = "*" -lupa = "*" -aiofile = "*" toml = "*" "discord.py" = {version = "*",extras = ["voice",]} -matplotlib = "*" -humanize = "*" + +[dev-packages] pytest = "*" sphinx = "*" sphinx-rtd-theme = "*" -[dev-packages] - [requires] python_version = "3.8" diff --git a/Pipfile.lock b/Pipfile.lock index 4b54f5d..e46b07e 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "d52eccc06eb777b155f756b9aadf63b98a045a979bd4882fa0ea63278245a435" + "sha256": "56274993c51b422f6d178e8a21bbf669b8b508facb513d6c63f3373ebf707863" }, "pipfile-spec": 6, "requires": { @@ -16,27 +16,6 @@ ] }, "default": { - "aiofile": { - "hashes": [ - "sha256:229078abbaab87adfcaad0fa7766b9b8251d42d0242deac6166da433b027ef1f", - "sha256:312d50ed7e646a40ab2a5457fdf382870aca926f956921ab8c7ab72c3922f372", - "sha256:8c50fcb42ee2bad2ae811edb972724e7f6bf3b0a6565a498f2432862b548b92d", - "sha256:a9a457654e561c396b88f70a0d5fa00e40a337853aa180bc805d9d5efb82317c", - "sha256:cef9e7bdf93db6a4c7ffe9ef0c354e2887695ec2a3a9dda8ed285005ec835616", - "sha256:d1da2fc9aa7509d29ea09617bf533bd1045f79cfdfb10ee83da90ba2212720a2", - "sha256:e43cb5e3181a8dfb73afbb4749b024e9a35a52e60ecf97d1d3db2731212cb0a0" - ], - "index": "pypi", - "version": "==1.5.2" - }, - "aiofiles": { - "hashes": [ - "sha256:377fdf7815cc611870c59cbd07b68b180841d2a2b79812d8c218be02448c2acb", - "sha256:98e6bcfd1b50f97db4980e182ddd509b7cc35909e903a8fe50d8849e02d815af" - ], - "index": "pypi", - "version": "==0.5.0" - }, "aiohttp": { "hashes": [ "sha256:1e984191d1ec186881ffaed4581092ba04f7c61582a177b187d3a2f07ed9719e", @@ -52,16 +31,8 @@ "sha256:ae55bac364c405caa23a4f2d6cfecc6a0daada500274ffca4a9230e7129eac59", "sha256:b778ce0c909a2653741cb4b1ac7015b5c130ab9c897611df43ae6a58523cb965" ], - "index": "pypi", "version": "==3.6.2" }, - "alabaster": { - "hashes": [ - "sha256:446438bdcca0e05bd45ea2de1668c1d9b032e1a9154c2c259092d77031ddd359", - "sha256:a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02" - ], - "version": "==0.7.12" - }, "async-timeout": { "hashes": [ "sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f", @@ -76,20 +47,6 @@ ], "version": "==19.3.0" }, - "babel": { - "hashes": [ - "sha256:1aac2ae2d0d8ea368fa90906567f5c08463d98ade155c0c4bfedd6a0f7160e38", - "sha256:d670ea0b10f8b723672d3a6abeb87b565b244da220d76b4dba1b66269ec152d4" - ], - "version": "==2.8.0" - }, - "certifi": { - "hashes": [ - "sha256:1d987a998c75633c40847cc966fcf5904906c920a7f17ef374f5aa4282abd304", - "sha256:51fcb31174be6e6664c5f69e3e1691a2d72a1a12e90f872cbdb1567eb47b6519" - ], - "version": "==2020.4.5.1" - }, "cffi": { "hashes": [ "sha256:001bf3242a1bb04d985d63e138230802c6c8d4db3668fb545fb5005ddf5bb5ff", @@ -130,13 +87,6 @@ ], "version": "==3.0.4" }, - "cycler": { - "hashes": [ - "sha256:1d8a5ae1ff6c5cf9b93e8811e581232ad8920aeec647c37316ceac982b08cb2d", - "sha256:cd7b2d1018258d7247a71425e9f26463dfb444d411c39569972f4ce586b0c9d8" - ], - "version": "==0.10.0" - }, "discord.py": { "extras": [ "voice" @@ -148,6 +98,184 @@ "index": "pypi", "version": "==1.3.3" }, + "idna": { + "hashes": [ + "sha256:7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb", + "sha256:a068a21ceac8a4d63dbfd964670474107f541babbd2250d61922f029858365fa" + ], + "version": "==2.9" + }, + "multidict": { + "hashes": [ + "sha256:317f96bc0950d249e96d8d29ab556d01dd38888fbe68324f46fd834b430169f1", + "sha256:42f56542166040b4474c0c608ed051732033cd821126493cf25b6c276df7dd35", + "sha256:4b7df040fb5fe826d689204f9b544af469593fb3ff3a069a6ad3409f742f5928", + "sha256:544fae9261232a97102e27a926019100a9db75bec7b37feedd74b3aa82f29969", + "sha256:620b37c3fea181dab09267cd5a84b0f23fa043beb8bc50d8474dd9694de1fa6e", + "sha256:6e6fef114741c4d7ca46da8449038ec8b1e880bbe68674c01ceeb1ac8a648e78", + "sha256:7774e9f6c9af3f12f296131453f7b81dabb7ebdb948483362f5afcaac8a826f1", + "sha256:85cb26c38c96f76b7ff38b86c9d560dea10cf3459bb5f4caf72fc1bb932c7136", + "sha256:a326f4240123a2ac66bb163eeba99578e9d63a8654a59f4688a79198f9aa10f8", + "sha256:ae402f43604e3b2bc41e8ea8b8526c7fa7139ed76b0d64fc48e28125925275b2", + "sha256:aee283c49601fa4c13adc64c09c978838a7e812f85377ae130a24d7198c0331e", + "sha256:b51249fdd2923739cd3efc95a3d6c363b67bbf779208e9f37fd5e68540d1a4d4", + "sha256:bb519becc46275c594410c6c28a8a0adc66fe24fef154a9addea54c1adb006f5", + "sha256:c2c37185fb0af79d5c117b8d2764f4321eeb12ba8c141a95d0aa8c2c1d0a11dd", + "sha256:dc561313279f9d05a3d0ffa89cd15ae477528ea37aa9795c4654588a3287a9ab", + "sha256:e439c9a10a95cb32abd708bb8be83b2134fa93790a4fb0535ca36db3dda94d20", + "sha256:fc3b4adc2ee8474cb3cd2a155305d5f8eda0a9c91320f83e55748e1fcb68f8e3" + ], + "version": "==4.7.5" + }, + "packaging": { + "hashes": [ + "sha256:3c292b474fda1671ec57d46d739d072bfd495a4f51ad01a055121d81e952b7a3", + "sha256:82f77b9bee21c1bafbf35a84905d604d5d1223801d639cf3ed140bd651c08752" + ], + "index": "pypi", + "version": "==20.3" + }, + "pycparser": { + "hashes": [ + "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0", + "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705" + ], + "version": "==2.20" + }, + "pynacl": { + "hashes": [ + "sha256:05c26f93964373fc0abe332676cb6735f0ecad27711035b9472751faa8521255", + "sha256:0c6100edd16fefd1557da078c7a31e7b7d7a52ce39fdca2bec29d4f7b6e7600c", + "sha256:0d0a8171a68edf51add1e73d2159c4bc19fc0718e79dec51166e940856c2f28e", + "sha256:1c780712b206317a746ace34c209b8c29dbfd841dfbc02aa27f2084dd3db77ae", + "sha256:2424c8b9f41aa65bbdbd7a64e73a7450ebb4aa9ddedc6a081e7afcc4c97f7621", + "sha256:2d23c04e8d709444220557ae48ed01f3f1086439f12dbf11976e849a4926db56", + "sha256:30f36a9c70450c7878053fa1344aca0145fd47d845270b43a7ee9192a051bf39", + "sha256:37aa336a317209f1bb099ad177fef0da45be36a2aa664507c5d72015f956c310", + "sha256:4943decfc5b905748f0756fdd99d4f9498d7064815c4cf3643820c9028b711d1", + "sha256:53126cd91356342dcae7e209f840212a58dcf1177ad52c1d938d428eebc9fee5", + "sha256:57ef38a65056e7800859e5ba9e6091053cd06e1038983016effaffe0efcd594a", + "sha256:5bd61e9b44c543016ce1f6aef48606280e45f892a928ca7068fba30021e9b786", + "sha256:6482d3017a0c0327a49dddc8bd1074cc730d45db2ccb09c3bac1f8f32d1eb61b", + "sha256:7d3ce02c0784b7cbcc771a2da6ea51f87e8716004512493a2b69016326301c3b", + "sha256:a14e499c0f5955dcc3991f785f3f8e2130ed504fa3a7f44009ff458ad6bdd17f", + "sha256:a39f54ccbcd2757d1d63b0ec00a00980c0b382c62865b61a505163943624ab20", + "sha256:aabb0c5232910a20eec8563503c153a8e78bbf5459490c49ab31f6adf3f3a415", + "sha256:bd4ecb473a96ad0f90c20acba4f0bf0df91a4e03a1f4dd6a4bdc9ca75aa3a715", + "sha256:bf459128feb543cfca16a95f8da31e2e65e4c5257d2f3dfa8c0c1031139c9c92", + "sha256:e2da3c13307eac601f3de04887624939aca8ee3c9488a0bb0eca4fb9401fc6b1", + "sha256:f67814c38162f4deb31f68d590771a29d5ae3b1bd64b75cf232308e5c74777e0" + ], + "version": "==1.3.0" + }, + "pyparsing": { + "hashes": [ + "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1", + "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b" + ], + "version": "==2.4.7" + }, + "six": { + "hashes": [ + "sha256:236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a", + "sha256:8f3cd2e254d8f793e7f3d6d9df77b92252b52637291d0f0da013c76ea2724b6c" + ], + "version": "==1.14.0" + }, + "toml": { + "hashes": [ + "sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c", + "sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e" + ], + "index": "pypi", + "version": "==0.10.0" + }, + "websockets": { + "hashes": [ + "sha256:0e4fb4de42701340bd2353bb2eee45314651caa6ccee80dbd5f5d5978888fed5", + "sha256:1d3f1bf059d04a4e0eb4985a887d49195e15ebabc42364f4eb564b1d065793f5", + "sha256:20891f0dddade307ffddf593c733a3fdb6b83e6f9eef85908113e628fa5a8308", + "sha256:295359a2cc78736737dd88c343cd0747546b2174b5e1adc223824bcaf3e164cb", + "sha256:2db62a9142e88535038a6bcfea70ef9447696ea77891aebb730a333a51ed559a", + "sha256:3762791ab8b38948f0c4d281c8b2ddfa99b7e510e46bd8dfa942a5fff621068c", + "sha256:3db87421956f1b0779a7564915875ba774295cc86e81bc671631379371af1170", + "sha256:3ef56fcc7b1ff90de46ccd5a687bbd13a3180132268c4254fc0fa44ecf4fc422", + "sha256:4f9f7d28ce1d8f1295717c2c25b732c2bc0645db3215cf757551c392177d7cb8", + "sha256:5c01fd846263a75bc8a2b9542606927cfad57e7282965d96b93c387622487485", + "sha256:5c65d2da8c6bce0fca2528f69f44b2f977e06954c8512a952222cea50dad430f", + "sha256:751a556205d8245ff94aeef23546a1113b1dd4f6e4d102ded66c39b99c2ce6c8", + "sha256:7ff46d441db78241f4c6c27b3868c9ae71473fe03341340d2dfdbe8d79310acc", + "sha256:965889d9f0e2a75edd81a07592d0ced54daa5b0785f57dc429c378edbcffe779", + "sha256:9b248ba3dd8a03b1a10b19efe7d4f7fa41d158fdaa95e2cf65af5a7b95a4f989", + "sha256:9bef37ee224e104a413f0780e29adb3e514a5b698aabe0d969a6ba426b8435d1", + "sha256:c1ec8db4fac31850286b7cd3b9c0e1b944204668b8eb721674916d4e28744092", + "sha256:c8a116feafdb1f84607cb3b14aa1418424ae71fee131642fc568d21423b51824", + "sha256:ce85b06a10fc65e6143518b96d3dca27b081a740bae261c2fb20375801a9d56d", + "sha256:d705f8aeecdf3262379644e4b55107a3b55860eb812b673b28d0fbc347a60c55", + "sha256:e898a0863421650f0bebac8ba40840fc02258ef4714cb7e1fd76b6a6354bda36", + "sha256:f8a7bff6e8664afc4e6c28b983845c5bc14965030e3fb98789734d416af77c4b" + ], + "version": "==8.1" + }, + "yarl": { + "hashes": [ + "sha256:0c2ab325d33f1b824734b3ef51d4d54a54e0e7a23d13b86974507602334c2cce", + "sha256:0ca2f395591bbd85ddd50a82eb1fde9c1066fafe888c5c7cc1d810cf03fd3cc6", + "sha256:2098a4b4b9d75ee352807a95cdf5f10180db903bc5b7270715c6bbe2551f64ce", + "sha256:25e66e5e2007c7a39541ca13b559cd8ebc2ad8fe00ea94a2aad28a9b1e44e5ae", + "sha256:26d7c90cb04dee1665282a5d1a998defc1a9e012fdca0f33396f81508f49696d", + "sha256:308b98b0c8cd1dfef1a0311dc5e38ae8f9b58349226aa0533f15a16717ad702f", + "sha256:3ce3d4f7c6b69c4e4f0704b32eca8123b9c58ae91af740481aa57d7857b5e41b", + "sha256:58cd9c469eced558cd81aa3f484b2924e8897049e06889e8ff2510435b7ef74b", + "sha256:5b10eb0e7f044cf0b035112446b26a3a2946bca9d7d7edb5e54a2ad2f6652abb", + "sha256:6faa19d3824c21bcbfdfce5171e193c8b4ddafdf0ac3f129ccf0cdfcb083e462", + "sha256:944494be42fa630134bf907714d40207e646fd5a94423c90d5b514f7b0713fea", + "sha256:a161de7e50224e8e3de6e184707476b5a989037dcb24292b391a3d66ff158e70", + "sha256:a4844ebb2be14768f7994f2017f70aca39d658a96c786211be5ddbe1c68794c1", + "sha256:c2b509ac3d4b988ae8769901c66345425e361d518aecbe4acbfc2567e416626a", + "sha256:c9959d49a77b0e07559e579f38b2f3711c2b8716b8410b320bf9713013215a1b", + "sha256:d8cdee92bc930d8b09d8bd2043cedd544d9c8bd7436a77678dd602467a993080", + "sha256:e15199cdb423316e15f108f51249e44eb156ae5dba232cb73be555324a1d49c2" + ], + "version": "==1.4.2" + } + }, + "develop": { + "alabaster": { + "hashes": [ + "sha256:446438bdcca0e05bd45ea2de1668c1d9b032e1a9154c2c259092d77031ddd359", + "sha256:a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02" + ], + "version": "==0.7.12" + }, + "attrs": { + "hashes": [ + "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c", + "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72" + ], + "version": "==19.3.0" + }, + "babel": { + "hashes": [ + "sha256:1aac2ae2d0d8ea368fa90906567f5c08463d98ade155c0c4bfedd6a0f7160e38", + "sha256:d670ea0b10f8b723672d3a6abeb87b565b244da220d76b4dba1b66269ec152d4" + ], + "version": "==2.8.0" + }, + "certifi": { + "hashes": [ + "sha256:1d987a998c75633c40847cc966fcf5904906c920a7f17ef374f5aa4282abd304", + "sha256:51fcb31174be6e6664c5f69e3e1691a2d72a1a12e90f872cbdb1567eb47b6519" + ], + "version": "==2020.4.5.1" + }, + "chardet": { + "hashes": [ + "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", + "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" + ], + "version": "==3.0.4" + }, "docutils": { "hashes": [ "sha256:0c5b78adfbf7762415433f5515cd5c9e762339e23369dbe8000d84a4bf4ab3af", @@ -155,14 +283,6 @@ ], "version": "==0.16" }, - "humanize": { - "hashes": [ - "sha256:98b7ac9d1a70ad62175c8e0dd44beebbd92418727fc4e214468dfb2baa8ebfb5", - "sha256:bc2a1ff065977011de2bc36197a4b14730c54bfc46ab12a153376684573a2dab" - ], - "index": "pypi", - "version": "==2.3.0" - }, "idna": { "hashes": [ "sha256:7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb", @@ -184,58 +304,6 @@ ], "version": "==2.11.2" }, - "kiwisolver": { - "hashes": [ - "sha256:03662cbd3e6729f341a97dd2690b271e51a67a68322affab12a5b011344b973c", - "sha256:18d749f3e56c0480dccd1714230da0f328e6e4accf188dd4e6884bdd06bf02dd", - "sha256:247800260cd38160c362d211dcaf4ed0f7816afb5efe56544748b21d6ad6d17f", - "sha256:443c2320520eda0a5b930b2725b26f6175ca4453c61f739fef7a5847bd262f74", - "sha256:4eadb361baf3069f278b055e3bb53fa189cea2fd02cb2c353b7a99ebb4477ef1", - "sha256:556da0a5f60f6486ec4969abbc1dd83cf9b5c2deadc8288508e55c0f5f87d29c", - "sha256:603162139684ee56bcd57acc74035fceed7dd8d732f38c0959c8bd157f913fec", - "sha256:60a78858580761fe611d22127868f3dc9f98871e6fdf0a15cc4203ed9ba6179b", - "sha256:7cc095a4661bdd8a5742aaf7c10ea9fac142d76ff1770a0f84394038126d8fc7", - "sha256:c31bc3c8e903d60a1ea31a754c72559398d91b5929fcb329b1c3a3d3f6e72113", - "sha256:c955791d80e464da3b471ab41eb65cf5a40c15ce9b001fdc5bbc241170de58ec", - "sha256:d069ef4b20b1e6b19f790d00097a5d5d2c50871b66d10075dab78938dc2ee2cf", - "sha256:d52b989dc23cdaa92582ceb4af8d5bcc94d74b2c3e64cd6785558ec6a879793e", - "sha256:e586b28354d7b6584d8973656a7954b1c69c93f708c0c07b77884f91640b7657", - "sha256:efcf3397ae1e3c3a4a0a0636542bcad5adad3b1dd3e8e629d0b6e201347176c8", - "sha256:fccefc0d36a38c57b7bd233a9b485e2f1eb71903ca7ad7adacad6c28a56d62d2" - ], - "version": "==1.2.0" - }, - "lupa": { - "hashes": [ - "sha256:09d6c45eb3b9407588c5a168e3371b629e75c5822050e9feff393601709bd0d7", - "sha256:162f6793b2ad40d25710b9998bce2eeb3938efbb4dbad49fb8c5082d214237b3", - "sha256:2551ae82ea0f90383fb153ecd29a1a166e2552e10b7a712ff047cad88062ad37", - "sha256:42285855c022b36ed3f0c5d19d0ef27b1648e0683838cddaf9191acad4d6616c", - "sha256:42fcd8f7b33b84abce90c57aaeb80d9a2ba3c3fdb4cde2fac1c8f9e4eb00d581", - "sha256:49afbeaf90c758512d3c0dea48ac0ecfa460974690cf1af58b95845e6b607c4b", - "sha256:4badf4180f8fd28e032e8716422b7a0117879569e694b5e2e803a7e39fa85213", - "sha256:517b96b23b4ce19feb54ee93d8c3b94f601a3d46cd1d570ecc5137fc7b9cb68c", - "sha256:632e7a101c288e05b823c2bae71ac69e0253e7f4120bc39b5dc1fcaf5daba0fb", - "sha256:6d65bdc251cd12b85487a1790ca1b282288be84555fe11fbe8b4357ae64708f5", - "sha256:7619fbd85d9ece1d48fb72bb7389e98d878621d2da0b7622c99066671f294b65", - "sha256:8434fdda16d101c458570d21baf9cd064304b515ed4ef9569949222ba04c3e37", - "sha256:9823322e60b0d9695754e28f5a17323d111d6951933e958cfe72df9523a39e94", - "sha256:9ee2aa3e1e852a2917c5869e8ab69d725407a218d14c4c0c98f4b04b3b2a73a7", - "sha256:a3e11d806ca02cf72e490ec1974f8b96a14a1091895c9dccebe0b8d52dd82e8e", - "sha256:a690b0bafb7e50dd8ba14a06065059b11f5c8e5961564d5d45de2d9b4a9972b1", - "sha256:a7d7761b007fbf8b524291ac42bccc32b072102e7f7e547783a5a5ded66a0c39", - "sha256:abb357c35ad1c1b78b140c8cf1fd678bcaa04bab275c6d55e47a07717138e551", - "sha256:ac7585125af7d7214e1f9dbdda965d7455c5065f71be20374c7900e01c74c05f", - "sha256:acaecd88ce6b708fbaf20b76b4d35ecb2817159f8a939b0a73d2aa840dfef850", - "sha256:ba879849832b87c18dbc471bffc62ff3393b2034a3b103348d620646575f448a", - "sha256:c57cda6ba3dc55ddd8b6c566c4f315d6152307aee23f212aa06c5e653cde4f13", - "sha256:d3cf15d0c1126373535452bdeb71b016fe970d7e5ee2bc0381df7bd35f99c820", - "sha256:d497f4727060a1daf8603e86cb731f587c38ab9a3451cd3c9c70f27859cbd3bd", - "sha256:fe1db400b471a0854fe364b63d7836973ee0d897a76628340d1721b6b4b89ddc" - ], - "index": "pypi", - "version": "==1.9" - }, "markupsafe": { "hashes": [ "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473", @@ -274,26 +342,6 @@ ], "version": "==1.1.1" }, - "matplotlib": { - "hashes": [ - "sha256:2466d4dddeb0f5666fd1e6736cc5287a4f9f7ae6c1a9e0779deff798b28e1d35", - "sha256:282b3fc8023c4365bad924d1bb442ddc565c2d1635f210b700722776da466ca3", - "sha256:4bb50ee4755271a2017b070984bcb788d483a8ce3132fab68393d1555b62d4ba", - "sha256:56d3147714da5c7ac4bc452d041e70e0e0b07c763f604110bd4e2527f320b86d", - "sha256:7a9baefad265907c6f0b037c8c35a10cf437f7708c27415a5513cf09ac6d6ddd", - "sha256:aae7d107dc37b4bb72dcc45f70394e6df2e5e92ac4079761aacd0e2ad1d3b1f7", - "sha256:af14e77829c5b5d5be11858d042d6f2459878f8e296228c7ea13ec1fd308eb68", - "sha256:c1cf735970b7cd424502719b44288b21089863aaaab099f55e0283a721aaf781", - "sha256:ce378047902b7a05546b6485b14df77b2ff207a0054e60c10b5680132090c8ee", - "sha256:d35891a86a4388b6965c2d527b9a9f9e657d9e110b0575ca8a24ba0d4e34b8fc", - "sha256:e06304686209331f99640642dee08781a9d55c6e32abb45ed54f021f46ccae47", - "sha256:e20ba7fb37d4647ac38f3c6d8672dd8b62451ee16173a0711b37ba0ce42bf37d", - "sha256:f4412241e32d0f8d3713b68d3ca6430190a5e8a7c070f1c07d7833d8c5264398", - "sha256:ffe2f9cdcea1086fc414e82f42271ecf1976700b8edd16ca9d376189c6d93aee" - ], - "index": "pypi", - "version": "==3.2.1" - }, "more-itertools": { "hashes": [ "sha256:5dd8bcf33e5f9513ffa06d5ad33d78f31e1931ac9a18f33d37e77a180d393a7c", @@ -301,54 +349,6 @@ ], "version": "==8.2.0" }, - "multidict": { - "hashes": [ - "sha256:317f96bc0950d249e96d8d29ab556d01dd38888fbe68324f46fd834b430169f1", - "sha256:42f56542166040b4474c0c608ed051732033cd821126493cf25b6c276df7dd35", - "sha256:4b7df040fb5fe826d689204f9b544af469593fb3ff3a069a6ad3409f742f5928", - "sha256:544fae9261232a97102e27a926019100a9db75bec7b37feedd74b3aa82f29969", - "sha256:620b37c3fea181dab09267cd5a84b0f23fa043beb8bc50d8474dd9694de1fa6e", - "sha256:6e6fef114741c4d7ca46da8449038ec8b1e880bbe68674c01ceeb1ac8a648e78", - "sha256:7774e9f6c9af3f12f296131453f7b81dabb7ebdb948483362f5afcaac8a826f1", - "sha256:85cb26c38c96f76b7ff38b86c9d560dea10cf3459bb5f4caf72fc1bb932c7136", - "sha256:a326f4240123a2ac66bb163eeba99578e9d63a8654a59f4688a79198f9aa10f8", - "sha256:ae402f43604e3b2bc41e8ea8b8526c7fa7139ed76b0d64fc48e28125925275b2", - "sha256:aee283c49601fa4c13adc64c09c978838a7e812f85377ae130a24d7198c0331e", - "sha256:b51249fdd2923739cd3efc95a3d6c363b67bbf779208e9f37fd5e68540d1a4d4", - "sha256:bb519becc46275c594410c6c28a8a0adc66fe24fef154a9addea54c1adb006f5", - "sha256:c2c37185fb0af79d5c117b8d2764f4321eeb12ba8c141a95d0aa8c2c1d0a11dd", - "sha256:dc561313279f9d05a3d0ffa89cd15ae477528ea37aa9795c4654588a3287a9ab", - "sha256:e439c9a10a95cb32abd708bb8be83b2134fa93790a4fb0535ca36db3dda94d20", - "sha256:fc3b4adc2ee8474cb3cd2a155305d5f8eda0a9c91320f83e55748e1fcb68f8e3" - ], - "version": "==4.7.5" - }, - "numpy": { - "hashes": [ - "sha256:0aa2b318cf81eb1693fcfcbb8007e95e231d7e1aa24288137f3b19905736c3ee", - "sha256:163c78c04f47f26ca1b21068cea25ed7c5ecafe5f5ab2ea4895656a750582b56", - "sha256:1e37626bcb8895c4b3873fcfd54e9bfc5ffec8d0f525651d6985fcc5c6b6003c", - "sha256:264fd15590b3f02a1fbc095e7e1f37cdac698ff3829e12ffdcffdce3772f9d44", - "sha256:3d9e1554cd9b5999070c467b18e5ae3ebd7369f02706a8850816f576a954295f", - "sha256:40c24960cd5cec55222963f255858a1c47c6fa50a65a5b03fd7de75e3700eaaa", - "sha256:46f404314dbec78cb342904f9596f25f9b16e7cf304030f1339e553c8e77f51c", - "sha256:4847f0c993298b82fad809ea2916d857d0073dc17b0510fbbced663b3265929d", - "sha256:48e15612a8357393d176638c8f68a19273676877caea983f8baf188bad430379", - "sha256:6725d2797c65598778409aba8cd67077bb089d5b7d3d87c2719b206dc84ec05e", - "sha256:99f0ba97e369f02a21bb95faa3a0de55991fd5f0ece2e30a9e2eaebeac238921", - "sha256:a41f303b3f9157a31ce7203e3ca757a0c40c96669e72d9b6ee1bce8507638970", - "sha256:a4305564e93f5c4584f6758149fd446df39fd1e0a8c89ca0deb3cce56106a027", - "sha256:a551d8cc267c634774830086da42e4ba157fa41dd3b93982bc9501b284b0c689", - "sha256:a6bc9432c2640b008d5f29bad737714eb3e14bb8854878eacf3d7955c4e91c36", - "sha256:c60175d011a2e551a2f74c84e21e7c982489b96b6a5e4b030ecdeacf2914da68", - "sha256:e46e2384209c91996d5ec16744234d1c906ab79a701ce1a26155c9ec890b8dc8", - "sha256:e607b8cdc2ae5d5a63cd1bec30a15b5ed583ac6a39f04b7ba0f03fcfbf29c05b", - "sha256:e94a39d5c40fffe7696009dbd11bc14a349b377e03a384ed011e03d698787dd3", - "sha256:eb2286249ebfe8fcb5b425e5ec77e4736d53ee56d3ad296f8947f67150f495e3", - "sha256:fdee7540d12519865b423af411bd60ddb513d2eb2cd921149b732854995bbf8b" - ], - "version": "==1.18.3" - }, "packaging": { "hashes": [ "sha256:3c292b474fda1671ec57d46d739d072bfd495a4f51ad01a055121d81e952b7a3", @@ -371,13 +371,6 @@ ], "version": "==1.8.1" }, - "pycparser": { - "hashes": [ - "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0", - "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705" - ], - "version": "==2.20" - }, "pygments": { "hashes": [ "sha256:647344a061c249a3b74e230c739f434d7ea4d8b1d5f3721bc0f3558049b38f44", @@ -385,32 +378,6 @@ ], "version": "==2.6.1" }, - "pynacl": { - "hashes": [ - "sha256:05c26f93964373fc0abe332676cb6735f0ecad27711035b9472751faa8521255", - "sha256:0c6100edd16fefd1557da078c7a31e7b7d7a52ce39fdca2bec29d4f7b6e7600c", - "sha256:0d0a8171a68edf51add1e73d2159c4bc19fc0718e79dec51166e940856c2f28e", - "sha256:1c780712b206317a746ace34c209b8c29dbfd841dfbc02aa27f2084dd3db77ae", - "sha256:2424c8b9f41aa65bbdbd7a64e73a7450ebb4aa9ddedc6a081e7afcc4c97f7621", - "sha256:2d23c04e8d709444220557ae48ed01f3f1086439f12dbf11976e849a4926db56", - "sha256:30f36a9c70450c7878053fa1344aca0145fd47d845270b43a7ee9192a051bf39", - "sha256:37aa336a317209f1bb099ad177fef0da45be36a2aa664507c5d72015f956c310", - "sha256:4943decfc5b905748f0756fdd99d4f9498d7064815c4cf3643820c9028b711d1", - "sha256:53126cd91356342dcae7e209f840212a58dcf1177ad52c1d938d428eebc9fee5", - "sha256:57ef38a65056e7800859e5ba9e6091053cd06e1038983016effaffe0efcd594a", - "sha256:5bd61e9b44c543016ce1f6aef48606280e45f892a928ca7068fba30021e9b786", - "sha256:6482d3017a0c0327a49dddc8bd1074cc730d45db2ccb09c3bac1f8f32d1eb61b", - "sha256:7d3ce02c0784b7cbcc771a2da6ea51f87e8716004512493a2b69016326301c3b", - "sha256:a14e499c0f5955dcc3991f785f3f8e2130ed504fa3a7f44009ff458ad6bdd17f", - "sha256:a39f54ccbcd2757d1d63b0ec00a00980c0b382c62865b61a505163943624ab20", - "sha256:aabb0c5232910a20eec8563503c153a8e78bbf5459490c49ab31f6adf3f3a415", - "sha256:bd4ecb473a96ad0f90c20acba4f0bf0df91a4e03a1f4dd6a4bdc9ca75aa3a715", - "sha256:bf459128feb543cfca16a95f8da31e2e65e4c5257d2f3dfa8c0c1031139c9c92", - "sha256:e2da3c13307eac601f3de04887624939aca8ee3c9488a0bb0eca4fb9401fc6b1", - "sha256:f67814c38162f4deb31f68d590771a29d5ae3b1bd64b75cf232308e5c74777e0" - ], - "version": "==1.3.0" - }, "pyparsing": { "hashes": [ "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1", @@ -426,13 +393,6 @@ "index": "pypi", "version": "==5.4.1" }, - "python-dateutil": { - "hashes": [ - "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c", - "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a" - ], - "version": "==2.8.1" - }, "pytz": { "hashes": [ "sha256:1c557d7d0e871de1f5ccd5833f60fb2550652da6be2693c1e02300743d21500d", @@ -519,14 +479,6 @@ ], "version": "==1.1.4" }, - "toml": { - "hashes": [ - "sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c", - "sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e" - ], - "index": "pypi", - "version": "==0.10.0" - }, "urllib3": { "hashes": [ "sha256:3018294ebefce6572a474f0604c2021e33b3fd8006ecd11d62107a5d2a963527", @@ -540,56 +492,6 @@ "sha256:ee73862862a156bf77ff92b09034fc4825dd3af9cf81bc5b360668d425f3c5f1" ], "version": "==0.1.9" - }, - "websockets": { - "hashes": [ - "sha256:0e4fb4de42701340bd2353bb2eee45314651caa6ccee80dbd5f5d5978888fed5", - "sha256:1d3f1bf059d04a4e0eb4985a887d49195e15ebabc42364f4eb564b1d065793f5", - "sha256:20891f0dddade307ffddf593c733a3fdb6b83e6f9eef85908113e628fa5a8308", - "sha256:295359a2cc78736737dd88c343cd0747546b2174b5e1adc223824bcaf3e164cb", - "sha256:2db62a9142e88535038a6bcfea70ef9447696ea77891aebb730a333a51ed559a", - "sha256:3762791ab8b38948f0c4d281c8b2ddfa99b7e510e46bd8dfa942a5fff621068c", - "sha256:3db87421956f1b0779a7564915875ba774295cc86e81bc671631379371af1170", - "sha256:3ef56fcc7b1ff90de46ccd5a687bbd13a3180132268c4254fc0fa44ecf4fc422", - "sha256:4f9f7d28ce1d8f1295717c2c25b732c2bc0645db3215cf757551c392177d7cb8", - "sha256:5c01fd846263a75bc8a2b9542606927cfad57e7282965d96b93c387622487485", - "sha256:5c65d2da8c6bce0fca2528f69f44b2f977e06954c8512a952222cea50dad430f", - "sha256:751a556205d8245ff94aeef23546a1113b1dd4f6e4d102ded66c39b99c2ce6c8", - "sha256:7ff46d441db78241f4c6c27b3868c9ae71473fe03341340d2dfdbe8d79310acc", - "sha256:965889d9f0e2a75edd81a07592d0ced54daa5b0785f57dc429c378edbcffe779", - "sha256:9b248ba3dd8a03b1a10b19efe7d4f7fa41d158fdaa95e2cf65af5a7b95a4f989", - "sha256:9bef37ee224e104a413f0780e29adb3e514a5b698aabe0d969a6ba426b8435d1", - "sha256:c1ec8db4fac31850286b7cd3b9c0e1b944204668b8eb721674916d4e28744092", - "sha256:c8a116feafdb1f84607cb3b14aa1418424ae71fee131642fc568d21423b51824", - "sha256:ce85b06a10fc65e6143518b96d3dca27b081a740bae261c2fb20375801a9d56d", - "sha256:d705f8aeecdf3262379644e4b55107a3b55860eb812b673b28d0fbc347a60c55", - "sha256:e898a0863421650f0bebac8ba40840fc02258ef4714cb7e1fd76b6a6354bda36", - "sha256:f8a7bff6e8664afc4e6c28b983845c5bc14965030e3fb98789734d416af77c4b" - ], - "version": "==8.1" - }, - "yarl": { - "hashes": [ - "sha256:0c2ab325d33f1b824734b3ef51d4d54a54e0e7a23d13b86974507602334c2cce", - "sha256:0ca2f395591bbd85ddd50a82eb1fde9c1066fafe888c5c7cc1d810cf03fd3cc6", - "sha256:2098a4b4b9d75ee352807a95cdf5f10180db903bc5b7270715c6bbe2551f64ce", - "sha256:25e66e5e2007c7a39541ca13b559cd8ebc2ad8fe00ea94a2aad28a9b1e44e5ae", - "sha256:26d7c90cb04dee1665282a5d1a998defc1a9e012fdca0f33396f81508f49696d", - "sha256:308b98b0c8cd1dfef1a0311dc5e38ae8f9b58349226aa0533f15a16717ad702f", - "sha256:3ce3d4f7c6b69c4e4f0704b32eca8123b9c58ae91af740481aa57d7857b5e41b", - "sha256:58cd9c469eced558cd81aa3f484b2924e8897049e06889e8ff2510435b7ef74b", - "sha256:5b10eb0e7f044cf0b035112446b26a3a2946bca9d7d7edb5e54a2ad2f6652abb", - "sha256:6faa19d3824c21bcbfdfce5171e193c8b4ddafdf0ac3f129ccf0cdfcb083e462", - "sha256:944494be42fa630134bf907714d40207e646fd5a94423c90d5b514f7b0713fea", - "sha256:a161de7e50224e8e3de6e184707476b5a989037dcb24292b391a3d66ff158e70", - "sha256:a4844ebb2be14768f7994f2017f70aca39d658a96c786211be5ddbe1c68794c1", - "sha256:c2b509ac3d4b988ae8769901c66345425e361d518aecbe4acbfc2567e416626a", - "sha256:c9959d49a77b0e07559e579f38b2f3711c2b8716b8410b320bf9713013215a1b", - "sha256:d8cdee92bc930d8b09d8bd2043cedd544d9c8bd7436a77678dd602467a993080", - "sha256:e15199cdb423316e15f108f51249e44eb156ae5dba232cb73be555324a1d49c2" - ], - "version": "==1.4.2" } - }, - "develop": {} + } } From c17f2a2db6351a1336b1639dbf82055f96cf4117 Mon Sep 17 00:00:00 2001 From: Louis Chauvet Date: Fri, 24 Apr 2020 22:53:57 +0200 Subject: [PATCH 13/15] =?UTF-8?q?[config]=20Ajout=20de=20la=20doc=20pour?= =?UTF-8?q?=20Channel=20[config]=20R=C3=A9paration=20de=20Channel=20et=20G?= =?UTF-8?q?uild?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../config_types/discord_types/channel.py | 98 ++++++++++++++++++- .../config_types/discord_types/guild.py | 2 + 2 files changed, 98 insertions(+), 2 deletions(-) diff --git a/src/config/config_types/discord_types/channel.py b/src/config/config_types/discord_types/channel.py index b29c7fc..95898b4 100644 --- a/src/config/config_types/discord_types/channel.py +++ b/src/config/config_types/discord_types/channel.py @@ -11,16 +11,48 @@ if typing.TYPE_CHECKING: class Channel(BaseType): + #: :class:`BotBase`: Client instance for checking client: BotBase + #: :class:`typing.Optional` [:class:`int`]: Current channel id + value: int + #: :class:`typing.Optional` [:class:`discord.TextChannel`]: Current guild instance + channel_instance: typing.Optional[discord.TextChannel] def __init__(self, client): + """ + Base Channel type for config. + + :param BotBase client: Client instance + + :Basic usage: + + >>> Channel(client) #doctest: +SKIP + + """ self.value = 0 self.channel_instance = None self.client = client - def check_value(self, value): + def check_value(self, value: typing.Union[int, discord.TextChannel]) -> bool: + """ + Check if value is correct + + If bot is not connected, always True + + :Basic usage: + + >>> my_channel = Channel(client) #doctest: +SKIP + >>> my_channel.check_value(invalid_id_or_channel) #doctest: +SKIP + False + >>> my_channel.check_value(valid_id_or_channel) #doctest: +SKIP + True + + :param value: Value to test + :type value: Union[int, discord.TextChannel] + :return: True if guild exists + """ id = value - if isinstance(value, discord.Guild): + if isinstance(value, discord.TextChannel): id = value.id if not self.client.is_ready(): self.client.warning(f"No check for channel {value} because client is not initialized!") @@ -30,21 +62,83 @@ class Channel(BaseType): return True def set(self, value): + """ + Set value of parameter + + :Basic usage: + + >>> my_channel = Channel(client) #doctest: +SKIP + >>> my_channel.set(valid_id_or_channel) #doctest: +SKIP + >>> my_channel.set(invalid_id_or_channel) #doctest: +SKIP +IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + ValueError: ... + + :raise ValueError: if attempt to set invalid value + :param value: value to set + :type value: Union[int, discord.TextChannel] + """ if not self.check_value(value): raise ValueError("Attempt to set incompatible value.") + if isinstance(value, discord.TextChannel): + value = value.id self.value = value self._update() def get(self): + """ + Get value of parameter + + :Basic usage: + + >>> my_channel = Channel(client) #doctest: +SKIP + >>> my_channel.set(valid_id_or_channel) #doctest: +SKIP + >>> my_channel.get() #doctest: +SKIP + + + If client is not connected: + >>> my_channel = Channel(client) #doctest: +SKIP + >>> my_channel.set(valid_id_or_channel) #doctest: +SKIP + >>> my_channel.get() #doctest: +SKIP + 23411424132412 + + :return: Guild object if client is connected, else id + :rtype: Union[int, discord.Guild] + """ if self.channel_instance is None: self._update() return self.channel_instance or self.value def to_save(self): + """ + Return id of channel + + :Basic usage: + >>> my_channel = Channel(client) #doctest: +SKIP + >>> my_channel.set(valid_id_or_channel) #doctest: +SKIP + >>> my_channel.to_save() #doctest: +SKIP + 123412412421 + + :return: Current id + :rtype: Optional[int] + """ return self.value or 0 def load(self, value): + """ + Load value from config + :Basic usage: + + >>> my_channel = Channel(client) #doctest: +SKIP + >>> my_channel.set(valid_id) #doctest: +SKIP + >>> my_channel.set(invalid_id) #doctest: +SKIP +IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + ValueError: ... + + :raise ValueError: if attempt to set invalid value + :param value: value to set + :type value: Union[int, discord.TextChannel] + """ if self.check_value(value): raise ValueError("Attempt to load incompatible value.") self.set(value) diff --git a/src/config/config_types/discord_types/guild.py b/src/config/config_types/discord_types/guild.py index 164a97f..f7df824 100644 --- a/src/config/config_types/discord_types/guild.py +++ b/src/config/config_types/discord_types/guild.py @@ -80,6 +80,8 @@ class Guild(BaseType): """ if not self.check_value(value): raise ValueError("Attempt to set incompatible value.") + if isinstance(value, discord.Guild): + value = value.id self.value = value self._update() From dcd36dd13bf61a236c2c588a46e01400fad7e249 Mon Sep 17 00:00:00 2001 From: Louis Chauvet Date: Fri, 24 Apr 2020 23:14:06 +0200 Subject: [PATCH 14/15] [config] Ajout de role --- src/bot_base/__init__.py | 1 + .../config_types/discord_types/channel.py | 18 ++- .../config_types/discord_types/guild.py | 10 +- src/config/config_types/discord_types/role.py | 146 ++++++++++++++++-- 4 files changed, 148 insertions(+), 27 deletions(-) diff --git a/src/bot_base/__init__.py b/src/bot_base/__init__.py index e69de29..331c692 100644 --- a/src/bot_base/__init__.py +++ b/src/bot_base/__init__.py @@ -0,0 +1 @@ +import bot_base \ No newline at end of file diff --git a/src/config/config_types/discord_types/channel.py b/src/config/config_types/discord_types/channel.py index 95898b4..cd9bf21 100644 --- a/src/config/config_types/discord_types/channel.py +++ b/src/config/config_types/discord_types/channel.py @@ -18,7 +18,7 @@ class Channel(BaseType): #: :class:`typing.Optional` [:class:`discord.TextChannel`]: Current guild instance channel_instance: typing.Optional[discord.TextChannel] - def __init__(self, client): + def __init__(self, client: BotBase) -> None: """ Base Channel type for config. @@ -61,7 +61,7 @@ class Channel(BaseType): return True return True - def set(self, value): + def set(self, value: typing.Union[int, discord.TextChannel]): """ Set value of parameter @@ -84,7 +84,7 @@ class Channel(BaseType): self.value = value self._update() - def get(self): + def get(self) -> typing.Union[int, discord.Channel]: """ Get value of parameter @@ -101,14 +101,14 @@ class Channel(BaseType): >>> my_channel.get() #doctest: +SKIP 23411424132412 - :return: Guild object if client is connected, else id - :rtype: Union[int, discord.Guild] + :return: Channel object if client is connected, else id + :rtype: Union[int, discord.Channel] """ if self.channel_instance is None: self._update() return self.channel_instance or self.value - def to_save(self): + def to_save(self) -> int: """ Return id of channel @@ -119,11 +119,11 @@ class Channel(BaseType): 123412412421 :return: Current id - :rtype: Optional[int] + :rtype: int """ return self.value or 0 - def load(self, value): + def load(self, value: typing.Union[int, discord.Channel]) -> None: """ Load value from config @@ -150,3 +150,5 @@ class Channel(BaseType): else: self.channel_instance = None + def __repr__(self): + return f'' diff --git a/src/config/config_types/discord_types/guild.py b/src/config/config_types/discord_types/guild.py index f7df824..66a74f2 100644 --- a/src/config/config_types/discord_types/guild.py +++ b/src/config/config_types/discord_types/guild.py @@ -62,7 +62,7 @@ class Guild(BaseType): return True return True - def set(self, value: typing.Union[int, discord.Guild]): + def set(self, value: typing.Union[int, discord.Guild]) -> None: """ Set value of parameter @@ -108,7 +108,7 @@ class Guild(BaseType): self._update() return self.guild_instance or self.value - def to_save(self) -> typing.Optional: + def to_save(self) -> int: """ Return id of guild @@ -119,11 +119,11 @@ class Guild(BaseType): 123412412421 :return: Current id - :rtype: Optional[int] + :rtype: int """ return self.value or 0 - def load(self, value): + def load(self, value: typing.Union[int, discord.Guild]) -> None: """ Load value from config @@ -145,7 +145,7 @@ class Guild(BaseType): self._update() def __repr__(self): - return f'' + return f'' def _update(self): if self.client.is_ready() and self.guild_instance is None: diff --git a/src/config/config_types/discord_types/role.py b/src/config/config_types/discord_types/role.py index 6c12d2b..17d72d0 100644 --- a/src/config/config_types/discord_types/role.py +++ b/src/config/config_types/discord_types/role.py @@ -1,34 +1,152 @@ from __future__ import annotations import typing +import discord + from config.config_types.base_type import BaseType if typing.TYPE_CHECKING: from bot_base import BotBase class Role(BaseType): + #: :class:`BotBase`: Client instance for checking client: BotBase + #: :class:`typing.Optional` [:class:`int`]: Current channel id + value: int + #: :class:`typing.Optional` [:class:`discord.Role`]: Current guild instance + role_instance: typing.Optional[discord.Role] - def __init__(self, client): - self.value = None + def __init__(self, client: BotBase) -> None: + """ + Base Role type for config. + + :param BotBase client: Client instance + + :Basic usage: + + >>> Role(client) #doctest: +SKIP + + """ + self.value = 0 + self.role_instance = None self.client = client - def check_value(self, value): + def check_value(self, value: typing.Union[int, discord.Role]) -> bool: + """ + Check if value is correct + + If bot is not connected, always True + + :Basic usage: + + >>> my_role = Role(client) #doctest: +SKIP + >>> my_role.check_value(invalid_id_or_role) #doctest: +SKIP + False + >>> my_role.check_value(valid_id_or_role) #doctest: +SKIP + True + + :param value: Value to test + :type value: Union[int, discord.Role] + :return: True if guild exists + """ + id = value + if isinstance(value, discord.TextChannel): + id = value.id + if not self.client.is_ready(): + self.client.warning(f"No check for channel {value} because client is not initialized!") + return True + if self.client.get_channel(id): + return True return True - def set(self, value): - if self.check_value(value): - self.value = value - return - raise ValueError("Attempt to set incompatible value.") + def set(self, value: typing.Union[int, discord.Role]) -> None: + """ + Set value of parameter - def get(self): - return self.value + :Basic usage: - def to_save(self): - return self.value + >>> my_role = Role(client) #doctest: +SKIP + >>> my_role.set(valid_id_or_role) #doctest: +SKIP + >>> my_role.set(invalid_id_or_role) #doctest: +SKIP +IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + ValueError: ... - def load(self, value): + :raise ValueError: if attempt to set invalid value + :param value: value to set + :type value: Union[int, discord.Role] + """ + if not self.check_value(value): + raise ValueError("Attempt to set incompatible value.") + if isinstance(value, discord.Role): + value = value.id + self.value = value + self._update() + + def get(self) -> typing.Union[int, discord.Role]: + """ + Get value of parameter + + :Basic usage: + + >>> my_role = Role(client) #doctest: +SKIP + >>> my_role.set(valid_id_or_role) #doctest: +SKIP + >>> my_role.get() #doctest: +SKIP + + + If client is not connected: + >>> my_role = Role(client) #doctest: +SKIP + >>> my_role.set(valid_id_or_role) #doctest: +SKIP + >>> my_role.get() #doctest: +SKIP + 23411424132412 + + :return: Role object if client is connected, else id + :rtype: Union[int, discord.Guild] + """ + if self.channel_instance is None: + self._update() + return self.channel_instance or self.value + + def to_save(self) -> int: + """ + Return id of channel + + :Basic usage: + >>> my_role = Role(client) #doctest: +SKIP + >>> my_role.set(valid_id_or_role) #doctest: +SKIP + >>> my_role.to_save() #doctest: +SKIP + 123412412421 + + :return: Current id + :rtype: int + """ + return self.value or 0 + + def load(self, value: typing.Union[int, discord.Role]) -> None: + """ + Load value from config + + :Basic usage: + + >>> my_role = Role(client) #doctest: +SKIP + >>> my_role.set(valid_id) #doctest: +SKIP + >>> my_role.set(invalid_id) #doctest: +SKIP +IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + ValueError: ... + + :raise ValueError: if attempt to set invalid value + :param value: value to set + :type value: Union[int, discord.Role] + """ if self.check_value(value): raise ValueError("Attempt to load incompatible value.") - self.value = value + self.set(value) + self._update() + + def _update(self): + if self.client.is_ready() and self.role_instance is None: + self.channel_instance = self.client.get_role(self.value) + else: + self.channel_instance = None + + def __repr__(self): + return f'' \ No newline at end of file From 6a9f46f32a1b1b97a6b64ed3e1f98b7a6fcb40a5 Mon Sep 17 00:00:00 2001 From: Louis Chauvet Date: Fri, 24 Apr 2020 23:25:44 +0200 Subject: [PATCH 15/15] =?UTF-8?q?[config]=20Normalement=20c'est=20bon,=20u?= =?UTF-8?q?ser=20ajout=C3=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../config_types/discord_types/channel.py | 7 +- .../config_types/discord_types/guild.py | 1 + src/config/config_types/discord_types/role.py | 29 ++-- src/config/config_types/discord_types/user.py | 151 ++++++++++++++++-- 4 files changed, 154 insertions(+), 34 deletions(-) diff --git a/src/config/config_types/discord_types/channel.py b/src/config/config_types/discord_types/channel.py index cd9bf21..6563b1f 100644 --- a/src/config/config_types/discord_types/channel.py +++ b/src/config/config_types/discord_types/channel.py @@ -15,7 +15,7 @@ class Channel(BaseType): client: BotBase #: :class:`typing.Optional` [:class:`int`]: Current channel id value: int - #: :class:`typing.Optional` [:class:`discord.TextChannel`]: Current guild instance + #: :class:`typing.Optional` [:class:`discord.TextChannel`]: Current channel instance channel_instance: typing.Optional[discord.TextChannel] def __init__(self, client: BotBase) -> None: @@ -49,7 +49,7 @@ class Channel(BaseType): :param value: Value to test :type value: Union[int, discord.TextChannel] - :return: True if guild exists + :return: True if channel exists """ id = value if isinstance(value, discord.TextChannel): @@ -93,7 +93,7 @@ class Channel(BaseType): >>> my_channel = Channel(client) #doctest: +SKIP >>> my_channel.set(valid_id_or_channel) #doctest: +SKIP >>> my_channel.get() #doctest: +SKIP - + If client is not connected: >>> my_channel = Channel(client) #doctest: +SKIP @@ -113,6 +113,7 @@ class Channel(BaseType): Return id of channel :Basic usage: + >>> my_channel = Channel(client) #doctest: +SKIP >>> my_channel.set(valid_id_or_channel) #doctest: +SKIP >>> my_channel.to_save() #doctest: +SKIP diff --git a/src/config/config_types/discord_types/guild.py b/src/config/config_types/discord_types/guild.py index 66a74f2..2ace07d 100644 --- a/src/config/config_types/discord_types/guild.py +++ b/src/config/config_types/discord_types/guild.py @@ -113,6 +113,7 @@ class Guild(BaseType): Return id of guild :Basic usage: + >>> my_guild = Guild(client) #doctest: +SKIP >>> my_guild.set(valid_id_or_guild) #doctest: +SKIP >>> my_guild.to_save() #doctest: +SKIP diff --git a/src/config/config_types/discord_types/role.py b/src/config/config_types/discord_types/role.py index 17d72d0..4c19d61 100644 --- a/src/config/config_types/discord_types/role.py +++ b/src/config/config_types/discord_types/role.py @@ -11,9 +11,9 @@ if typing.TYPE_CHECKING: class Role(BaseType): #: :class:`BotBase`: Client instance for checking client: BotBase - #: :class:`typing.Optional` [:class:`int`]: Current channel id + #: :class:`typing.Optional` [:class:`int`]: Current role id value: int - #: :class:`typing.Optional` [:class:`discord.Role`]: Current guild instance + #: :class:`typing.Optional` [:class:`discord.Role`]: Current role instance role_instance: typing.Optional[discord.Role] def __init__(self, client: BotBase) -> None: @@ -47,15 +47,15 @@ class Role(BaseType): :param value: Value to test :type value: Union[int, discord.Role] - :return: True if guild exists + :return: True if role exists """ id = value - if isinstance(value, discord.TextChannel): + if isinstance(value, discord.Role): id = value.id if not self.client.is_ready(): - self.client.warning(f"No check for channel {value} because client is not initialized!") + self.client.warning(f"No check for role {value} because client is not initialized!") return True - if self.client.get_channel(id): + if self.client.get_role(id): return True return True @@ -91,7 +91,7 @@ class Role(BaseType): >>> my_role = Role(client) #doctest: +SKIP >>> my_role.set(valid_id_or_role) #doctest: +SKIP >>> my_role.get() #doctest: +SKIP - + If client is not connected: >>> my_role = Role(client) #doctest: +SKIP @@ -100,17 +100,18 @@ class Role(BaseType): 23411424132412 :return: Role object if client is connected, else id - :rtype: Union[int, discord.Guild] + :rtype: Union[int, discord.Role] """ - if self.channel_instance is None: + if self.role_instance is None: self._update() - return self.channel_instance or self.value + return self.role_instance or self.value def to_save(self) -> int: """ - Return id of channel + Return id of role :Basic usage: + >>> my_role = Role(client) #doctest: +SKIP >>> my_role.set(valid_id_or_role) #doctest: +SKIP >>> my_role.to_save() #doctest: +SKIP @@ -144,9 +145,9 @@ class Role(BaseType): def _update(self): if self.client.is_ready() and self.role_instance is None: - self.channel_instance = self.client.get_role(self.value) + self.role_instance = self.client.get_role(self.value) else: - self.channel_instance = None + self.role_instance = None def __repr__(self): - return f'' \ No newline at end of file + return f'' \ No newline at end of file diff --git a/src/config/config_types/discord_types/user.py b/src/config/config_types/discord_types/user.py index 420d6db..6aa146d 100644 --- a/src/config/config_types/discord_types/user.py +++ b/src/config/config_types/discord_types/user.py @@ -1,36 +1,153 @@ from __future__ import annotations - import typing -from config.config_types.base_type import BaseType +import discord +from config.config_types.base_type import BaseType if typing.TYPE_CHECKING: from bot_base import BotBase + class User(BaseType): - + #: :class:`BotBase`: Client instance for checking client: BotBase + #: :class:`typing.Optional` [:class:`int`]: Current user id + value: int + #: :class:`typing.Optional` [:class:`discord.User`]: Current user instance + user_instance: typing.Optional[discord.User] - def __init__(self, client): - self.value = None + def __init__(self, client: BotBase) -> None: + """ + Base User type for config. + + :param BotBase client: Client instance + + :Basic usage: + + >>> User(client) #doctest: +SKIP + + """ + self.value = 0 + self.user_instance = None self.client = client - def check_value(self, value): + def check_value(self, value: typing.Union[int, discord.User]) -> bool: + """ + Check if value is correct + + If bot is not connected, always True + + :Basic usage: + + >>> my_user = User(client) #doctest: +SKIP + >>> my_user.check_value(invalid_id_or_user) #doctest: +SKIP + False + >>> my_user.check_value(valid_id_or_user) #doctest: +SKIP + True + + :param value: Value to test + :type value: Union[int, discord.User] + :return: True if user exists + """ + id = value + if isinstance(value, discord.User): + id = value.id + if not self.client.is_ready(): + self.client.warning(f"No check for user {value} because client is not initialized!") + return True + if self.client.get_user(id): + return True return True - def set(self, value): - if self.check_value(value): - self.value = value - return - raise ValueError("Attempt to set incompatible value.") + def set(self, value: typing.Union[int, discord.User]) -> None: + """ + Set value of parameter - def get(self): - return self.value + :Basic usage: - def to_save(self): - return self.value + >>> my_user = User(client) #doctest: +SKIP + >>> my_user.set(valid_id_or_user) #doctest: +SKIP + >>> my_user.set(invalid_id_or_user) #doctest: +SKIP +IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + ValueError: ... - def load(self, value): + :raise ValueError: if attempt to set invalid value + :param value: value to set + :type value: Union[int, discord.User] + """ + if not self.check_value(value): + raise ValueError("Attempt to set incompatible value.") + if isinstance(value, discord.User): + value = value.id + self.value = value + self._update() + + def get(self) -> typing.Union[int, discord.User]: + """ + Get value of parameter + + :Basic usage: + + >>> my_user = User(client) #doctest: +SKIP + >>> my_user.set(valid_id_or_user) #doctest: +SKIP + >>> my_user.get() #doctest: +SKIP + + + If client is not connected: + >>> my_user = User(client) #doctest: +SKIP + >>> my_user.set(valid_id_or_user) #doctest: +SKIP + >>> my_user.get() #doctest: +SKIP + 23411424132412 + + :return: User object if client is connected, else id + :rtype: Union[int, discord.User] + """ + if self.user_instance is None: + self._update() + return self.user_instance or self.value + + def to_save(self) -> int: + """ + Return id of user + + :Basic usage: + + >>> my_user = User(client) #doctest: +SKIP + >>> my_user.set(valid_id_or_user) #doctest: +SKIP + >>> my_user.to_save() #doctest: +SKIP + 123412412421 + + :return: Current id + :rtype: int + """ + return self.value or 0 + + def load(self, value: typing.Union[int, discord.User]) -> None: + """ + Load value from config + + :Basic usage: + + >>> my_user = User(client) #doctest: +SKIP + >>> my_user.set(valid_id) #doctest: +SKIP + >>> my_user.set(invalid_id) #doctest: +SKIP +IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + ValueError: ... + + :raise ValueError: if attempt to set invalid value + :param value: value to set + :type value: Union[int, discord.User] + """ if self.check_value(value): raise ValueError("Attempt to load incompatible value.") - self.value = value \ No newline at end of file + self.set(value) + self._update() + + def _update(self): + if self.client.is_ready() and self.user_instance is None: + self.user_instance = self.client.get_user(self.value) + else: + self.user_instance = None + + def __repr__(self): + return f'' \ No newline at end of file