[bot-base] Ajout de la doc

[config] Début de la réécriture, pour l'instant ca casse tous les modules, je répare demain, écriture d'une partie de la doc
[utils/emojis] Passage à la notation unicode, j'en ai marre des doubles caractères, écriture de la doc
This commit is contained in:
Louis Chauvet 2020-04-14 02:31:36 +02:00
parent 9f29e13938
commit 903d43efb3
Signed by: fomys
GPG Key ID: 1ECA046A9615ABA0
40 changed files with 1139 additions and 160 deletions

20
Makefile Normal file
View File

@ -0,0 +1,20 @@
# Minimal makefile for Sphinx documentation
#
# You can set these variables from the command line, and also
# from the environment for the first two.
SPHINXOPTS ?=
SPHINXBUILD ?= sphinx-build
SOURCEDIR = source
BUILDDIR = build
# Put it first so that "make" without argument is like "make help".
help:
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
.PHONY: help Makefile
# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)

View File

@ -1,88 +0,0 @@
from __future__ import annotations
from typing import Dict, Any, Optional
class Config:
name: Optional[str]
parent: Optional[Config]
config: Dict[Any, Any]
def __init__(self, parent: Config = None, name: str = None):
"""Create Config
:param parent: Parent configuration
:param name: Configuration name
:type parent: Config
:type name: str"""
self.parent = parent
self.config = dict()
self.name = None
if self.parent:
self.name = name
def init(self, config):
"""Load default configuration
:param config: Default configuration
:type config: dict
:return: None"""
# Load data from config file before initialisation
self.load()
# Get data from parent
if self.parent is not None:
self.parent.config[self.name] = self.parent.config.get(self.name) if self.parent.config.get(
self.name) is not None else self.config
self.config = self.parent.config[self.name]
# Set config only if not already defined
for k, v in config.items():
self.config[k] = self.config.get(k) if self.config.get(k) is not None else v
# Save new datas
self.save()
def _save(self):
"""Internal function for save
Must be overridden by all type of config to handle saving"""
# Call parent save if necessary
if self.parent:
self.parent.save()
def save(self):
"""Public save function
Do not override"""
self._save()
def _load(self):
"""Internal function for load
Mus be overridden by all type of config to handle loading"""
# Load parent if necessary
if self.parent:
self.parent.load()
self.config = self.parent.config.get(self.name)
# Initialize parent if necessary
if self.config is None:
self.parent.config[self.name] = {}
self.config = {}
def load(self):
"""Public load function
Do not override"""
self._load()
def __getattr__(self, item):
return self.config.get(item)
def __getitem__(self, item):
return self.config.get(item)
def __setitem__(self, key, value):
if self.parent:
self.parent[self.name][key] = value
self.config = self.parent[self.name]
else:
self.config[key] = value
self.save()

View File

@ -1,27 +0,0 @@
import os
import toml
from config.Base import Config
class FSConfig(Config):
path: str
def __init__(self, path="config.toml", *args, **kwargs):
super().__init__(*args, **kwargs)
self.path = path
os.makedirs(os.path.dirname(path), exist_ok=True)
open(path, "a").close()
def _load(self):
with open(self.path, "r") as file:
content = file.read()
self.config = toml.loads(content)
if self.config is None:
self.config = {}
def _save(self):
content = toml.dumps(self.config)
with open(self.path, "w") as file:
file.write(content)

View File

@ -1,2 +1,3 @@
from .Base import Config
from .FileSystem import FSConfig
from .base import Config
__all__ = ["Config"]

128
config/base.py Normal file
View File

@ -0,0 +1,128 @@
from __future__ import annotations
from typing import Dict, Type, Any, TYPE_CHECKING
import toml
if TYPE_CHECKING:
from config.config_types.base_type import BaseType
class Config:
#: Path of config file
path: str
#: Current fields
fields: Dict[str, BaseType]
def __init__(self, path: str) -> None:
"""
Create config object
Basic usage:
>>> config = Config("doctest_config.toml")
:param path: Path of config file
:type path: str
:rtype: None
:rtype: None
"""
self.fields = {}
self.path = path
def register(self, name: str, type_: Type[BaseType]) -> None:
"""
Register option
Basic usage:
>>> from config.config_types import factory, Int
>>> config = Config("doctest_config.toml")
>>> config.register("my_parameter", factory(Int))
:param name: Name of config parameter
:param type_: Type of config parameter
:type name: str
:type type_: Type[BaseType]
:return: None
:rtype: None
"""
self.fields.update({
name: type_()
})
def set(self, values: dict) -> None:
"""
Set all parameters with values (and override old ones)
Basic usage:
>>> from config.config_types import factory, Int
>>> config = Config("doctest_config.toml")
>>> config.register("my_parameter", factory(Int))
>>> config.set({"my_parameter": 3})
:type values: dict
:param values: dict of parameters
:return: None
:rtype: None
"""
for k, v in values.items():
self.fields[k].set(v)
def save(self) -> None:
"""
Save config to ``self.file``
Basic usage:
>>> from config.config_types import factory, Int
>>> config = Config("doctest_config.toml")
>>> config.register("my_parameter", factory(Int))
>>> config.set({"my_parameter": 3})
>>> config.save()
:return: None
"""
with open(self.path, 'w') as file:
toml.dump({k: v.to_save() for k, v in self.fields.items()}, file)
def load(self) -> None:
"""
Load config from ``self.file``
Basic usage:
>>> from config.config_types import factory, Int
>>> config = Config("doctest_config.toml")
>>> config.register("my_parameter", factory(Int))
>>> config.set({"my_parameter": 3})
>>> config.save()
>>> new_config = Config("doctest_config.toml")
>>> new_config.register("my_parameter", factory(Int))
>>> new_config.load()
>>> new_config["my_parameter"]
3
:return: None
"""
with open(self.path, 'r') as file:
self.set(toml.load(file))
def __getitem__(self, item: str) -> Any:
"""
Save config to ``self.file``
Basic usage:
>>> from config.config_types import factory, Int
>>> config = Config("doctest_config.toml")
>>> config.register("my_parameter", factory(Int))
>>> config.set({"my_parameter": 3})
>>> print(config["my_parameter"])
3
:return: None
"""
return self.fields[item].get()

View File

@ -0,0 +1,32 @@
from typing import Type
from config.config_types.base_type import BaseType
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
__all__ = ['factory', "BaseType", 'Dict', 'Float', 'Int', 'List', 'Str']
def factory(type: Type[BaseType], *args, **kwargs):
"""
Create a new test ``type`` with parameters args and kwargs
:Basic usage:
>>> factory(Int) # doctest: +ELLIPSIS
<class '...'>
>>> factory(Int, min=0, max=10) # doctest: +ELLIPSIS
<class '...'>
:param type: Type to create
:return: New type
"""
class Type(type):
def __init__(self):
super().__init__(*args, **kwargs)
return Type

View File

@ -0,0 +1,20 @@
class BaseType:
def check_value(self, value):
"""Check if value is good"""
pass
def set(self, value):
"""Check and set value"""
pass
def get(self):
"""Get value"""
pass
def to_save(self):
"""Build a serializable data to save"""
pass
def load(self, value):
"""Fill with value"""
pass

View File

@ -0,0 +1,63 @@
from typing import Type
from config.config_types.base_type import BaseType
class Dict(BaseType):
type_key: Type[BaseType]
type_value: Type[BaseType]
def __init__(self, type_key, type_value):
self.type_key = type_key
self.type_value = type_value
self.values = None
def check_value(self, value):
"""Check if value is good"""
o_key = self.type_key()
o_value = self.type_value()
if type(value) == dict:
for k, v in value.items():
if not (o_key.check_value(k) and o_value.check_value(v)):
return False
return True
if (type(value) == list or type(value) == tuple) and len(value) == 2:
return o_key.check_value(value[0]) and o_value.check_value(value[1])
return False
def set(self, value):
"""Check and set value"""
new_dict = dict()
if not self.check_value(value):
raise ValueError("Tentative de définir une valeur incompatible")
for k, v in value.items():
new_key = self.type_key()
new_key.set(k)
new_value = self.type_value()
new_value.set(v)
new_dict.update({new_key: new_value})
self.values = new_dict
def get(self):
"""Get value"""
if self.values is not None:
return {k.get(): v.get() for k, v in self.values.items()}
return dict()
def to_save(self):
"""Build a serializable data to save"""
# Construction d'une liste de liste: [[key, value], ...]
if self.values is not None:
return [[k.to_save(), v.to_save()] for k, v in self.values.items()]
return list()
def load(self, value):
"""Fill with value"""
new_values = dict()
for v in value:
new_key = self.type_key()
new_key.load(v[0])
new_value = self.type_value()
new_value.load(v[1])
new_values.update({new_key: new_value})
self.values = new_values

View File

@ -0,0 +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
__all__ = ['Channel', "Guild", "User", "Role"]

View File

@ -0,0 +1,36 @@
from __future__ import annotations
from typing import TYPE_CHECKING
from config.config_types.base_type import BaseType
if TYPE_CHECKING:
from main import LBI
class Channel(BaseType):
client: LBI
def __init__(self, client):
self.value = None
self.client = client
def check_value(self, value):
return True
def set(self, value):
if self.check_value(value):
self.value = value
return
raise ValueError("Tentative de définir une valeur incompatible")
def get(self):
return self.value
def to_save(self):
return self.value
def load(self, value):
if self.check_value(value):
raise ValueError("Tentative de charger une donnée incompatible.")
self.value = value

View File

@ -0,0 +1,36 @@
from __future__ import annotations
from typing import TYPE_CHECKING
from config.config_types.base_type import BaseType
if TYPE_CHECKING:
from main import LBI
class Guild(BaseType):
client: LBI
def __init__(self, client):
self.value = None
self.client = client
def check_value(self, value):
return True
def set(self, value):
if self.check_value(value):
self.value = value
return
raise ValueError("Tentative de définir une valeur incompatible")
def get(self):
return self.value
def to_save(self):
return self.value
def load(self, value):
if self.check_value(value):
raise ValueError("Tentative de charger une donnée incompatible.")
self.value = value

View File

@ -0,0 +1,36 @@
from __future__ import annotations
from typing import TYPE_CHECKING
from config.config_types.base_type import BaseType
if TYPE_CHECKING:
from main import LBI
class Role(BaseType):
client: LBI
def __init__(self, client):
self.value = None
self.client = client
def check_value(self, value):
return True
def set(self, value):
if self.check_value(value):
self.value = value
return
raise ValueError("Tentative de définir une valeur incompatible")
def get(self):
return self.value
def to_save(self):
return self.value
def load(self, value):
if self.check_value(value):
raise ValueError("Tentative de charger une donnée incompatible.")
self.value = value

View File

@ -0,0 +1,36 @@
from __future__ import annotations
from typing import TYPE_CHECKING
from config.config_types.base_type import BaseType
if TYPE_CHECKING:
from main import LBI
class User(BaseType):
client: LBI
def __init__(self, client):
self.value = None
self.client = client
def check_value(self, value):
return True
def set(self, value):
if self.check_value(value):
self.value = value
return
raise ValueError("Tentative de définir une valeur incompatible")
def get(self):
return self.value
def to_save(self):
return self.value
def load(self, value):
if self.check_value(value):
raise ValueError("Tentative de charger une donnée incompatible.")
self.value = value

View File

@ -0,0 +1,38 @@
from config.config_types.base_type import BaseType
class Float(BaseType):
def __init__(self, min_=None, max_=None):
self.value = None
self.min = min_
self.max = max_
def check_value(self, value):
try:
float(value)
except ValueError:
return False
# TODO: < ou <=? > ou >=?
# Check min/max
if self.min is not None and float(value) < self.min:
return False
if self.max is not None and float(value) > self.max:
return False
return True
def set(self, value):
if self.check_value(value):
self.value = value
return
raise ValueError("Tentative de définir une valeur incompatible")
def get(self):
return self.value
def to_save(self):
return self.value
def load(self, value):
if self.check_value(value):
raise ValueError("Tentative de charger une donnée incompatible.")
self.value = value

177
config/config_types/int.py Normal file
View File

@ -0,0 +1,177 @@
from typing import Optional, List
from config.config_types.base_type import BaseType
class Int(BaseType):
#: Max value for parameter
max: Optional[int]
#: Min value for parameter
min: Optional[int]
#: List of valid values for parameter
values: Optional[List[int]]
#: Current value of parameter
value: Optional[int]
def __init__(self, min: Optional[int] = None, max: Optional[int] = None,
values: Optional[List[int]] = None) -> None:
"""
Base Int type for config
:Basic usage:
>>> Int()
<Int object with value None>
>>> Int(min=0)
<Int object with value None, min=0 max=None>
>>> Int(max=0)
<Int object with value None, min=None max=0>
>>> Int(min=10, max=20)
<Int object with value None, min=10 max=20>
>>> Int(values=[2, 3, 5, 7])
<Int object with value None, values=[2, 3, 5, 7]>
>>> Int(min=0, values=[3, 4, 5]) # doctest: +IGNORE_EXCEPTION_DETAIL
Traceback (most recent call last):
ValueError: ...
:raise ValueError: If min and/or max are set when using values
:param min: Min value for this parameter
:param max: Max value for this parameter
:param values: This parameter can only be in one of these values (raise ValueError if min or max are set with values)
"""
self.value = None
if values is not None and (min is not None or max is not None):
raise ValueError("Il n'est pas possible de définir un champ avec à "
"la fois un max/min et une série de valeur")
self.values = values
self.min = min
self.max = max
def check_value(self, value: int) -> bool:
"""
Check if value is a correct int
Check if value is int, and if applicable, between ``min`` and ``max`` or in ``values``
:Basic usage:
>>> positive = Int(min=0)
>>> negative = Int(max=0)
>>> ten_to_twenty = Int(min=10, max=20)
>>> prime = Int(values=[2,3,5,7])
>>> positive.check_value(0)
True
>>> positive.check_value(-2)
False
>>> positive.check_value(345)
True
>>> negative.check_value(0)
True
>>> negative.check_value(-2)
True
>>> negative.check_value(345)
False
>>> ten_to_twenty.check_value(10)
True
>>> ten_to_twenty.check_value(-2)
False
>>> ten_to_twenty.check_value(20)
True
>>> prime.check_value(2)
True
>>> prime.check_value(4)
False
>>> prime.check_value(5)
True
:param value: value to check
:return: True if value is correct
"""
try:
int(value)
except ValueError:
return False
# TODO: < ou <=? > ou >=?
# Check min/max
if self.min is not None and int(value) < self.min:
return False
if self.max is not None and int(value) > self.max:
return False
# Check validity
if self.values is not None and value not in self.values:
return False
return True
def set(self, value: int) -> None:
"""
Set value of parameter
:Basic usage:
>>> my_int = Int(min=0)
>>> my_int.set(34)
>>> my_int.set(-34) # doctest: +IGNORE_EXCEPTION_DETAIL
Traceback (most recent call last):
ValueError: ...
:raise ValueError: if attempt to set invalid value
:param value: Value to set
:return: None
"""
if not self.check_value(value):
raise ValueError("Tentative de définir une valeur incompatible")
self.value = value
def get(self) -> Optional[int]:
"""
Get value of parameter
:Basic usage:
>>> my_int = Int()
>>> my_int.set(34)
>>> my_int.get()
34
:return: Value of parameter
"""
return self.value
def to_save(self) -> int:
"""
Build a serializable object
:Basic usage:
>>> my_int = Int()
>>> my_int.to_save()
>>> my_int.set(34)
>>> my_int.to_save()
34
:return: Current value
"""
return self.value
def load(self, value: int) -> None:
"""
Load serialized value
>>> my_int = Int()
>>> my_int.load(34)
>>> my_int.get()
34
:param value: Value to load
:return: None
"""
if not self.check_value(value):
raise ValueError("Tentative de charger une donnée incompatible.")
self.value = value
def __repr__(self):
if self.min is not None or self.max is not None:
return f'<Int object with value {self.value}, min={self.min} max={self.max}>'
if self.values:
return f'<Int object with value {self.value}, values={self.values}>'
return f'<Int object with value {self.value}>'

View File

@ -0,0 +1,42 @@
from typing import Type
from config.config_types.base_type import BaseType
class List(BaseType):
type_: Type[BaseType]
def __init__(self, type_, max_len=None):
self.type_ = type_
self.max_len = max_len
self.values = None
def check_value(self, value):
new_object = self.type_()
return new_object.check_value(value)
def set(self, value):
"""Check and set value"""
new_liste = []
for v in value:
new_element = self.type_()
new_element.set(v)
new_liste.append(new_element)
self.values = new_liste
def get(self):
"""Get value"""
if self.values is None:
raise ValueError("Config non initialisée")
return [v.get() for v in self.values]
def to_save(self):
"""Build a serializable data to save"""
return [v.to_save() for v in self.values]
def load(self, value):
"""Fill with value"""
for v in value:
new_object = self.type_()
new_object.load(v)
self.values.append(new_object)

View File

@ -0,0 +1,35 @@
from config.config_types.base_type import BaseType
class Str(BaseType):
def __init__(self):
self.value = None
def check_value(self, value):
"""Check if value is good"""
try:
str(value)
except ValueError:
return False
return True
def set(self, value):
"""Check and set value"""
if self.check_value(value):
self.value = value
return
raise ValueError("Tentative de définir une valeur incompatible")
def get(self):
"""Get value"""
return self.value
def to_save(self):
"""Build a serializable data to save"""
return self.value
def load(self, value):
"""Fill with value"""
if not self.check_value(value):
raise ValueError("Tentative de charger une donnée incompatible.")
self.value = value

54
config/log_config.json Normal file
View File

@ -0,0 +1,54 @@
{
"version": 1,
"disable_existing_loggers": false,
"formatters": {
"simple": {
"format": "%(asctime)s :: %(name)s :: %(levelname)s :: %(message)s"
}
},
"handlers": {
"console": {
"class": "logging.StreamHandler",
"level": "DEBUG",
"formatter": "simple",
"stream": "ext://sys.stdout"
},
"info_file_handler": {
"class": "logging.handlers.RotatingFileHandler",
"level": "INFO",
"formatter": "simple",
"filename": "info.log",
"maxBytes": 1048576,
"backupCount": 20,
"encoding": "utf8"
},
"error_file_handler": {
"class": "logging.handlers.RotatingFileHandler",
"level": "ERROR",
"formatter": "simple",
"filename": "errors.log",
"maxBytes": 1048576,
"backupCount": 20,
"encoding": "utf8"
}
},
"loggers": {
"discord": {
"level":"ERROR",
"handlers":["console", "info_file_handler", "error_file_handler"]
},
"LBI": {
"level":"DEBUG",
"handlers":["console", "info_file_handler", "error_file_handler"]
}
},
"root": {
"level": "INFO",
"handlers": []
}
}

66
main.py
View File

@ -1,4 +1,6 @@
#!/usr/bin/python3
from __future__ import annotations
import asyncio
import importlib
import json
@ -12,7 +14,8 @@ import discord
import humanize
from packaging.version import Version
from config.FileSystem import FSConfig
from config import Config, config_types
from config.config_types import factory
from errors import IncompatibleModule
from modules.base import base_supported_type
@ -211,7 +214,7 @@ def event(func):
setup_logging()
log_discord = logging.getLogger('discord')
log_discord = logging.getLogger('discord_types')
log_LBI = logging.getLogger('LBI')
log_communication = logging.getLogger('communication')
@ -222,6 +225,7 @@ def load_modules_info():
class LBI(discord.Client):
by_id: ClientById
base_path = "data"
debug = log_LBI.debug
info = log_LBI.info
@ -229,21 +233,35 @@ class LBI(discord.Client):
error = log_LBI.error
critical = log_LBI.critical
def __init__(self, config=None, *args, **kwargs):
def __init__(self, config: Config = None, *args, **kwargs):
super().__init__(*args, **kwargs)
if config is None:
config = FSConfig(path="data/config.toml")
config = Config(path="data/config.toml")
self.reloading = False
self.id = ClientById(self)
self.by_id = ClientById(self)
self.ready = False
# Content: {"module_name": {"module": imported module, "class": initialized class}}
self.modules = {}
self.config = config
self.config.init(
{"modules": ["modules", "errors"], "prefix": "%", "admin_roles": [], "admin_users": [], "main_guild": 0,
"locale": "fr_FR.utf8"})
locale.setlocale(locale.LC_TIME, self.config.locale)
humanize.i18n.activate(self.config.locale)
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.Role, self)))
self.config.register("admin_users", factory(config_types.List, factory(config_types.discord.User, self)))
self.config.register("main_guild", factory(config_types.discord.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
@ -367,10 +385,10 @@ class ClientById:
:param id_: Id of message to find
:type id_: int
:raises discord.NotFound: This exception is raised when a message is not found (or not accessible by bot)
:raises discord_types.NotFound: This exception is raised when a message is not found (or not accessible by bot)
:rtype: discord.Message
:return: discord.Message instance if message is found.
:return: discord_types.Message instance if message is found.
"""
msg = None
for channel in self.client.get_all_channels():
@ -432,7 +450,6 @@ class ClientById:
return None
client1 = LBI(max_messages=500000)
class Communication(asyncio.Protocol):
@ -460,19 +477,20 @@ class Communication(asyncio.Protocol):
def connection_lost(self, exc):
print('%s: connection lost: %s' % (self.name, exc))
communication = Communication(client1)
if __name__ == "__main__":
client1 = LBI(max_messages=500000)
communication = Communication(client1)
async def start_bot():
await client1.start(os.environ.get("DISCORD_TOKEN"))
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")
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"))
loop.run_until_complete(t)
loop.create_task(start_bot())
loop.run_forever()
loop = asyncio.get_event_loop()
t = loop.create_unix_server(Communication,
path=os.path.join("/tmp", os.path.dirname(os.path.realpath(__file__)) + ".sock"))
loop.run_until_complete(t)
loop.create_task(start_bot())
loop.run_forever()

35
make.bat Normal file
View File

@ -0,0 +1,35 @@
@ECHO OFF
pushd %~dp0
REM Command file for Sphinx documentation
if "%SPHINXBUILD%" == "" (
set SPHINXBUILD=sphinx-build
)
set SOURCEDIR=source
set BUILDDIR=build
if "%1" == "" goto help
%SPHINXBUILD% >NUL 2>NUL
if errorlevel 9009 (
echo.
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
echo.installed, then set the SPHINXBUILD environment variable to point
echo.to the full path of the 'sphinx-build' executable. Alternatively you
echo.may add the Sphinx directory to PATH.
echo.
echo.If you don't have Sphinx installed, grab it from
echo.http://sphinx-doc.org/
exit /b 1
)
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
goto end
:help
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
:end
popd

View File

@ -25,7 +25,7 @@ class MainClass(BaseClassPython):
def __init__(self, client):
super().__init__(client)
self.config.init({"spectate_channel": 0,
self.config.set({"spectate_channel": 0,
"illustrations":{"merlin":"",
"perceval":"",
"gentil":"",
@ -43,7 +43,7 @@ class MainClass(BaseClassPython):
"oberon":0,
"mechant":0,
"test":15},
"test":{"merlin":"",
"test":{"merlin":"",
"perceval":0,
"gentil":0,
"assassin":0,
@ -52,5 +52,5 @@ class MainClass(BaseClassPython):
"oberon":0,
"mechant":0,
"test":15}
})
})

View File

@ -29,8 +29,8 @@ class BaseClass:
:type client: LBI"""
self.client = client
self.objects = Objects(path=os.path.join("data", self.name.lower()))
self.config = Config(parent=self.client.config, name="mod-" + self.name.lower())
self.config.init({"help_active": True, "color": 0x000000, "auth_everyone": False, "authorized_roles": [],
self.config = Config(path=os.path.join("data", self.name.lower(), "config.toml"))
self.config.set({"help_active": True, "color": 0x000000, "auth_everyone": False, "authorized_roles": [],
"authorized_users": [], "command_text": self.name.lower(), "configured": False})
async def send_help(self, channel):

View File

@ -13,7 +13,7 @@ class MainClass(BaseClassPython):
authorized_users = []
authorized_roles = []
help = {
"description": "Montre toutes les erreurs du bot dans discord.",
"description": "Montre toutes les erreurs du bot dans discord_types.",
"commands": {
"`{prefix}{command}`": "Renvoie une erreur de test.",
}
@ -21,7 +21,7 @@ class MainClass(BaseClassPython):
def __init__(self, client):
super().__init__(client)
self.config.init({"dev_chan": [], "memes": [""], "icon": ""})
self.config.set({"dev_chan": [], "memes": [""], "icon": ""})
self.errorsList = None
async def on_load(self):

View File

@ -11,7 +11,7 @@ class MainClass(BaseClassPython):
def __init__(self, client):
super().__init__(client)
self.config.init({"new_role": 0,
self.config.set({"new_role": 0,
"motd": "Bienvenue !"})
async def on_ready(self):

View File

@ -5,7 +5,9 @@ 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
@ -24,7 +26,8 @@ class MainClass(BaseClassPython):
def __init__(self, client):
super().__init__(client)
self.config.init({"channel": 0, "lost_role": 0, "min_delta": datetime.timedelta(minutes=26).total_seconds()})
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_message(self, message: discord.Message):

View File

@ -13,7 +13,7 @@ class MainClass(BaseClassPython):
def __init__(self, client):
super().__init__(client)
self.config.init({"accepted_role": 0,
self.config.set({"accepted_role": 0,
"new_role": 0,
"listen_chan": 0,
"log_chan": 0,

View File

@ -34,7 +34,7 @@ class MainClass(BaseClassPython):
def __init__(self, client):
super().__init__(client)
self.config.init({"roles": {}})
self.config.set({"roles": {}})
async def com_list(self, message, args, kwargs):
response = discord.Embed(title="Roles disponibles", color=self.config.color)

View File

@ -16,7 +16,7 @@ class MainClass(BaseClassPython):
def __init__(self, client):
super().__init__(client)
self.config.init({"memes": []})
self.config.set({"memes": []})
async def command(self, message, args, kwargs):
await message.channel.send(

2
pytest.ini Normal file
View File

@ -0,0 +1,2 @@
[pytest]
addopts = --doctest-modules

View File

@ -0,0 +1,10 @@
config.config\_types.discord package
====================================
Module contents
---------------
.. automodule:: config.config_types.discord_types
:members:
:undoc-members:
:show-inheritance:

View File

@ -0,0 +1,20 @@
config.config\_types package
============================
Module contents
---------------
.. automodule:: config.config_types
:members:
:undoc-members:
:show-inheritance:
Subpackages
-----------
.. toctree::
:maxdepth: 4
config.config_types.discord_types

19
source/api/config.rst Normal file
View File

@ -0,0 +1,19 @@
config package
==============
Module contents
---------------
.. automodule:: config
:members:
:undoc-members:
:show-inheritance:
Subpackages
-----------
.. toctree::
:maxdepth: 4
config.config_types

8
source/api/modules.rst Normal file
View File

@ -0,0 +1,8 @@
API Reference
=============
.. toctree::
:maxdepth: 4
config
utils

View File

@ -0,0 +1,10 @@
utils.emojis package
============================
Module contents
---------------
.. automodule:: utils.emojis
:members:
:undoc-members:
:show-inheritance:

19
source/api/utils.rst Normal file
View File

@ -0,0 +1,19 @@
utils package
=============
Module contents
---------------
.. automodule:: utils
:members:
:undoc-members:
:show-inheritance:
Subpackages
-----------
.. toctree::
:maxdepth: 4
utils.emojis

59
source/conf.py Normal file
View File

@ -0,0 +1,59 @@
# Configuration file for the Sphinx documentation builder.
#
# This file only contains a selection of the most common options. For a full
# list see the documentation:
# https://www.sphinx-doc.org/en/master/usage/configuration.html
# -- Path setup --------------------------------------------------------------
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#
import os
import sys
sys.path.insert(0, os.path.abspath('../'))
# -- Project information -----------------------------------------------------
project = 'Python Discord Bot'
copyright = '2020, Chauvet Louis <louis.chauvet@free.fr>, Suwako'
author = 'Chauvet Louis <louis.chauvet@free.fr>, Suwako'
# The full version, including alpha/beta/rc tags
release = '0.0.1'
# -- General configuration ---------------------------------------------------
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
'sphinx.ext.autodoc',
'sphinx_autodoc_typehints',
]
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This pattern also affects html_static_path and html_extra_path.
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
# -- Options for HTML output -------------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
#
html_theme = 'classic'
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
pygments_style = 'sphinx'
set_type_checking_flag = True
autoclass_content = 'both'

34
source/index.rst Normal file
View File

@ -0,0 +1,34 @@
.. Python Discord Bot documentation master file, created by
sphinx-quickstart on Mon Apr 13 12:55:17 2020.
You can adapt this file completely to your liking, but it should at least
contain the root `toctree` directive.
Welcome to Python Discord Bot's documentation!
==============================================
.. toctree::
:maxdepth: 2
:caption: Contents:
module_creation/index
api/modules
Introduction
============
"Python Discord Bot" is a fully modular, self-hostable discord bot.
Its goal is to provide a solid and minimal base (only error handling, help, modules and configuration management) and to provide a large amount of modules.
In addition to being fully modular, this bot is meant to be a single server, in order to allow advanced configuration and simple management of private messages (many modules are games that need to use private messages, and it wouldn't be nice to add a choice of server for each action).
For users, nothing could be simpler, you just install, register your bot on discordapp.com and let yourself be guided by !config.
For developers, all the documentation is here, and the source code is fully documented.
Indices and tables
==================
* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`

View File

@ -0,0 +1,8 @@
Module creation
===============
.. toctree::
:maxdepth: 2
:caption: Contents:
intro

View File

@ -0,0 +1,67 @@
Introduction
============
Creating a module is relatively simple: just create a python package (a folder that contains a ``__init__.py`` file) in
the modules folder, insert a ``version.json`` file (which will allow you to add dependencies and general information for
your module) and have a MainClass class in the ``__init__.py`` file.
So the next step is to create the :py:class:`MainClass`, which inherits from :py:class:`BaseClassPython`, here is a minimal example:
.. code-block:: python
:linenos:
class MainClass:
name = "MyFirstModule"
help = {
"description": "My first module",
"commands": {
}
}
As you can see it's very simple, from now on you can start the bot and load the module.
Currently it does nothing, so let's add a ``say`` command:
.. code-block:: python
:linenos:
:emphasize-lines: 6,10,11
class MainClass:
name = "MyFirstModule"
help = {
"description": "My first module",
"commands": {
"{prefix}{command} say <message>": "Bot send message <message>",
}
}
async def com_say(self, message, args, kwargs):
await message.channel.send(args[0])
You can now reload the module and test the command ``!myfirstmodule say "Hello world"``.
You can see that without the quotation marks the returned message contains only the first word. Indeed each message is
processed to extract the module (here ``module``), the command (here ``say``) and the arguments. This is how the
arguments are processed:
``!mymodule say "Hello world" "Goodbye world"`` - ``args = ["Hello world", "Goodbye world"] kwargs=[]``
``!mymodule say --long-option -an -s "s value"`` - ``args = [] kwargs = [("long-option", None), ("a", None), ("n", None), ("s", "s value")]``
``!mymodule say -s "s value" "value"`` - ``args = ["value"] kwargs = [("s", "s value")]``
So let's add an ``-m`` option that adds the mention of the author to the message:
.. code-block:: python
:linenos:
:lineno-start: 10
:emphasize-lines: 2,3,4
async def com_say(self, message, args, kwargs):
if 'm' in [k for k, v in kwargs]:
await message.channel.send(message.author.mention + args[0])
return
await message.channel.send(args[0])

View File

@ -1,16 +1,38 @@
NUMBERS = ["1⃣", "2⃣", "3⃣", "4⃣", "5⃣", "6⃣", "7⃣", "8⃣", "9⃣", "🔟"]
from typing import Union
THUMBS_UP = "👍"
THUMBS_DOWN = "👎"
WHITE_CHECK_MARK = ""
NUMBERS = ["\u0030\u20e3", "\u0031\u20e3", "\u0032\u20e3", "\u0033\u20e3", "\u0034\u20e3", "\u0035\u20e3",
"\u0036\u20e3", "\u0037\u20e3", "\u0038\u20e3", "\u0039\u20e3", "\U0001f51f"]
MINUS = "\u2796"
THUMBS_UP = "\U0001f44d"
THUMBS_DOWN = "\U0001f44e"
WHITE_CHECK_MARK = "\u2705"
def write_with_number(i):
raw = str(i)
def write_with_number(i: Union[int, float]):
"""
Write number with emoji
:Basic usage:
>>> write_with_number(23)
'2⃣3⃣'
>>> write_with_number(-23)
'2⃣3⃣'
>>> write_with_number(-23.34)
'2⃣3⃣.3⃣4⃣'
>>> write_with_number(-1234567890.098)
'1⃣2⃣3⃣4⃣5⃣6⃣7⃣8⃣9⃣0⃣.0⃣9⃣8⃣'
:param i: number to write
:return: string with emojis
"""
s = ""
for c in str(i):
if raw == ".":
if c == ".":
s += "."
elif c == "-":
s += MINUS
else:
s += NUMBERS[int(c)]
return s