-
-
Notifications
You must be signed in to change notification settings - Fork 2.4k
Expand file tree
/
Copy pathbot.py
More file actions
2513 lines (2118 loc) · 88.7 KB
/
bot.py
File metadata and controls
2513 lines (2118 loc) · 88.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
from __future__ import annotations
import asyncio
import inspect
import logging
import os
import platform
import shutil
import sys
import contextlib
import weakref
import functools
from collections import namedtuple, OrderedDict
from datetime import datetime
from importlib.machinery import ModuleSpec
from pathlib import Path
from typing import (
Optional,
Union,
List,
Iterable,
Dict,
NoReturn,
Set,
TypeVar,
Callable,
Awaitable,
Any,
Literal,
MutableMapping,
Set,
overload,
TYPE_CHECKING,
)
from types import MappingProxyType
import discord
from discord.ext import commands as dpy_commands
from discord.ext.commands import when_mentioned_or
from . import Config, _i18n, i18n, app_commands, commands, errors, _drivers, modlog, bank
from ._cli import ExitCodes
from ._cog_manager import CogManager, CogManagerUI
from .core_commands import Core
from .data_manager import cog_data_path
from .dev_commands import Dev
from ._events import init_events
from ._global_checks import init_global_checks
from ._settings_caches import (
PrefixManager,
IgnoreManager,
WhitelistBlacklistManager,
DisabledCogCache,
I18nManager,
)
from .utils.predicates import MessagePredicate
from ._rpc import RPCMixin
from .tree import RedTree
from .utils import can_user_send_messages_in, common_filters, AsyncIter
from .utils.chat_formatting import box, text_to_file
from .utils._internal_utils import send_to_owners_with_prefix_replaced
if TYPE_CHECKING:
from discord.ext.commands.hybrid import CommandCallback, ContextT, P
_T = TypeVar("_T")
CUSTOM_GROUPS = "CUSTOM_GROUPS"
COMMAND_SCOPE = "COMMAND"
SHARED_API_TOKENS = "SHARED_API_TOKENS"
log = logging.getLogger("red")
__all__ = ("Red",)
NotMessage = namedtuple("NotMessage", "guild")
DataDeletionResults = namedtuple("DataDeletionResults", "failed_modules failed_cogs unhandled")
PreInvokeCoroutine = Callable[[commands.Context], Awaitable[Any]]
T_BIC = TypeVar("T_BIC", bound=PreInvokeCoroutine)
UserOrRole = Union[int, discord.Role, discord.Member, discord.User]
_ = i18n.Translator("Core", __file__)
def _is_submodule(parent, child):
return parent == child or child.startswith(parent + ".")
class _NoOwnerSet(RuntimeError):
"""Raised when there is no owner set for the instance that is trying to start."""
# Order of inheritance here matters.
# d.py autoshardedbot should be at the end
# all of our mixins should happen before,
# and must include a call to super().__init__ unless they do not provide an init
class Red(
commands.GroupMixin, RPCMixin, dpy_commands.bot.AutoShardedBot
): # pylint: disable=no-member # barely spurious warning caused by shadowing
"""Our subclass of discord.ext.commands.AutoShardedBot"""
def __init__(self, *args, cli_flags=None, bot_dir: Path = Path.cwd(), **kwargs):
self._shutdown_mode = ExitCodes.CRITICAL
self._cli_flags = cli_flags
self._config = Config.get_core_conf(force_registration=False)
self.rpc_enabled = cli_flags.rpc
self.rpc_port = cli_flags.rpc_port
self._last_exception = None
self._config.register_global(
token=None,
prefix=[],
packages=[],
owner=None,
whitelist=[],
blacklist=[],
locale="en-US",
regional_format=None,
embeds=True,
color=15158332,
fuzzy=False,
custom_info=None,
help__page_char_limit=1000,
help__max_pages_in_guild=2,
help__delete_delay=0,
help__use_menus=0,
help__show_hidden=False,
help__show_aliases=True,
help__verify_checks=True,
help__verify_exists=False,
help__tagline="",
help__use_tick=False,
help__react_timeout=30,
description="Red V3",
invite_public=False,
invite_perm=0,
invite_commands_scope=False,
disabled_commands=[],
disabled_command_msg="That command is disabled.",
invoke_error_msg=None,
extra_owner_destinations=[],
owner_opt_out_list=[],
last_system_info__python_version=[3, 7],
last_system_info__machine=None,
last_system_info__system=None,
schema_version=0,
datarequests__allow_user_requests=True,
datarequests__user_requests_are_strict=True,
use_buttons=False,
enabled_slash_commands={},
enabled_user_commands={},
enabled_message_commands={},
)
self._config.register_guild(
prefix=[],
whitelist=[],
blacklist=[],
admin_role=[],
mod_role=[],
embeds=None,
ignored=False,
use_bot_color=False,
fuzzy=False,
disabled_commands=[],
autoimmune_ids=[],
delete_delay=-1,
locale=None,
regional_format=None,
)
self._config.register_channel(embeds=None, ignored=False)
self._config.register_user(embeds=None)
self._config.init_custom("COG_DISABLE_SETTINGS", 2)
self._config.register_custom("COG_DISABLE_SETTINGS", disabled=None)
self._config.init_custom(CUSTOM_GROUPS, 2)
self._config.register_custom(CUSTOM_GROUPS)
# {COMMAND_NAME: {GUILD_ID: {...}}}
# GUILD_ID=0 for global setting
self._config.init_custom(COMMAND_SCOPE, 2)
self._config.register_custom(COMMAND_SCOPE, embeds=None)
# TODO: add cache for embed settings
self._config.init_custom(SHARED_API_TOKENS, 2)
self._config.register_custom(SHARED_API_TOKENS)
self._prefix_cache = PrefixManager(self._config, cli_flags)
self._disabled_cog_cache = DisabledCogCache(self._config)
self._ignored_cache = IgnoreManager(self._config)
self._whiteblacklist_cache = WhitelistBlacklistManager(self._config)
self._i18n_cache = I18nManager(self._config)
self._bypass_cooldowns = False
async def prefix_manager(bot, message) -> List[str]:
prefixes = await self._prefix_cache.get_prefixes(message.guild)
if cli_flags.mentionable:
return when_mentioned_or(*prefixes)(bot, message)
return prefixes
if "command_prefix" not in kwargs:
kwargs["command_prefix"] = prefix_manager
if "owner_id" in kwargs:
raise RuntimeError("Red doesn't accept owner_id kwarg, use owner_ids instead.")
if "intents" not in kwargs:
intents = discord.Intents.all()
for intent_name in cli_flags.disable_intent:
setattr(intents, intent_name, False)
kwargs["intents"] = intents
self._owner_id_overwrite = cli_flags.owner
if "owner_ids" in kwargs:
kwargs["owner_ids"] = set(kwargs["owner_ids"])
else:
kwargs["owner_ids"] = set()
kwargs["owner_ids"].update(cli_flags.co_owner)
if "command_not_found" not in kwargs:
kwargs["command_not_found"] = "Command {} not found.\n{}"
if "allowed_mentions" not in kwargs:
kwargs["allowed_mentions"] = discord.AllowedMentions(everyone=False, roles=False)
message_cache_size = cli_flags.message_cache_size
if cli_flags.no_message_cache:
message_cache_size = None
kwargs["max_messages"] = message_cache_size
self._max_messages = message_cache_size
self._uptime = None
self._checked_time_accuracy = None
self._main_dir = bot_dir
self._cog_mgr = CogManager()
self._use_team_features = cli_flags.use_team_features
super().__init__(*args, help_command=None, tree_cls=RedTree, **kwargs)
# Do not manually use the help formatter attribute here, see `send_help_for`,
# for a documented API. The internals of this object are still subject to change.
self._help_formatter = commands.help.RedHelpFormatter()
self.add_command(commands.help.red_help)
self._permissions_hooks: List[commands.CheckPredicate] = []
self._red_ready = asyncio.Event()
self._red_before_invoke_objs: Set[PreInvokeCoroutine] = set()
self._deletion_requests: MutableMapping[int, asyncio.Lock] = weakref.WeakValueDictionary()
def set_help_formatter(self, formatter: commands.help.HelpFormatterABC):
"""
Set's Red's help formatter.
.. warning::
This method is `provisional <developer-guarantees-exclusions>`.
The formatter must implement all methods in
``commands.help.HelpFormatterABC``
Cogs which set a help formatter should inform users of this.
Users should not use multiple cogs which set a help formatter.
This should specifically be an instance of a formatter.
This allows cogs to pass a config object or similar to the
formatter prior to the bot using it.
See ``commands.help.HelpFormatterABC`` for more details.
Raises
------
RuntimeError
If the default formatter has already been replaced
TypeError
If given an invalid formatter
"""
if not isinstance(formatter, commands.help.HelpFormatterABC):
raise TypeError(
"Help formatters must inherit from `commands.help.HelpFormatterABC` "
"and implement the required interfaces."
)
# do not switch to isinstance, we want to know that this has not been overridden,
# even with a subclass.
if type(self._help_formatter) is commands.help.RedHelpFormatter:
self._help_formatter = formatter
else:
raise RuntimeError("The formatter has already been overridden.")
def reset_help_formatter(self):
"""
Resets Red's help formatter.
.. warning::
This method is `provisional <developer-guarantees-exclusions>`.
This exists for use in ``cog_unload`` for cogs which replace the formatter
as well as for a rescue command in core_commands.
"""
self._help_formatter = commands.help.RedHelpFormatter()
def add_dev_env_value(self, name: str, value: Callable[[commands.Context], Any]):
"""
Add a custom variable to the dev environment (``[p]debug``, ``[p]eval``, and ``[p]repl`` commands).
If dev mode is disabled, nothing will happen.
Example
-------
.. code-block:: python
class MyCog(commands.Cog):
def __init__(self, bot):
self.bot = bot
bot.add_dev_env_value("mycog", lambda ctx: self)
bot.add_dev_env_value("mycogdata", lambda ctx: self.settings[ctx.guild.id])
def cog_unload(self):
self.bot.remove_dev_env_value("mycog")
self.bot.remove_dev_env_value("mycogdata")
Once your cog is loaded, the custom variables ``mycog`` and ``mycogdata``
will be included in the environment of dev commands.
Parameters
----------
name: str
The name of your custom variable.
value: Callable[[commands.Context], Any]
The function returning the value of the variable.
It must take a `commands.Context` as its sole parameter
Raises
------
TypeError
``value`` argument isn't a callable.
ValueError
The passed callable takes no or more than one argument.
RuntimeError
The name of the custom variable is either reserved by a variable
from the default environment or already taken by some other custom variable.
"""
signature = inspect.signature(value)
if len(signature.parameters) != 1:
raise ValueError("Callable must take exactly one argument for context")
dev = self.get_cog("Dev")
if dev is None:
return
if name in [
"bot",
"ctx",
"channel",
"author",
"guild",
"message",
"asyncio",
"aiohttp",
"discord",
"commands",
"_",
"__name__",
"__builtins__",
]:
raise RuntimeError(f"The name {name} is reserved for default environment.")
if name in dev.env_extensions:
raise RuntimeError(f"The name {name} is already used.")
dev.env_extensions[name] = value
def remove_dev_env_value(self, name: str):
"""
Remove a custom variable from the dev environment.
Parameters
----------
name: str
The name of the custom variable.
Raises
------
KeyError
The custom variable was never set.
"""
dev = self.get_cog("Dev")
if dev is None:
return
del dev.env_extensions[name]
def get_command(self, name: str, /) -> Optional[commands.Command]:
com = super().get_command(name)
assert com is None or isinstance(com, commands.Command)
return com
def get_cog(self, name: str, /) -> Optional[commands.Cog]:
cog = super().get_cog(name)
assert cog is None or isinstance(cog, commands.Cog)
return cog
@property
def _before_invoke(self): # DEP-WARN
return self._red_before_invoke_method
@_before_invoke.setter
def _before_invoke(self, val): # DEP-WARN
"""Prevent this from being overwritten in super().__init__"""
pass
async def _red_before_invoke_method(self, ctx):
await self.wait_until_red_ready()
return_exceptions = isinstance(ctx.command, commands.commands._RuleDropper)
if self._red_before_invoke_objs:
await asyncio.gather(
*(coro(ctx) for coro in self._red_before_invoke_objs),
return_exceptions=return_exceptions,
)
async def cog_disabled_in_guild(
self, cog: commands.Cog, guild: Optional[discord.Guild]
) -> bool:
"""
Check if a cog is disabled in a guild
Parameters
----------
cog: commands.Cog
guild: Optional[discord.Guild]
Returns
-------
bool
"""
if guild is None:
return False
return await self._disabled_cog_cache.cog_disabled_in_guild(cog.qualified_name, guild.id)
async def cog_disabled_in_guild_raw(self, cog_name: str, guild_id: int) -> bool:
"""
Check if a cog is disabled in a guild without the cog or guild object
Parameters
----------
cog_name: str
This should be the cog's qualified name, not necessarily the classname
guild_id: int
Returns
-------
bool
"""
return await self._disabled_cog_cache.cog_disabled_in_guild(cog_name, guild_id)
def remove_before_invoke_hook(self, coro: PreInvokeCoroutine) -> None:
"""
Functional method to remove a `before_invoke` hook.
"""
self._red_before_invoke_objs.discard(coro)
def before_invoke(self, coro: T_BIC, /) -> T_BIC:
"""
Overridden decorator method for Red's ``before_invoke`` behavior.
This can safely be used purely functionally as well.
3rd party cogs should remove any hooks which they register at unload
using `remove_before_invoke_hook`
Below behavior shared with discord.py:
.. note::
The ``before_invoke`` hooks are
only called if all checks and argument parsing procedures pass
without error. If any check or argument parsing procedures fail
then the hooks are not called.
Parameters
----------
coro: Callable[[commands.Context], Awaitable[Any]]
The coroutine to register as the pre-invoke hook.
Raises
------
TypeError
The coroutine passed is not actually a coroutine.
"""
if not asyncio.iscoroutinefunction(coro):
raise TypeError("The pre-invoke hook must be a coroutine.")
self._red_before_invoke_objs.add(coro)
return coro
async def before_identify_hook(self, shard_id, *, initial=False):
"""A hook that is called before IDENTIFYing a session.
Same as in discord.py, but also dispatches "on_red_identify" bot event."""
self.dispatch("red_before_identify", shard_id, initial)
return await super().before_identify_hook(shard_id, initial=initial)
@property
def cog_mgr(self) -> NoReturn:
raise AttributeError("Please don't mess with the cog manager internals.")
@property
def uptime(self) -> datetime:
"""Allow access to the value, but we don't want cog creators setting it"""
return self._uptime
@uptime.setter
def uptime(self, value) -> NoReturn:
raise RuntimeError(
"Hey, we're cool with sharing info about the uptime, but don't try and assign to it please."
)
@property
def db(self) -> NoReturn:
raise AttributeError(
"We really don't want you touching the bot config directly. "
"If you need something in here, take a look at the exposed methods "
"and use the one which corresponds to your needs or "
"open an issue if you need an additional method for your use case."
)
@property
def counter(self) -> NoReturn:
raise AttributeError(
"Please make your own counter object by importing ``Counter`` from ``collections``."
)
@property
def color(self) -> NoReturn:
raise AttributeError("Please fetch the embed color with `get_embed_color`")
@property
def colour(self) -> NoReturn:
raise AttributeError("Please fetch the embed colour with `get_embed_colour`")
@property
def max_messages(self) -> Optional[int]:
return self._max_messages
async def add_to_blacklist(
self, users_or_roles: Iterable[UserOrRole], *, guild: Optional[discord.Guild] = None
):
"""
Add users or roles to the global or local blocklist.
Parameters
----------
users_or_roles : Iterable[Union[int, discord.Role, discord.Member, discord.User]]
The items to add to the blocklist.
Roles and role IDs should only be passed when updating a local blocklist.
guild : Optional[discord.Guild]
The guild, whose local blocklist should be modified.
If not passed, the global blocklist will be modified.
Raises
------
TypeError
The values passed were not of the proper type.
"""
to_add: Set[int] = {getattr(uor, "id", uor) for uor in users_or_roles}
await self._whiteblacklist_cache.add_to_blacklist(guild, to_add)
async def remove_from_blacklist(
self, users_or_roles: Iterable[UserOrRole], *, guild: Optional[discord.Guild] = None
):
"""
Remove users or roles from the global or local blocklist.
Parameters
----------
users_or_roles : Iterable[Union[int, discord.Role, discord.Member, discord.User]]
The items to remove from the blocklist.
Roles and role IDs should only be passed when updating a local blocklist.
guild : Optional[discord.Guild]
The guild, whose local blocklist should be modified.
If not passed, the global blocklist will be modified.
Raises
------
TypeError
The values passed were not of the proper type.
"""
to_remove: Set[int] = {getattr(uor, "id", uor) for uor in users_or_roles}
await self._whiteblacklist_cache.remove_from_blacklist(guild, to_remove)
async def get_blacklist(self, guild: Optional[discord.Guild] = None) -> Set[int]:
"""
Get the global or local blocklist.
Parameters
----------
guild : Optional[discord.Guild]
The guild to get the local blocklist for.
If this is not passed, the global blocklist will be returned.
Returns
-------
Set[int]
The IDs of the blocked users/roles.
"""
return await self._whiteblacklist_cache.get_blacklist(guild)
async def clear_blacklist(self, guild: Optional[discord.Guild] = None):
"""
Clears the global or local blocklist.
Parameters
----------
guild : Optional[discord.Guild]
The guild, whose local blocklist should be cleared.
If not passed, the global blocklist will be cleared.
"""
await self._whiteblacklist_cache.clear_blacklist(guild)
async def add_to_whitelist(
self, users_or_roles: Iterable[UserOrRole], *, guild: Optional[discord.Guild] = None
):
"""
Add users or roles to the global or local allowlist.
Parameters
----------
users_or_roles : Iterable[Union[int, discord.Role, discord.Member, discord.User]]
The items to add to the allowlist.
Roles and role IDs should only be passed when updating a local allowlist.
guild : Optional[discord.Guild]
The guild, whose local allowlist should be modified.
If not passed, the global allowlist will be modified.
Raises
------
TypeError
The passed values were not of the proper type.
"""
to_add: Set[int] = {getattr(uor, "id", uor) for uor in users_or_roles}
await self._whiteblacklist_cache.add_to_whitelist(guild, to_add)
async def remove_from_whitelist(
self, users_or_roles: Iterable[UserOrRole], *, guild: Optional[discord.Guild] = None
):
"""
Remove users or roles from the global or local allowlist.
Parameters
----------
users_or_roles : Iterable[Union[int, discord.Role, discord.Member, discord.User]]
The items to remove from the allowlist.
Roles and role IDs should only be passed when updating a local allowlist.
guild : Optional[discord.Guild]
The guild, whose local allowlist should be modified.
If not passed, the global allowlist will be modified.
Raises
------
TypeError
The passed values were not of the proper type.
"""
to_remove: Set[int] = {getattr(uor, "id", uor) for uor in users_or_roles}
await self._whiteblacklist_cache.remove_from_whitelist(guild, to_remove)
async def get_whitelist(self, guild: Optional[discord.Guild] = None):
"""
Get the global or local allowlist.
Parameters
----------
guild : Optional[discord.Guild]
The guild to get the local allowlist for.
If this is not passed, the global allowlist will be returned.
Returns
-------
Set[int]
The IDs of the allowed users/roles.
"""
return await self._whiteblacklist_cache.get_whitelist(guild)
async def clear_whitelist(self, guild: Optional[discord.Guild] = None):
"""
Clears the global or local allowlist.
Parameters
----------
guild : Optional[discord.Guild]
The guild, whose local allowlist should be cleared.
If not passed, the global allowlist will be cleared.
"""
await self._whiteblacklist_cache.clear_whitelist(guild)
async def allowed_by_whitelist_blacklist(
self,
who: Optional[Union[discord.Member, discord.User]] = None,
*,
who_id: Optional[int] = None,
guild: Optional[discord.Guild] = None,
role_ids: Optional[List[int]] = None,
) -> bool:
"""
This checks if a user or member is allowed to run things,
as considered by Red's allowlist and blocklist.
If given a user object, this function will check the global lists
If given a member, this will additionally check guild lists
If omitting a user or member, you must provide a value for ``who_id``
You may also provide a value for ``guild`` in this case
If providing a member by guild and member ids,
you should supply ``role_ids`` as well
Parameters
----------
who : Optional[Union[discord.Member, discord.User]]
The user or member object to check
Other Parameters
----------------
who_id : Optional[int]
The id of the user or member to check
If not providing a value for ``who``, this is a required parameter.
guild : Optional[discord.Guild]
When used in conjunction with a provided value for ``who_id``, checks
the lists for the corresponding guild as well.
This is ignored when ``who`` is passed.
role_ids : Optional[List[int]]
When used with both ``who_id`` and ``guild``, checks the role ids provided.
This is required for accurate checking of members in a guild if providing ids.
This is ignored when ``who`` is passed.
Raises
------
TypeError
Did not provide ``who`` or ``who_id``
Returns
-------
bool
`True` if user is allowed to run things, `False` otherwise
"""
# Contributor Note:
# All config calls are delayed until needed in this section
# All changes should be made keeping in mind that this is also used as a global check
mocked = False # used for an accurate delayed role id expansion later.
if not who:
if not who_id:
raise TypeError("Must provide a value for either `who` or `who_id`")
mocked = True
who = discord.Object(id=who_id)
else:
guild = getattr(who, "guild", None)
if await self.is_owner(who):
return True
global_whitelist = await self.get_whitelist()
if global_whitelist:
if who.id not in global_whitelist:
return False
else:
# blacklist is only used when whitelist doesn't exist.
global_blacklist = await self.get_blacklist()
if who.id in global_blacklist:
return False
if guild:
if guild.owner_id == who.id:
return True
# The delayed expansion of ids to check saves time in the DM case.
# Converting to a set reduces the total lookup time in section
if mocked:
ids = {i for i in (who.id, *(role_ids or [])) if i != guild.id}
else:
# DEP-WARN
# This uses member._roles (getattr is for the user case)
# If this is removed upstream (undocumented)
# there is a silent failure potential, and role blacklist/whitelists will break.
ids = {i for i in (who.id, *(getattr(who, "_roles", []))) if i != guild.id}
guild_whitelist = await self.get_whitelist(guild)
if guild_whitelist:
if ids.isdisjoint(guild_whitelist):
return False
else:
guild_blacklist = await self.get_blacklist(guild)
if not ids.isdisjoint(guild_blacklist):
return False
return True
async def message_eligible_as_command(self, message: discord.Message) -> bool:
"""
Runs through the things which apply globally about commands
to determine if a message may be responded to as a command.
This can't interact with permissions as permissions is hyper-local
with respect to command objects, create command objects for this
if that's needed.
This also does not check for prefix or command conflicts,
as it is primarily designed for non-prefix based response handling
via on_message_without_command
Parameters
----------
message
The message object to check
Returns
-------
bool
Whether or not the message is eligible to be treated as a command.
"""
channel = message.channel
guild = message.guild
if message.author.bot:
return False
# We do not consider messages with PartialMessageable channel as eligible.
# See `process_commands()` for our handling of it.
# 27-03-2023: Addendum, DMs won't run into the same issues that guild partials will,
# so we can safely continue to execute the command.
if (
isinstance(channel, discord.PartialMessageable)
and channel.type is not discord.ChannelType.private
):
return False
if guild:
assert isinstance(
channel,
(discord.TextChannel, discord.VoiceChannel, discord.StageChannel, discord.Thread),
)
if not can_user_send_messages_in(guild.me, channel):
return False
if not (await self.ignored_channel_or_guild(message)):
return False
if not (await self.allowed_by_whitelist_blacklist(message.author)):
return False
return True
async def ignored_channel_or_guild(
self, ctx: Union[commands.Context, discord.Message, discord.Interaction]
) -> bool:
"""
This checks if the bot is meant to be ignoring commands in a channel or guild,
as considered by Red's whitelist and blacklist.
Parameters
----------
ctx :
Context object of the command which needs to be checked prior to invoking
or a Message object which might be intended for use as a command
or an Interaction object which might be intended for use with a command.
Returns
-------
bool
`True` if commands are allowed in the channel, `False` otherwise
Raises
------
TypeError
``ctx.channel`` is a `discord.PartialMessageable` with a ``type`` other
than ``discord.ChannelType.private``
"""
if isinstance(ctx, discord.Interaction):
author = ctx.user
else:
author = ctx.author
is_private = isinstance(ctx.channel, discord.abc.PrivateChannel)
if isinstance(ctx.channel, discord.PartialMessageable):
if ctx.channel.type is not discord.ChannelType.private:
raise TypeError("Can't check permissions for non-private PartialMessageable.")
is_private = True
if isinstance(ctx, discord.Message):
perms = ctx.channel.permissions_for(author)
else:
# `permissions` attribute will use permissions from the interaction when possible,
# or `ctx.channel.permissions_for(author)` for non-interaction contexts.
perms = ctx.permissions
surpass_ignore = (
is_private
or perms.manage_guild
or await self.is_owner(author)
or await self.is_admin(author)
)
# guild-wide checks
if surpass_ignore:
return True
guild_ignored = await self._ignored_cache.get_ignored_guild(ctx.guild)
if guild_ignored:
return False
# (parent) channel checks
if perms.manage_channels:
return True
if isinstance(ctx.channel, discord.Thread):
if isinstance(ctx, discord.Interaction) and ctx.is_user_integration():
ctx: discord.Interaction
# This is a user installed interaction, and thus... We're doomed!
# We must mock an object because we don't have the channel cached,
# and we are unable to fetch a full channel from the interaction
# #BlameDiscord, See Red#6501 for more details.
# LIMITATIONS: Due the fact that we don't know the categories either as they aren't...
# communicated in the interaction, we can't check for category ignores.
channel = discord.Object(id=ctx.channel.parent_id)
else:
channel = ctx.channel.parent
thread = ctx.channel
else:
channel = ctx.channel
thread = None
chann_ignored = await self._ignored_cache.get_ignored_channel(channel)
if chann_ignored:
return False
if thread is None:
return True
# thread checks
if perms.manage_threads:
return True
thread_ignored = await self._ignored_cache.get_ignored_channel(
thread,
check_category=False, # already checked for parent
)
return not thread_ignored
async def get_valid_prefixes(self, guild: Optional[discord.Guild] = None) -> List[str]:
"""
This gets the valid prefixes for a guild.
If not provided a guild (or passed None) it will give the DM prefixes.
This is just a fancy wrapper around ``get_prefix``
Parameters
----------
guild : Optional[discord.Guild]
The guild you want prefixes for. Omit (or pass None) for the DM prefixes
Returns
-------
List[str]
If a guild was specified, the valid prefixes in that guild.
If a guild was not specified, the valid prefixes for DMs
"""
return await self.get_prefix(NotMessage(guild))
async def set_prefixes(self, prefixes: List[str], guild: Optional[discord.Guild] = None):
"""
Set global/server prefixes.
If ``guild`` is not provided (or None is passed), this will set the global prefixes.
Parameters
----------
prefixes : List[str]
The prefixes you want to set. Passing empty list will reset prefixes for the ``guild``
guild : Optional[discord.Guild]
The guild you want to set the prefixes for. Omit (or pass None) to set the global prefixes
Raises
------
TypeError
If ``prefixes`` is not a list of strings
ValueError
If empty list is passed to ``prefixes`` when setting global prefixes
"""
await self._prefix_cache.set_prefixes(guild=guild, prefixes=prefixes)
async def get_embed_color(self, location: discord.abc.Messageable) -> discord.Color:
"""
Get the embed color for a location. This takes into account all related settings.
Parameters
----------
location : `discord.abc.Messageable`
Location to check embed color for.
Returns
-------
discord.Color