PycordViews 1.1.3__py3-none-any.whl → 1.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pycordViews/__init__.py +2 -2
- pycordViews/menu/__init__.py +1 -1
- pycordViews/menu/menu.py +75 -14
- pycordViews/menu/selectMenu.py +29 -9
- pycordViews/multibot/__init__.py +1 -5
- pycordViews/multibot/bot.py +194 -0
- pycordViews/multibot/errors.py +25 -5
- pycordViews/multibot/multibot.py +206 -0
- pycordViews/multibot/process.py +217 -0
- pycordViews/pagination/pagination_view.py +7 -0
- pycordViews/views/easy_modified_view.py +40 -18
- pycordviews-1.2.0.dist-info/METADATA +241 -0
- pycordviews-1.2.0.dist-info/RECORD +21 -0
- {PycordViews-1.1.3.dist-info → pycordviews-1.2.0.dist-info}/WHEEL +1 -1
- PycordViews-1.1.3.dist-info/METADATA +0 -80
- PycordViews-1.1.3.dist-info/RECORD +0 -23
- pycordViews/multibot/process_for_bots.py +0 -112
- pycordViews/multibot/process_messages.py +0 -7
- pycordViews/multibot/runner.py +0 -14
- pycordViews/multibot/start_multibot.py +0 -80
- pycordViews/typeViews.py +0 -4
- {PycordViews-1.1.3.dist-info → pycordviews-1.2.0.dist-info/licenses}/LICENSE +0 -0
- {PycordViews-1.1.3.dist-info → pycordviews-1.2.0.dist-info}/top_level.txt +0 -0
pycordViews/__init__.py
CHANGED
@@ -1,5 +1,5 @@
|
|
1
|
-
from .typeViews import *
|
2
1
|
from .views.easy_modified_view import EasyModifiedViews
|
3
2
|
from .pagination.pagination_view import Pagination
|
4
|
-
from .multibot
|
3
|
+
from .multibot import Multibot
|
5
4
|
from .menu.selectMenu import SelectMenu
|
5
|
+
from .menu.menu import Menu, CustomSelect
|
pycordViews/menu/__init__.py
CHANGED
pycordViews/menu/menu.py
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
from discord.components import ComponentType
|
2
|
-
from discord.ui import Select
|
3
|
-
from discord import MISSING, Emoji, PartialEmoji
|
2
|
+
from discord.ui import Select, Item
|
3
|
+
from discord import MISSING, Emoji, PartialEmoji, Interaction
|
4
4
|
from typing import Callable, Union
|
5
5
|
|
6
6
|
from .errors import NotCoroutineError, ComponentTypeError
|
@@ -12,12 +12,14 @@ class Menu:
|
|
12
12
|
"""
|
13
13
|
A basic menu from selectMenu class
|
14
14
|
"""
|
15
|
-
self.__menu:
|
15
|
+
self.__menu: CustomSelect = CustomSelect(menu=self, select_type=menu_type, **kwargs)
|
16
16
|
self.__selectMenu = selectmenu
|
17
|
+
self.__menu_type: ComponentType = menu_type
|
17
18
|
|
18
|
-
def set_callable(self, _callable: Callable) -> "Menu":
|
19
|
+
def set_callable(self, _callable: Union[Callable, None]) -> "Menu":
|
19
20
|
"""
|
20
|
-
Add a coroutine to the menu
|
21
|
+
Add a coroutine to the menu (same function on SelectMenu class)
|
22
|
+
This coroutine can have 2 parameters (X, interaction)
|
21
23
|
"""
|
22
24
|
if not isinstance(_callable, Callable):
|
23
25
|
raise NotCoroutineError(_callable)
|
@@ -30,28 +32,87 @@ class Menu:
|
|
30
32
|
Add an option to choice.
|
31
33
|
Only from string_select type !
|
32
34
|
"""
|
33
|
-
|
34
|
-
raise ComponentTypeError()
|
35
|
+
self.__is_string_select()
|
35
36
|
|
36
37
|
self.__menu.add_option(label=label, value=value, description=description, emoji=emoji, default=default)
|
37
38
|
return self
|
38
39
|
|
40
|
+
def remove_options(self, *labels: str) -> "Menu":
|
41
|
+
"""
|
42
|
+
Remove an option to choice.
|
43
|
+
Only from string_select type !
|
44
|
+
:param labels: Label option name to delete
|
45
|
+
"""
|
46
|
+
self.__is_string_select()
|
47
|
+
|
48
|
+
self.__menu.options = [i for i in self.__menu.options if i.label not in labels]
|
49
|
+
return self
|
50
|
+
|
51
|
+
def update_option(self, current_label: str, new_label: str = None, value: str = None, description: Union[str, None] = None, emoji: Union[str, Emoji, PartialEmoji, None] = None, default: Union[bool, None] = None) -> "Menu":
|
52
|
+
"""
|
53
|
+
Update option. To find the option, write her actual label in "current_label" option.
|
54
|
+
Only from string_select type !
|
55
|
+
:param current_label: The current label option to edit
|
56
|
+
"""
|
57
|
+
self.__is_string_select()
|
58
|
+
|
59
|
+
for option in self.__menu.options:
|
60
|
+
if option.label == current_label:
|
61
|
+
option.label = new_label if new_label is not None else option.label
|
62
|
+
option.value = value if value is not None else option.value
|
63
|
+
option.description = description if description is not None else option.description
|
64
|
+
option.default = default if default is not None else option.default
|
65
|
+
option.emoji = emoji if emoji is not None else option.emoji
|
66
|
+
break
|
67
|
+
return self
|
68
|
+
|
69
|
+
def __is_string_select(self) -> None:
|
70
|
+
"""
|
71
|
+
Check if the menu is a string_select
|
72
|
+
:raise: ComponentTypeError
|
73
|
+
"""
|
74
|
+
if self.__menu.type != ComponentType.string_select:
|
75
|
+
raise ComponentTypeError()
|
76
|
+
|
39
77
|
@property
|
40
|
-
def component(self) ->
|
78
|
+
def component(self) -> "CustomSelect":
|
41
79
|
"""
|
42
80
|
Get the component
|
43
81
|
"""
|
44
82
|
return self.__menu
|
45
83
|
|
84
|
+
@property
|
85
|
+
def selectmenu(self) -> "SelectMenu":
|
86
|
+
"""
|
87
|
+
Get the selectMenu
|
88
|
+
"""
|
89
|
+
return self.__selectMenu
|
90
|
+
|
46
91
|
@property
|
47
92
|
def callable(self) -> Callable:
|
48
93
|
"""
|
49
|
-
Get the callable
|
94
|
+
Get the current callable menu
|
50
95
|
"""
|
51
|
-
return self.
|
96
|
+
return self.__selectMenu.get_callable(self.__menu.custom_id)
|
52
97
|
|
53
|
-
|
54
|
-
|
98
|
+
class CustomSelect(Select):
|
99
|
+
"""
|
100
|
+
Subclass of Select discord Class to use some SelectMenu functions
|
101
|
+
"""
|
102
|
+
|
103
|
+
def __init__(self, menu: Menu, select_type: ComponentType, *items: Item, **kwargs):
|
104
|
+
super().__init__(select_type=select_type, *items, **kwargs)
|
105
|
+
self.__menu: Menu = menu
|
106
|
+
|
107
|
+
async def update(self):
|
108
|
+
"""
|
109
|
+
Bridge to SelectMenu update function
|
110
|
+
"""
|
111
|
+
return await self.__menu.selectmenu.update()
|
112
|
+
|
113
|
+
@property
|
114
|
+
def get_view(self) -> "EasyModifiedViews":
|
115
|
+
"""
|
116
|
+
Bridge to SelectMenu get_view property
|
55
117
|
"""
|
56
|
-
|
57
|
-
"""
|
118
|
+
return self.__menu.selectmenu.get_view
|
pycordViews/menu/selectMenu.py
CHANGED
@@ -1,9 +1,11 @@
|
|
1
|
+
|
1
2
|
from ..views.easy_modified_view import EasyModifiedViews
|
2
3
|
from .menu import Menu
|
3
4
|
|
4
5
|
from typing import Union, Callable, Any
|
5
6
|
from discord.components import ComponentType
|
6
|
-
from discord import ChannelType, Member,
|
7
|
+
from discord import ChannelType, Member, ApplicationContext
|
8
|
+
from discord.abc import GuildChannel
|
7
9
|
|
8
10
|
class SelectMenu:
|
9
11
|
"""
|
@@ -52,9 +54,9 @@ class SelectMenu:
|
|
52
54
|
"""
|
53
55
|
return self.__global_add_component(ComponentType.role_select, custom_id=custom_id, placeholder=placeholder, max_values=max_values, min_values=min_values, disabled=disabled, row=row)
|
54
56
|
|
55
|
-
def
|
57
|
+
def add_mentionable_select_menu(self, custom_id: str = None, placeholder: str = None, min_values: int = 1, max_values: int = 1, disabled=False, row=None) -> Menu:
|
56
58
|
"""
|
57
|
-
Add a
|
59
|
+
Add a mentionable select menu in the ui
|
58
60
|
:param custom_id: The ID of the select menu that gets received during an interaction. If not given then one is generated for you.
|
59
61
|
:param placeholder: The placeholder text that is shown if nothing is selected, if any.
|
60
62
|
:param max_values: The maximum number of items that must be chosen for this select menu. Defaults to 1 and must be between 1 and 25.
|
@@ -66,7 +68,7 @@ class SelectMenu:
|
|
66
68
|
|
67
69
|
def add_channel_select_menu(self, custom_id: str = None, placeholder: str = None, min_values: int = 1, max_values: int = 1, disabled=False, row=None, channel_types: list[ChannelType] = None):
|
68
70
|
"""
|
69
|
-
Add a
|
71
|
+
Add a channel select menu in the ui
|
70
72
|
:param custom_id: The ID of the select menu that gets received during an interaction. If not given then one is generated for you.
|
71
73
|
:param placeholder: The placeholder text that is shown if nothing is selected, if any.
|
72
74
|
:param max_values: The maximum number of items that must be chosen for this select menu. Defaults to 1 and must be between 1 and 25.
|
@@ -89,7 +91,7 @@ class SelectMenu:
|
|
89
91
|
global function to add a Select component
|
90
92
|
"""
|
91
93
|
menu = Menu(component_type,
|
92
|
-
self
|
94
|
+
self,
|
93
95
|
**{'custom_id': custom_id, 'placeholder': placeholder, 'min_values': min_values,
|
94
96
|
'max_values': max_values, 'disabled': disabled, 'row': row, 'channel_types': channel_types})
|
95
97
|
|
@@ -97,10 +99,11 @@ class SelectMenu:
|
|
97
99
|
|
98
100
|
return menu
|
99
101
|
|
100
|
-
def set_callable(self, *custom_ids: str, _callable
|
102
|
+
def set_callable(self, *custom_ids: str, _callable: Union[Callable, None]) -> "SelectMenu":
|
101
103
|
"""
|
102
|
-
Set a callable for
|
103
|
-
:param custom_ids: IDs
|
104
|
+
Set a callable for menus associated with custom_ids
|
105
|
+
:param custom_ids: IDs menus
|
106
|
+
:param _callable: The coroutine to set for all menus
|
104
107
|
"""
|
105
108
|
self.__select_menu.set_callable(*custom_ids, _callable=_callable)
|
106
109
|
return self
|
@@ -111,15 +114,32 @@ class SelectMenu:
|
|
111
114
|
"""
|
112
115
|
return await self.__select_menu.respond(ctx=ctx, *args, view=self.__select_menu, **kwargs)
|
113
116
|
|
114
|
-
async def send(self, target: Union[Member,
|
117
|
+
async def send(self, target: Union[Member, GuildChannel], *args, **kwargs) -> Any:
|
115
118
|
"""
|
116
119
|
Send at the target
|
117
120
|
"""
|
118
121
|
return await self.__select_menu.send(target=target, *args, view=self.__select_menu, **kwargs)
|
119
122
|
|
123
|
+
async def update(self):
|
124
|
+
"""
|
125
|
+
Update the message.
|
126
|
+
If the command doesn't have been respond, nothing happened.
|
127
|
+
"""
|
128
|
+
if self.get_view.message is None:
|
129
|
+
return
|
130
|
+
await self.__select_menu.update_items(*self.get_view.items)
|
131
|
+
|
132
|
+
def get_callable(self, custom_id: str) -> Union[Callable, None]:
|
133
|
+
"""
|
134
|
+
Get the callable UI
|
135
|
+
:param custom_id: UI ID
|
136
|
+
"""
|
137
|
+
return self.__select_menu.get_callable(custom_id)
|
138
|
+
|
120
139
|
@property
|
121
140
|
def get_view(self) -> EasyModifiedViews:
|
122
141
|
"""
|
123
142
|
Get the current view
|
124
143
|
"""
|
125
144
|
return self.__select_menu
|
145
|
+
|
pycordViews/multibot/__init__.py
CHANGED
@@ -0,0 +1,194 @@
|
|
1
|
+
from threading import Thread
|
2
|
+
from discord import Intents, Bot, ApplicationCommand
|
3
|
+
from asyncio import run_coroutine_threadsafe, new_event_loop, set_event_loop, Future, AbstractEventLoop, sleep
|
4
|
+
from time import sleep as tsleep
|
5
|
+
from .errors import BotNotStartedError, SetupCommandFunctionNotFound, CommandFileNotFoundError
|
6
|
+
from typing import Optional
|
7
|
+
from importlib import reload
|
8
|
+
from importlib.util import spec_from_file_location, module_from_spec
|
9
|
+
from os import path
|
10
|
+
from sys import modules
|
11
|
+
|
12
|
+
|
13
|
+
class DiscordBot:
|
14
|
+
|
15
|
+
def __init__(self, token: str, intents: Intents, command_prefix: Optional[str] = None):
|
16
|
+
self.__token: str = token
|
17
|
+
self.__command_prefix: str = command_prefix
|
18
|
+
self.__running_bot: Future = None
|
19
|
+
self.__loop: AbstractEventLoop = new_event_loop()
|
20
|
+
self.__thread: Thread = Thread(target=self.run_loop, daemon=True) # Thread pour exécuter l'event loop
|
21
|
+
self.__thread.start()
|
22
|
+
self.__bot: Bot = Bot(loop=self.__loop, intents=intents, command_prefix=command_prefix, help_commad=None, auto_sync_commands=False)
|
23
|
+
self.__intents: Intents = intents
|
24
|
+
self.__imported_module: list[dict[str, ...]] = []
|
25
|
+
|
26
|
+
def run_loop(self):
|
27
|
+
"""Lance la boucle asyncio dans un thread séparé."""
|
28
|
+
set_event_loop(self.__loop)
|
29
|
+
self.__loop.run_forever()
|
30
|
+
|
31
|
+
def start(self) -> None:
|
32
|
+
"""Démarre le bot"""
|
33
|
+
|
34
|
+
self.__running_bot: Future = run_coroutine_threadsafe(self.__bot.start(token=self.__token, reconnect=True), self.__loop)
|
35
|
+
|
36
|
+
def stop(self) -> None:
|
37
|
+
"""
|
38
|
+
Stop le bot proprement depuis un autre thread
|
39
|
+
:raise: BotNotStartedError
|
40
|
+
"""
|
41
|
+
if self.is_running:
|
42
|
+
# Attendre que la fermeture du bot soit terminée
|
43
|
+
run_coroutine_threadsafe(self.__stop_bot_in_thread(), self.__loop).result(timeout=30)
|
44
|
+
self.__bot = Bot(token=self.__token, intents=self.__intents, command_prefix=self.__command_prefix, help_command=None)
|
45
|
+
self.__running_bot = None
|
46
|
+
else:
|
47
|
+
raise BotNotStartedError(self.__bot.user.name)
|
48
|
+
|
49
|
+
def add_pyFile_commands(self, file: str, setup_function: str, reload_command: bool):
|
50
|
+
"""
|
51
|
+
Ajoute et charge un fichier de commande bot et ses dépendances.
|
52
|
+
Les fichiers doivent avoir une fonction appelée « setup » ou un équivalent passé en paramètre.
|
53
|
+
|
54
|
+
def setup(bot: Bot) :
|
55
|
+
...
|
56
|
+
|
57
|
+
:param bot_name : Le nom du bot à ajouter au fichier de commandes
|
58
|
+
:param file: Chemin relatif ou absolue du fichier de commande
|
59
|
+
:param setup_function : Nom de la fonction appelée par le processus pour donner l'instance de Bot.
|
60
|
+
:param reload_command : Recharge toutes les commandes dans le fichier et les dépendances. Défaut : True
|
61
|
+
"""
|
62
|
+
module_name = path.splitext(path.basename(file))[0] # récupère le nom du fichier
|
63
|
+
spec = spec_from_file_location(module_name, file) # renvoie un "module spec" à partir du nom et du fichier
|
64
|
+
if spec and spec.loader:
|
65
|
+
module = module_from_spec(spec) # crée le package à partir du "module spec" s'il existe
|
66
|
+
spec.loader.exec_module(module) # charge tout le package
|
67
|
+
modules[module_name] = module # enregistre le modul dans les packages du système pour qu'il soit retrouvable lors du rechargement de celui-ci
|
68
|
+
self.__call_setup_function_in_command_file(file, module, setup_function, reload_command)
|
69
|
+
else:
|
70
|
+
raise CommandFileNotFoundError(file)
|
71
|
+
|
72
|
+
def modify_pyFile_commands(self, file: str, setup_function: str):
|
73
|
+
"""
|
74
|
+
Modifie un fichier de comandes et le recharge.
|
75
|
+
Ne recharge que le fichier et non les commandes du bot !
|
76
|
+
:param file: Le chemin d'accès relatif ou absolue du fichier
|
77
|
+
"""
|
78
|
+
print('ok')
|
79
|
+
module_name = path.splitext(path.basename(file))[0]
|
80
|
+
module_found = False
|
81
|
+
|
82
|
+
# Mise à jour du module et de son setup
|
83
|
+
print(self.__imported_module)
|
84
|
+
for imported in self.__imported_module:
|
85
|
+
print(imported['module'].__name__, module_name)
|
86
|
+
if imported['module'].__name__ == module_name:
|
87
|
+
print('reload module :', module_name)
|
88
|
+
reload(imported['module'])
|
89
|
+
imported['setup_function'] = setup_function
|
90
|
+
module_found = True
|
91
|
+
break
|
92
|
+
|
93
|
+
if not module_found:
|
94
|
+
raise CommandFileNotFoundError(file)
|
95
|
+
|
96
|
+
# Supprimer toutes les commandes du bot
|
97
|
+
for command in self.__bot.application_commands:
|
98
|
+
print('del commande', command.name)
|
99
|
+
self.__bot.remove_application_command(command)
|
100
|
+
|
101
|
+
# Réattacher toutes les commandes en réexécutant tous les setup
|
102
|
+
for imported in self.__imported_module:
|
103
|
+
self.__call_setup_function_in_command_file(
|
104
|
+
file,
|
105
|
+
imported['module'],
|
106
|
+
imported['setup_function'],
|
107
|
+
reload_command=False
|
108
|
+
)
|
109
|
+
|
110
|
+
|
111
|
+
def reload_commands(self, commands: Optional[list[ApplicationCommand]] = None):
|
112
|
+
"""
|
113
|
+
Charge toutes les commandes du bot sur Discord
|
114
|
+
"""
|
115
|
+
run_coroutine_threadsafe(self.__reload_commands(commands=commands), self.__loop).result(timeout=30)
|
116
|
+
|
117
|
+
|
118
|
+
@property
|
119
|
+
def is_running(self) -> bool:
|
120
|
+
"""Renvoie si la Websocket est connectée"""
|
121
|
+
return not self.__bot.is_closed()
|
122
|
+
|
123
|
+
@property
|
124
|
+
def is_ready(self) -> bool:
|
125
|
+
"""
|
126
|
+
Renvoie si le bot est ready
|
127
|
+
"""
|
128
|
+
return self.__bot.is_ready()
|
129
|
+
|
130
|
+
@property
|
131
|
+
def is_ws_ratelimited(self) -> bool:
|
132
|
+
"""
|
133
|
+
Renvoie si le bot est rate limit
|
134
|
+
"""
|
135
|
+
return self.__bot.is_ws_ratelimited()
|
136
|
+
|
137
|
+
|
138
|
+
async def __stop_bot_in_thread(self):
|
139
|
+
"""
|
140
|
+
Clear le cache du bot de manière asynchrone
|
141
|
+
"""
|
142
|
+
await self.__bot.close()
|
143
|
+
|
144
|
+
def close_ascyncio_loop(self):
|
145
|
+
"""
|
146
|
+
Ferme la boucle asyncio
|
147
|
+
"""
|
148
|
+
if self.__loop.is_running():
|
149
|
+
self.__loop.stop()
|
150
|
+
|
151
|
+
while self.__loop.is_running():
|
152
|
+
tsleep(0.3)
|
153
|
+
|
154
|
+
self.__loop.close()
|
155
|
+
|
156
|
+
async def __reload_commands(self, commands: Optional[list[ApplicationCommand]]):
|
157
|
+
"""
|
158
|
+
Recharge les commandes quand le bot est ready
|
159
|
+
"""
|
160
|
+
if self.__running_bot is not None:
|
161
|
+
while not self.is_ready:
|
162
|
+
await sleep(0.3)
|
163
|
+
await self.__bot.register_commands(commands=commands, method='individual', force=False)
|
164
|
+
else:
|
165
|
+
raise BotNotStartedError(self.__token)
|
166
|
+
|
167
|
+
def __call_setup_function_in_command_file(self, file: str, module, setup_function: str, reload_command: bool):
|
168
|
+
"""
|
169
|
+
Appel la fonction de setup du module pour charger toutes les commandes du bot
|
170
|
+
:param file: Le chemin d'accès du fichier de commandes
|
171
|
+
:param module: Le module préchargé
|
172
|
+
:param setup_function: Le nom de la fonction de setup
|
173
|
+
:param reload_command: Si les commandes doivent être recharger sur le bot (envoie une requête à Discord) automatiquement
|
174
|
+
"""
|
175
|
+
if hasattr(module, setup_function): # si la fonction setup (ou autre) est dans le package
|
176
|
+
getattr(module, setup_function)(self.__bot)
|
177
|
+
|
178
|
+
########## permet de modifier le dictionnaire des modules importés si celui-ci existe déjà, sinon il l'ajoute à la liste des dictionaires des modules importés. Utile car on reload tout si un des modules est modifié
|
179
|
+
find = False
|
180
|
+
for mod in self.__imported_module:
|
181
|
+
if mod['module'].__name__ == module.__name__:
|
182
|
+
mod['setup_function'] = setup_function
|
183
|
+
find = True
|
184
|
+
break
|
185
|
+
|
186
|
+
if not find:
|
187
|
+
self.__imported_module.append({'setup_function': setup_function, 'module': module})
|
188
|
+
##########
|
189
|
+
|
190
|
+
if reload_command:
|
191
|
+
self.reload_commands()
|
192
|
+
else:
|
193
|
+
raise SetupCommandFunctionNotFound(setup_function, file)
|
194
|
+
|
pycordViews/multibot/errors.py
CHANGED
@@ -1,10 +1,30 @@
|
|
1
1
|
class MultibotError(Exception):
|
2
2
|
pass
|
3
3
|
|
4
|
+
class BotAlreadyExistError(MultibotError):
|
5
|
+
def __init__(self, bot_name: str):
|
6
|
+
super().__init__(f"'{bot_name}' bot already exist !")
|
4
7
|
|
5
8
|
class BotNotFoundError(MultibotError):
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
9
|
+
def __init__(self, bot_name: str):
|
10
|
+
super().__init__(f"'{bot_name}' bot doesn't exist !")
|
11
|
+
|
12
|
+
class BotNotStartedError(MultibotError):
|
13
|
+
def __init__(self, bot_name: str):
|
14
|
+
super().__init__(f"'{bot_name}' not started !")
|
15
|
+
|
16
|
+
class BotNotReadyedError(MultibotError):
|
17
|
+
def __init__(self, bot_name: str):
|
18
|
+
super().__init__(f"'{bot_name}' not ready !")
|
19
|
+
|
20
|
+
class BotAlreadyStartedError(MultibotError):
|
21
|
+
def __init__(self, bot_name: str):
|
22
|
+
super().__init__(f"'{bot_name}' already started !")
|
23
|
+
|
24
|
+
class SetupCommandFunctionNotFound(MultibotError):
|
25
|
+
def __init__(self, setup_command_name: str, file: str):
|
26
|
+
super().__init__(f"'{setup_command_name}' function not found in '{file}' file ! Init commands impossible.")
|
27
|
+
|
28
|
+
class CommandFileNotFoundError(MultibotError):
|
29
|
+
def __init__(self, file_name: str):
|
30
|
+
super().__init__(f"'{file_name}' file not found ! Init commands impossible.")
|
@@ -0,0 +1,206 @@
|
|
1
|
+
from multiprocessing import get_context
|
2
|
+
from multiprocessing.queues import Queue
|
3
|
+
from .process import ManageProcess
|
4
|
+
from discord import Intents
|
5
|
+
from sys import platform
|
6
|
+
from typing import Union
|
7
|
+
|
8
|
+
|
9
|
+
class Multibot:
|
10
|
+
|
11
|
+
def __init__(self, global_timeout: int = 30):
|
12
|
+
"""
|
13
|
+
Get instance to run few Discord bot
|
14
|
+
"""
|
15
|
+
if platform == 'win32':
|
16
|
+
ctx = get_context("spawn")
|
17
|
+
else:
|
18
|
+
ctx = get_context("forkserver")
|
19
|
+
self.__main_queue: Queue = ctx.Queue()
|
20
|
+
self.__process_queue: Queue = ctx.Queue()
|
21
|
+
# Création du processus gérant les bots
|
22
|
+
self.__DiscordProcess = ctx.Process(target=self._start_process)
|
23
|
+
self.__DiscordProcess.start()
|
24
|
+
|
25
|
+
self.global_timeout = global_timeout
|
26
|
+
|
27
|
+
def __get_data_queue(self) -> Union[list[dict], dict, None]:
|
28
|
+
"""
|
29
|
+
Récupère les données dans la queue processus
|
30
|
+
"""
|
31
|
+
#try:
|
32
|
+
result = self.__process_queue.get(timeout=self.global_timeout)
|
33
|
+
return result
|
34
|
+
#except:
|
35
|
+
#return None
|
36
|
+
|
37
|
+
def _start_process(self):
|
38
|
+
"""
|
39
|
+
Initialise et exécute le gestionnaire de processus.
|
40
|
+
"""
|
41
|
+
manager = ManageProcess(self.__main_queue, self.__process_queue)
|
42
|
+
manager.run()
|
43
|
+
|
44
|
+
def add_bot(self, bot_name: str, token: str, intents: Intents):
|
45
|
+
"""
|
46
|
+
Add a bot in the process
|
47
|
+
:param bot_name: Bot name
|
48
|
+
:param token: Token bot
|
49
|
+
:param intents: Intents bot to Intents discord class
|
50
|
+
"""
|
51
|
+
self.__main_queue.put({"type": "ADD", "bot_name": bot_name, "token": token, 'intents': intents})
|
52
|
+
response = self.__get_data_queue()
|
53
|
+
return response # Retourne le statut de l'ajout
|
54
|
+
|
55
|
+
def remove_bot(self, bot_name: str) -> dict[str, str]:
|
56
|
+
"""
|
57
|
+
Shutdown and remove à bot
|
58
|
+
:param bot_name: Bot name to remove
|
59
|
+
"""
|
60
|
+
self.__main_queue.put({"type": "REMOVE", "bot_name": bot_name})
|
61
|
+
response = self.__get_data_queue()
|
62
|
+
return response # Retourne le statut de la suppression
|
63
|
+
|
64
|
+
def start(self, *bot_names: str) -> list[dict[str, str]]:
|
65
|
+
"""
|
66
|
+
Start bots
|
67
|
+
:param bot_names: Bots name to start
|
68
|
+
:return: List of data bot status
|
69
|
+
"""
|
70
|
+
results = []
|
71
|
+
for bot_name in bot_names:
|
72
|
+
self.__main_queue.put({'type': "START", 'bot_name': bot_name})
|
73
|
+
results.append(self.__get_data_queue())
|
74
|
+
return results
|
75
|
+
|
76
|
+
def stop(self, *bot_names: str) -> list[dict[str, str]]:
|
77
|
+
"""
|
78
|
+
Stop bots
|
79
|
+
:param bot_names: Bots name to start
|
80
|
+
:return: Data status dict
|
81
|
+
"""
|
82
|
+
results = []
|
83
|
+
for bot_name in bot_names:
|
84
|
+
self.__main_queue.put({'type': "STOP", 'bot_name': bot_name})
|
85
|
+
results.append(self.__get_data_queue())
|
86
|
+
return results
|
87
|
+
|
88
|
+
def start_all(self) -> list[dict[str, list[str]]]:
|
89
|
+
"""
|
90
|
+
Start all bots in the process.
|
91
|
+
"""
|
92
|
+
self.__main_queue.put({'type': "STARTALL"})
|
93
|
+
return self.__get_data_queue()
|
94
|
+
|
95
|
+
def stop_all(self) -> list[dict[str, list[str]]]:
|
96
|
+
"""
|
97
|
+
Stop all bots in the process.
|
98
|
+
This function is slow ! It's shutdown all bots properly.
|
99
|
+
"""
|
100
|
+
self.__main_queue.put({'type': "STOPALL"})
|
101
|
+
return self.__get_data_queue()
|
102
|
+
|
103
|
+
def is_started(self, bot_name: str) -> bool:
|
104
|
+
"""
|
105
|
+
Return the current Websocket connexion status
|
106
|
+
:param bot_name: Bot name
|
107
|
+
:return: True if the Websocket is online, else False
|
108
|
+
"""
|
109
|
+
self.__main_queue.put({'type': "IS_STARTED", 'bot_name': bot_name})
|
110
|
+
return self.__get_data_queue()['message']
|
111
|
+
|
112
|
+
def is_ready(self, bot_name: str) -> bool:
|
113
|
+
"""
|
114
|
+
Return the current bot connexion status
|
115
|
+
:param bot_name: Bot name
|
116
|
+
:return: True if the bot if ready, else False
|
117
|
+
"""
|
118
|
+
self.__main_queue.put({'type': "IS_READY", 'bot_name': bot_name})
|
119
|
+
return self.__get_data_queue()['message']
|
120
|
+
|
121
|
+
def is_ws_ratelimited(self, bot_name: str) -> bool:
|
122
|
+
"""
|
123
|
+
Get the current ratelimit status of the bot
|
124
|
+
:param bot_name: Bot name
|
125
|
+
:return: True if the bot was ratelimited, else False
|
126
|
+
"""
|
127
|
+
self.__main_queue.put({'type': "IS_WS_RATELIMITED", 'bot_name': bot_name})
|
128
|
+
return self.__get_data_queue()['message']
|
129
|
+
|
130
|
+
def reload_commands(self, *bot_names: str) -> list[dict[str, str]]:
|
131
|
+
"""
|
132
|
+
Reload all commands for each bot when bots are ready
|
133
|
+
:param bot_names: Bots name to reload commands
|
134
|
+
"""
|
135
|
+
result = []
|
136
|
+
for name in bot_names:
|
137
|
+
self.__main_queue.put({'type': "RELOAD_COMMANDS", 'name': name})
|
138
|
+
result.append(self.__get_data_queue())
|
139
|
+
return result
|
140
|
+
|
141
|
+
def add_pyFile_commands(self, bot_name: str, file: str, setup_function: str = 'setup', reload_command: bool = True) -> dict[str, str]:
|
142
|
+
"""
|
143
|
+
Add and load a command bot file and dependencies.
|
144
|
+
Files must have a function called ‘setup’ or an equivalent passed as a parameter.
|
145
|
+
|
146
|
+
def setup(bot: Bot):
|
147
|
+
...
|
148
|
+
|
149
|
+
:param bot_name: The bot's name to add commands file
|
150
|
+
:param file: Relative or absolute commands file's path
|
151
|
+
:param setup_function: Function name called by the process to give the Bot instance.
|
152
|
+
:param reload_command: Reload all command in the fil and dependencies. Default : True
|
153
|
+
"""
|
154
|
+
self.__main_queue.put({'type': "ADD_COMMAND_FILE",
|
155
|
+
'bot_name': bot_name,
|
156
|
+
'file': file,
|
157
|
+
'setup_function': setup_function,
|
158
|
+
'reload_command': reload_command})
|
159
|
+
return self.__get_data_queue()
|
160
|
+
|
161
|
+
def modify_pyFile_commands(self, bot_name: str, file: str, setup_function: str = 'setup') -> dict[str, str]:
|
162
|
+
|
163
|
+
"""
|
164
|
+
Modifies a file of commands and reloads it.
|
165
|
+
Reloads only the file, not the bot commands!
|
166
|
+
:param bot_name: The bot's name
|
167
|
+
:param file: The file's relative or absolute path
|
168
|
+
"""
|
169
|
+
|
170
|
+
self.__main_queue.put({'type': "MODIFY_COMMAND_FILE",
|
171
|
+
'bot_name': bot_name,
|
172
|
+
'file': file,
|
173
|
+
'setup_function': setup_function})
|
174
|
+
return self.__get_data_queue()
|
175
|
+
|
176
|
+
@property
|
177
|
+
def bot_count(self) -> int:
|
178
|
+
"""
|
179
|
+
Return the total number of bots
|
180
|
+
"""
|
181
|
+
self.__main_queue.put({'type': "BOT_COUNT"})
|
182
|
+
return self.__get_data_queue()['message']
|
183
|
+
|
184
|
+
@property
|
185
|
+
def started_bot_count(self) -> int:
|
186
|
+
"""
|
187
|
+
Return the total number of started bots
|
188
|
+
"""
|
189
|
+
self.__main_queue.put({'type': "STARTED_BOT_COUNT"})
|
190
|
+
return self.__get_data_queue()['message']
|
191
|
+
|
192
|
+
@property
|
193
|
+
def shutdown_bot_count(self) -> int:
|
194
|
+
"""
|
195
|
+
Return the total number of shutdown bots
|
196
|
+
"""
|
197
|
+
self.__main_queue.put({'type': "SHUTDOWN_BOT_COUNT"})
|
198
|
+
return self.__get_data_queue()['message']
|
199
|
+
|
200
|
+
@property
|
201
|
+
def get_bots_name(self) -> list[str]:
|
202
|
+
"""
|
203
|
+
Return all bots name (not real name of bots)
|
204
|
+
"""
|
205
|
+
self.__main_queue.put({'type': "BOTS_NAME"})
|
206
|
+
return self.__get_data_queue()['message']
|