Migration to discord.py 2.0 (#5600)

* Temporarily set d.py to use latest git revision

* Remove `bot` param to Client.start

* Switch to aware datetimes

A lot of this is removing `.replace(...)` which while not technically
needed, simplifies the code base. There's only a few changes that are
actually necessary here.

* Update to work with new Asset design

* [threads] Update core ModLog API to support threads

- Added proper support for passing `Thread` to `channel`
  when creating/editing case
- Added `parent_channel_id` attribute to Modlog API's Case
    - Added `parent_channel` property that tries to get parent channel
- Updated case's content to show both thread and parent information

* [threads] Disallow usage of threads in some of the commands

- announceset channel
- filter channel clear
- filter channel add
- filter channel remove
- GlobalUniqueObjectFinder converter
    - permissions addglobalrule
    - permissions removeglobalrule
    - permissions removeserverrule
    - Permissions cog does not perform any validation for IDs
      when setting through YAML so that has not been touched
- streamalert twitch/youtube/picarto
- embedset channel
- set ownernotifications adddestination

* [threads] Handle threads in Red's permissions system (Requires)

- Made permissions system apply rules of (only) parent in threads

* [threads] Update embed_requested to support threads

- Threads don't have their own embed settings and inherit from parent

* [threads] Update Red.message_eligible_as_command to support threads

* [threads] Properly handle invocation of [p](un)mutechannel in threads

Usage of a (un)mutechannel will mute/unmute user in the parent channel
if it's invoked in a thread.

* [threads] Update Filter cog to properly handle threads

- `[p]filter channel list` in a threads sends list for parent channel
- Checking for filter hits for a message in a thread checks its parent
  channel's word list. There's no separate word list for threads.

* [threads] Support threads in Audio cog

- Handle threads being notify channels
- Update type hint for `is_query_allowed()`

* [threads] Update type hints and documentation to reflect thread support

- Documented that `{channel}` in CCs might be a thread
- Allowed (documented) usage of threads with `Config.channel()`
    - Separate thread scope is still in the picture though
      if it were to be done, it's going to be in separate in PR
- GuildContext.channel might be Thread

* Use less costy channel check in customcom's on_message_without_command

This isn't needed for d.py 2.0 but whatever...

* Update for in-place edits

* Embed's bool changed behavior, I'm hoping it doesn't affect us

* Address User.permissions_in() removal

* Swap VerificationLevel.extreme with VerificationLevel.highest

* Change to keyword-only parameters

* Change of `Guild.vanity_invite()` return type

* avatar -> display_avatar

* Fix metaclass shenanigans with Converter

* Update Red.add_cog() to be inline with `dpy_commands.Bot.add_cog()`

This means adding `override` keyword-only parameter and causing
small breakage by swapping RuntimeError with discord.ClientException.

* Address all DEP-WARNs

* Remove Context.clean_prefix and use upstream implementation instead

* Remove commands.Literal and use upstream implementation instead

Honestly, this was a rather bad implementation anyway...

Breaking but actually not really - it was provisional.

* Update Command.callback's setter

Support for functools.partial is now built into d.py

* Add new perms in HUMANIZED_PERM mapping (some from d.py 1.7 it seems)

BTW, that should really be in core instead of what we have now...

* Remove the part of do_conversion that has not worked for a long while

* Stop wrapping BadArgument in ConversionFailure

This is breaking but it's best to resolve it like this.

The functionality of ConversionFailure can be replicated with
Context.current_parameter and Context.current_argument.

* Add custom errors for int and float converters

* Remove Command.__call__ as it's now implemented in d.py

* Get rid of _dpy_reimplements

These were reimplemented for the purpose of typing
so it is no longer needed now that d.py is type hinted.

* Add return to Red.remove_cog

* Ensure we don't delete messages that differ only by used sticker

* discord.InvalidArgument->ValueError

* Move from raw <t:...> syntax to discord.utils.format_dt()

* Address AsyncIter removal

* Swap to pos-only for params that are pos-only in upstream

* Update for changes to Command.params

* [threads] Support threads in ignore checks and allow ignoring them

- Updated `[p](un)ignore channel` to accept threads
- Updated `[p]ignore list` to list ignored threads
- Updated logic in `Red.ignored_channel_or_guild()`

Ignores for guild channels now work as follows (only changes for threads):
- if channel is not a thread:
    - check if user has manage channels perm in channel
      and allow command usage if so
    - check if channel is ignored and disallow command usage if so
    - allow command usage if none of the conditions above happened
- if channel is a thread:
    - check if user has manage channels perm in parent channel
      and allow command usage if so
    - check if parent channel is ignored and disallow command usage
      if so
    - check if user has manage thread perm in parent channel
      and allow command usage if so
    - check if thread is ignored and disallow command usage if so
    - allow command usage if none of the conditions above happened

* [partial] Raise TypeError when channel is of PartialMessageable type

- Red.embed_requested
- Red.ignored_channel_or_guild

* [partial] Discard command messages when channel is PartialMessageable

* [threads] Add utilities for checking appropriate perms in both channels & threads

* [threads] Update code to use can_react_in() and @bot_can_react()

* [threads] Update code to use can_send_messages_in

* [threads] Add send_messages_in_threads perm to mute role and overrides

* [threads] Update code to use (bot/user)_can_manage_channel

* [threads] Update [p]diagnoseissues to work with threads

* Type hint fix

* [threads] Patch vendored discord.ext.menus to check proper perms in threads

I guess we've reached time when we have to patch the lib we vendor...

* Make docs generation work with non-final d.py releases

* Update discord.utils.oauth_url() usage

* Swap usage of discord.Embed.Empty/discord.embeds.EmptyEmbed to None

* Update usage of Guild.member_count to work with `None`

* Switch from Guild.vanity_invite() to Guild.vanity_url

* Update startup process to work with d.py's new asynchronous startup

* Use setup_hook() for pre-connect actions

* Update core's add_cog, remove_cog, and load_extension methods

* Update all setup functions to async and add awaits to bot.add_cog calls

* Modernize cogs by using async cog_load and cog_unload

* Address StoreChannel removal

* [partial] Disallow passing PartialMessageable to Case.channel

* [partial] Update cogs and utils to work better with PartialMessageable

- Ignore messages with PartialMessageable channel in CustomCommands cog
- In Filter cog, don't pass channel to modlog.create_case()
  if it's PartialMessageable
- In Trivia cog, only compare channel IDs
- Make `.utils.menus.menu()` work for messages
  with PartialMessageable channel
- Make checks in `.utils.tunnel.Tunnel.communicate()` more rigid

* Add few missing DEP-WARNs
This commit is contained in:
jack1142
2022-04-03 03:21:20 +02:00
committed by GitHub
parent c9a0971945
commit febca8ccbb
104 changed files with 1427 additions and 999 deletions

View File

@@ -106,7 +106,7 @@ async def _init(bot: Red):
except RuntimeError:
return # No modlog channel so no point in continuing
when = datetime.utcnow()
when = datetime.now(timezone.utc)
before = when + timedelta(minutes=1)
after = when - timedelta(minutes=1)
await asyncio.sleep(10) # prevent small delays from causing a 5 minute delay on entry
@@ -116,9 +116,12 @@ async def _init(bot: Red):
while attempts < 12 and guild.me.guild_permissions.view_audit_log:
attempts += 1
try:
entry = await guild.audit_logs(
action=discord.AuditLogAction.ban, before=before, after=after
).find(lambda e: e.target.id == member.id and after < e.created_at < before)
entry = await discord.utils.find(
lambda e: e.target.id == member.id and after < e.created_at < before,
guild.audit_logs(
action=discord.AuditLogAction.ban, before=before, after=after
),
)
except discord.Forbidden:
break
except discord.HTTPException:
@@ -128,7 +131,7 @@ async def _init(bot: Red):
if entry.user.id != guild.me.id:
# Don't create modlog entires for the bot's own bans, cogs do this.
mod, reason = entry.user, entry.reason
date = entry.created_at.replace(tzinfo=timezone.utc)
date = entry.created_at
await create_case(_bot_ref, guild, date, "ban", member, mod, reason)
return
@@ -143,7 +146,7 @@ async def _init(bot: Red):
except RuntimeError:
return # No modlog channel so no point in continuing
when = datetime.utcnow()
when = datetime.now(timezone.utc)
before = when + timedelta(minutes=1)
after = when - timedelta(minutes=1)
await asyncio.sleep(10) # prevent small delays from causing a 5 minute delay on entry
@@ -153,9 +156,12 @@ async def _init(bot: Red):
while attempts < 12 and guild.me.guild_permissions.view_audit_log:
attempts += 1
try:
entry = await guild.audit_logs(
action=discord.AuditLogAction.unban, before=before, after=after
).find(lambda e: e.target.id == user.id and after < e.created_at < before)
entry = await discord.utils.find(
lambda e: e.target.id == user.id and after < e.created_at < before,
guild.audit_logs(
action=discord.AuditLogAction.unban, before=before, after=after
),
)
except discord.Forbidden:
break
except discord.HTTPException:
@@ -165,7 +171,7 @@ async def _init(bot: Red):
if entry.user.id != guild.me.id:
# Don't create modlog entires for the bot's own unbans, cogs do this.
mod, reason = entry.user, entry.reason
date = entry.created_at.replace(tzinfo=timezone.utc)
date = entry.created_at
await create_case(_bot_ref, guild, date, "unban", user, mod, reason)
return
@@ -268,13 +274,16 @@ class Case:
until: Optional[int]
The UNIX time the action is in effect until.
`None` if the action is permanent.
channel: Optional[Union[discord.abc.GuildChannel, int]]
channel: Optional[Union[discord.abc.GuildChannel, discord.Thread, int]]
The channel the action was taken in.
`None` if the action was not related to a channel.
.. note::
This attribute will be of type `int`
if the channel seems to no longer exist.
parent_channel_id: Optional[int]
The parent channel ID of the thread in ``channel``.
`None` if the action was not done in a thread.
amended_by: Optional[Union[discord.abc.User, int]]
The moderator who made the last change to the case.
`None` if the case was never edited.
@@ -310,7 +319,8 @@ class Case:
case_number: int,
reason: Optional[str] = None,
until: Optional[int] = None,
channel: Optional[Union[discord.abc.GuildChannel, int]] = None,
channel: Optional[Union[discord.abc.GuildChannel, discord.Thread, int]] = None,
parent_channel_id: Optional[int] = None,
amended_by: Optional[Union[discord.Object, discord.abc.User, int]] = None,
modified_at: Optional[float] = None,
message: Optional[Union[discord.PartialMessage, discord.Message]] = None,
@@ -330,6 +340,7 @@ class Case:
self.reason = reason
self.until = until
self.channel = channel
self.parent_channel_id = parent_channel_id
self.amended_by = amended_by
if isinstance(amended_by, discord.Object):
self.amended_by = amended_by.id
@@ -337,6 +348,18 @@ class Case:
self.case_number = case_number
self.message = message
@property
def parent_channel(self) -> Optional[discord.TextChannel]:
"""
The parent text channel of the thread in `channel`.
This will be `None` if `channel` is not a thread
and when the parent text channel is not in cache (probably due to removal).
"""
if self.parent_channel_id is None:
return None
return self.guild.get_channel(self.parent_channel_id)
async def _set_message(self, message: discord.Message, /) -> None:
# This should only be used for setting the message right after case creation
# in order to avoid making an API request to "edit" the message with changes.
@@ -359,6 +382,8 @@ class Case:
# last username is set based on passed user object
data.pop("last_known_username", None)
for item, value in data.items():
if item == "channel" and isinstance(value, discord.PartialMessageable):
raise TypeError("Can't use PartialMessageable as the channel for a modlog case.")
if isinstance(value, discord.Object):
# probably expensive to call but meh should capture all cases
setattr(self, item, value.id)
@@ -369,6 +394,9 @@ class Case:
if not isinstance(self.user, int):
self.last_known_username = f"{self.user.name}#{self.user.discriminator}"
if isinstance(self.channel, discord.Thread):
self.parent_channel_id = self.channel.parent_id
await _config.custom(_CASES, str(self.guild.id), str(self.case_number)).set(self.to_json())
self.bot.dispatch("modlog_case_edit", self)
if not self.message:
@@ -443,7 +471,7 @@ class Case:
if self.until:
start = datetime.fromtimestamp(self.created_at, tz=timezone.utc)
end = datetime.fromtimestamp(self.until, tz=timezone.utc)
end_fmt = f"<t:{int(end.timestamp())}>"
end_fmt = discord.utils.format_dt(end)
duration = end - start
dur_fmt = _strfdelta(duration)
until = end_fmt
@@ -463,7 +491,9 @@ class Case:
last_modified = None
if self.modified_at:
last_modified = f"<t:{int(self.modified_at)}>"
last_modified = discord.utils.format_dt(
datetime.fromtimestamp(self.modified_at, tz=timezone.utc)
)
if isinstance(self.user, int):
if self.user == 0xDE1:
@@ -490,6 +520,31 @@ class Case:
)
) # Invites and spoilers get rendered even in embeds.
channel_value = None
if isinstance(self.channel, int):
if self.parent_channel_id is not None:
if (parent_channel := self.parent_channel) is not None:
channel_value = _(
"Deleted or archived thread ({thread_id}) in {channel_name}"
).format(thread_id=self.channel, channel_name=parent_channel)
else:
channel_value = _("Thread {thread_id} in {channel_id} (deleted)").format(
thread_id=self.channel, channel_id=self.parent_channel_id
)
else:
channel_value = _("{channel_id} (deleted)").format(channel_id=self.channel)
elif self.channel is not None:
channel_value = self.channel.name
if self.parent_channel_id is not None:
if (parent_channel := self.parent_channel) is not None:
channel_value = _("Thread {thread_name} in {channel_name}").format(
thread_name=self.channel, channel_name=parent_channel
)
else:
channel_value = _("Thread {thread_name} in {channel_id} (deleted)").format(
thread_name=self.channel, channel_id=self.parent_channel_id
)
if embed:
if self.reason:
reason = f"{bold(_('Reason:'))} {self.reason}"
@@ -510,20 +565,13 @@ class Case:
if until and duration:
emb.add_field(name=_("Until"), value=until)
emb.add_field(name=_("Duration"), value=duration)
if isinstance(self.channel, int):
emb.add_field(
name=_("Channel"),
value=_("{channel} (deleted)").format(channel=self.channel),
inline=False,
)
elif self.channel is not None:
emb.add_field(name=_("Channel"), value=self.channel.name, inline=False)
if channel_value:
emb.add_field(name=_("Channel"), value=channel_value, inline=False)
if amended_by:
emb.add_field(name=_("Amended by"), value=amended_by)
if last_modified:
emb.add_field(name=_("Last modified at"), value=last_modified)
emb.timestamp = datetime.utcfromtimestamp(self.created_at)
emb.timestamp = datetime.fromtimestamp(self.created_at, tz=timezone.utc)
return emb
else:
if self.reason:
@@ -549,9 +597,9 @@ class Case:
case_text += f"{bold(_('Until:'))} {until}\n{bold(_('Duration:'))} {duration}\n"
if self.channel:
if isinstance(self.channel, int):
case_text += f"{bold(_('Channel:'))} {self.channel} {_('(Deleted)')}\n"
case_text += f"{bold(_('Channel:'))} {channel_value}\n"
else:
case_text += f"{bold(_('Channel:'))} {self.channel.name}\n"
case_text += f"{bold(_('Channel:'))} {channel_value}\n"
if amended_by:
case_text += f"{bold(_('Amended by:'))} {amended_by}\n"
if last_modified:
@@ -590,6 +638,7 @@ class Case:
"reason": self.reason,
"until": self.until,
"channel": self.channel.id if hasattr(self.channel, "id") else None,
"parent_channel": self.parent_channel_id,
"amended_by": amended_by,
"modified_at": self.modified_at,
"message": self.message.id if hasattr(self.message, "id") else None,
@@ -650,7 +699,11 @@ class Case:
user_object = bot.get_user(user_id) or user_id
user_objects[user_key] = user_object
channel = kwargs.get("channel") or guild.get_channel(data["channel"]) or data["channel"]
channel = (
kwargs.get("channel")
or guild.get_channel_or_thread(data["channel"])
or data["channel"]
)
case_guild = kwargs.get("guild") or bot.get_guild(data["guild"])
return cls(
bot=bot,
@@ -661,6 +714,7 @@ class Case:
reason=data["reason"],
until=data["until"],
channel=channel,
parent_channel_id=data.get("parent_channel_id"),
modified_at=data["modified_at"],
message=message,
last_known_username=data.get("last_known_username"),
@@ -917,7 +971,7 @@ async def create_case(
moderator: Optional[Union[discord.Object, discord.abc.User, int]] = None,
reason: Optional[str] = None,
until: Optional[datetime] = None,
channel: Optional[discord.abc.GuildChannel] = None,
channel: Optional[Union[discord.abc.GuildChannel, discord.Thread]] = None,
last_known_username: Optional[str] = None,
) -> Optional[Case]:
"""
@@ -947,12 +1001,17 @@ async def create_case(
The time the action is in effect until.
If naive `datetime` object is passed, it's treated as a local time
(similarly to how Python treats naive `datetime` objects).
channel: Optional[discord.abc.GuildChannel]
channel: Optional[Union[discord.abc.GuildChannel, discord.Thread]]
The channel the action was taken in
last_known_username: Optional[str]
The last known username of the user
Note: This is ignored if a Member or User object is provided
in the user field
Raises
------
TypeError
If ``channel`` is of type `discord.PartialMessageable`.
"""
case_type = await get_casetype(action_type, guild)
if case_type is None:
@@ -964,6 +1023,11 @@ async def create_case(
if user == bot.user:
return
if isinstance(channel, discord.PartialMessageable):
raise TypeError("Can't use PartialMessageable as the channel for a modlog case.")
parent_channel_id = channel.parent_id if isinstance(channel, discord.Thread) else None
async with _config.guild(guild).latest_case_number.get_lock():
# We're getting the case number from config, incrementing it, awaiting something, then
# setting it again. This warrants acquiring the lock.
@@ -980,6 +1044,7 @@ async def create_case(
reason,
int(until.timestamp()) if until else None,
channel,
parent_channel_id,
amended_by=None,
modified_at=None,
message=None,