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

@@ -1,5 +1,7 @@
from redbot.core.bot import Red
from .admin import Admin
def setup(bot):
bot.add_cog(Admin(bot))
async def setup(bot: Red) -> None:
await bot.add_cog(Admin(bot))

View File

@@ -84,12 +84,9 @@ class Admin(commands.Cog):
)
self.__current_announcer = None
self._ready = asyncio.Event()
asyncio.create_task(self.handle_migrations())
# As this is a data migration, don't store this for cancelation.
async def cog_before_invoke(self, ctx: commands.Context):
await self._ready.wait()
async def cog_load(self) -> None:
await self.handle_migrations()
async def red_delete_data_for_user(self, **kwargs):
"""Nothing to delete"""
@@ -106,9 +103,7 @@ class Admin(commands.Cog):
await self.migrate_config_from_0_to_1()
await self.config.schema_version.set(1)
self._ready.set()
async def migrate_config_from_0_to_1(self):
async def migrate_config_from_0_to_1(self) -> None:
all_guilds = await self.config.all_guilds()
for guild_id, guild_data in all_guilds.items():
@@ -354,14 +349,8 @@ class Admin(commands.Cog):
pass
@announceset.command(name="channel")
async def announceset_channel(self, ctx, *, channel: discord.TextChannel = None):
"""
Change the channel where the bot will send announcements.
If channel is left blank it defaults to the current channel.
"""
if channel is None:
channel = ctx.channel
async def announceset_channel(self, ctx, *, channel: discord.TextChannel):
"""Change the channel where the bot will send announcements."""
await self.config.guild(ctx.guild).announce_channel.set(channel.id)
await ctx.send(
_("The announcement channel has been set to {channel.mention}").format(channel=channel)

View File

@@ -2,7 +2,5 @@ from .alias import Alias
from redbot.core.bot import Red
async def setup(bot: Red):
cog = Alias(bot)
bot.add_cog(cog)
cog.sync_init()
async def setup(bot: Red) -> None:
await bot.add_cog(Alias(bot))

View File

@@ -50,7 +50,12 @@ class Alias(commands.Cog):
self.config.register_global(entries=[], handled_string_creator=False)
self.config.register_guild(entries=[])
self._aliases: AliasCache = AliasCache(config=self.config, cache_enabled=True)
self._ready_event = asyncio.Event()
async def cog_load(self) -> None:
await self._maybe_handle_string_keys()
if not self._aliases._loaded:
await self._aliases.load_aliases()
async def red_delete_data_for_user(
self,
@@ -61,12 +66,8 @@ class Alias(commands.Cog):
if requester != "discord_deleted_user":
return
await self._ready_event.wait()
await self._aliases.anonymize_aliases(user_id)
async def cog_before_invoke(self, ctx):
await self._ready_event.wait()
async def _maybe_handle_string_keys(self):
# This isn't a normal schema migration because it's being added
# after the fact for GH-3788
@@ -119,28 +120,6 @@ class Alias(commands.Cog):
await self.config.handled_string_creator.set(True)
def sync_init(self):
t = asyncio.create_task(self._initialize())
def done_callback(fut: asyncio.Future):
try:
t.result()
except Exception as exc:
log.exception("Failed to load alias cog", exc_info=exc)
# Maybe schedule extension unloading with message to owner in future
t.add_done_callback(done_callback)
async def _initialize(self):
"""Should only ever be a task"""
await self._maybe_handle_string_keys()
if not self._aliases._loaded:
await self._aliases.load_aliases()
self._ready_event.set()
def is_command(self, alias_name: str) -> bool:
"""
The logic here is that if this returns true, the name should not be used for an alias
@@ -461,7 +440,7 @@ class Alias(commands.Cog):
@alias.command(name="list")
@commands.guild_only()
@checks.bot_has_permissions(add_reactions=True)
@commands.bot_can_react()
async def _list_alias(self, ctx: commands.Context):
"""List the available aliases on this server."""
guild_aliases = await self._aliases.get_guild_aliases(ctx.guild)
@@ -470,7 +449,7 @@ class Alias(commands.Cog):
await self.paginate_alias_list(ctx, guild_aliases)
@global_.command(name="list")
@checks.bot_has_permissions(add_reactions=True)
@commands.bot_can_react()
async def _list_global_alias(self, ctx: commands.Context):
"""List the available global aliases on this bot."""
global_aliases = await self._aliases.get_global_aliases()
@@ -480,8 +459,6 @@ class Alias(commands.Cog):
@commands.Cog.listener()
async def on_message_without_command(self, message: discord.Message):
await self._ready_event.wait()
if message.guild is not None:
if await self.bot.cog_disabled_in_guild(self, message.guild):
return

View File

@@ -2,7 +2,7 @@ from typing import Tuple, Dict, Optional, List, Union
from re import findall
import discord
from discord.ext.commands.view import StringView
from discord.ext.commands.view import StringView # DEP-WARN
from redbot.core import commands, Config
from redbot.core.i18n import Translator
from redbot.core.utils import AsyncIter

View File

@@ -3,7 +3,7 @@ from redbot.core.bot import Red
from .core import Audio
def setup(bot: Red):
async def setup(bot: Red) -> None:
cog = Audio(bot)
bot.add_cog(cog)
await bot.add_cog(cog)
cog.start_up_task()

View File

@@ -973,7 +973,7 @@ class AudioAPIInterface:
and not query.local_track_path.exists()
):
continue
notify_channel = player.guild.get_channel(notify_channel_id)
notify_channel = player.guild.get_channel_or_thread(notify_channel_id)
if not await self.cog.is_query_allowed(
self.config,
notify_channel,

View File

@@ -74,7 +74,6 @@ class Audio(
self.permission_cache = discord.Permissions(
embed_links=True,
read_messages=True,
send_messages=True,
read_message_history=True,
add_reactions=True,
)

View File

@@ -196,7 +196,7 @@ class MixinMeta(ABC):
async def is_query_allowed(
self,
config: Config,
ctx_or_channel: Optional[Union[Context, discord.TextChannel]],
ctx_or_channel: Optional[Union[Context, discord.TextChannel, discord.Thread]],
query: str,
query_obj: Query,
) -> bool:
@@ -250,7 +250,7 @@ class MixinMeta(ABC):
raise NotImplementedError()
@abstractmethod
def _has_notify_perms(self, channel: discord.TextChannel) -> bool:
def _has_notify_perms(self, channel: Union[discord.TextChannel, discord.Thread]) -> bool:
raise NotImplementedError()
@abstractmethod

View File

@@ -78,7 +78,7 @@ class AudioSetCommands(MixinMeta, metaclass=CompositeMetaClass):
)
@command_audioset_perms_global_whitelist.command(name="list")
@commands.bot_has_permissions(add_reactions=True)
@commands.bot_can_react()
async def command_audioset_perms_global_whitelist_list(self, ctx: commands.Context):
"""List all keywords added to the whitelist."""
whitelist = await self.config.url_keyword_whitelist()
@@ -172,7 +172,7 @@ class AudioSetCommands(MixinMeta, metaclass=CompositeMetaClass):
)
@command_audioset_perms_global_blacklist.command(name="list")
@commands.bot_has_permissions(add_reactions=True)
@commands.bot_can_react()
async def command_audioset_perms_global_blacklist_list(self, ctx: commands.Context):
"""List all keywords added to the blacklist."""
blacklist = await self.config.url_keyword_blacklist()
@@ -268,7 +268,7 @@ class AudioSetCommands(MixinMeta, metaclass=CompositeMetaClass):
)
@command_audioset_perms_whitelist.command(name="list")
@commands.bot_has_permissions(add_reactions=True)
@commands.bot_can_react()
async def command_audioset_perms_whitelist_list(self, ctx: commands.Context):
"""List all keywords added to the whitelist."""
whitelist = await self.config.guild(ctx.guild).url_keyword_whitelist()
@@ -361,7 +361,7 @@ class AudioSetCommands(MixinMeta, metaclass=CompositeMetaClass):
)
@command_audioset_perms_blacklist.command(name="list")
@commands.bot_has_permissions(add_reactions=True)
@commands.bot_can_react()
async def command_audioset_perms_blacklist_list(self, ctx: commands.Context):
"""List all keywords added to the blacklist."""
blacklist = await self.config.guild(ctx.guild).url_keyword_blacklist()
@@ -453,7 +453,7 @@ class AudioSetCommands(MixinMeta, metaclass=CompositeMetaClass):
await self.set_player_settings(ctx)
@command_audioset_autoplay.command(name="playlist", usage="<playlist_name_OR_id> [args]")
@commands.bot_has_permissions(add_reactions=True)
@commands.bot_can_react()
async def command_audioset_autoplay_playlist(
self,
ctx: commands.Context,
@@ -496,9 +496,7 @@ class AudioSetCommands(MixinMeta, metaclass=CompositeMetaClass):
ctx,
title=_("Playlists Are Not Available"),
description=_("The playlist section of Audio is currently unavailable"),
footer=discord.Embed.Empty
if not await self.bot.is_owner(ctx.author)
else _("Check your logs."),
footer=None if not await self.bot.is_owner(ctx.author) else _("Check your logs."),
)
if scope_data is None:
scope_data = [None, ctx.author, ctx.guild, False]
@@ -782,7 +780,7 @@ class AudioSetCommands(MixinMeta, metaclass=CompositeMetaClass):
@command_audioset.command(name="localpath")
@commands.is_owner()
@commands.bot_has_permissions(add_reactions=True)
@commands.bot_can_react()
async def command_audioset_localpath(self, ctx: commands.Context, *, local_path=None):
"""Set the localtracks path if the Lavalink.jar is not run from the Audio data folder.

View File

@@ -81,7 +81,8 @@ class PlayerControllerCommands(MixinMeta, metaclass=CompositeMetaClass):
@commands.command(name="now")
@commands.guild_only()
@commands.bot_has_permissions(embed_links=True, add_reactions=True)
@commands.bot_has_permissions(embed_links=True)
@commands.bot_can_react()
async def command_now(self, ctx: commands.Context):
"""Now playing."""
if not self._player_check(ctx):

View File

@@ -25,7 +25,8 @@ class EqualizerCommands(MixinMeta, metaclass=CompositeMetaClass):
@commands.group(name="eq", invoke_without_command=True)
@commands.guild_only()
@commands.cooldown(1, 15, commands.BucketType.guild)
@commands.bot_has_permissions(embed_links=True, add_reactions=True)
@commands.bot_has_permissions(embed_links=True)
@commands.bot_can_react()
async def command_equalizer(self, ctx: commands.Context):
"""Equalizer management.

View File

@@ -21,7 +21,8 @@ _ = Translator("Audio", Path(__file__))
class LocalTrackCommands(MixinMeta, metaclass=CompositeMetaClass):
@commands.group(name="local")
@commands.guild_only()
@commands.bot_has_permissions(embed_links=True, add_reactions=True)
@commands.bot_has_permissions(embed_links=True)
@commands.bot_can_react()
async def command_local(self, ctx: commands.Context):
"""Local playback commands."""

View File

@@ -41,7 +41,8 @@ class MiscellaneousCommands(MixinMeta, metaclass=CompositeMetaClass):
@commands.command(name="audiostats")
@commands.guild_only()
@commands.is_owner()
@commands.bot_has_permissions(embed_links=True, add_reactions=True)
@commands.bot_has_permissions(embed_links=True)
@commands.bot_can_react()
async def command_audiostats(self, ctx: commands.Context):
"""Audio stats."""
server_num = len(lavalink.active_players())

View File

@@ -9,7 +9,6 @@ import discord
import lavalink
from red_commons.logging import getLogger
from discord.embeds import EmptyEmbed
from lavalink import NodeNotFound
from redbot.core import commands
@@ -67,7 +66,7 @@ class PlayerCommands(MixinMeta, metaclass=CompositeMetaClass):
if not self._player_check(ctx):
if self.lavalink_connection_aborted:
msg = _("Connection to Lavalink node has failed")
desc = EmptyEmbed
desc = None
if await self.bot.is_owner(ctx.author):
desc = _("Please check your console or logs for details.")
return await self.send_embed_msg(ctx, title=msg, description=desc)
@@ -175,7 +174,7 @@ class PlayerCommands(MixinMeta, metaclass=CompositeMetaClass):
if not self._player_check(ctx):
if self.lavalink_connection_aborted:
msg = _("Connection to Lavalink node has failed")
desc = EmptyEmbed
desc = None
if await self.bot.is_owner(ctx.author):
desc = _("Please check your console or logs for details.")
return await self.send_embed_msg(ctx, title=msg, description=desc)
@@ -438,7 +437,7 @@ class PlayerCommands(MixinMeta, metaclass=CompositeMetaClass):
if not self._player_check(ctx):
if self.lavalink_connection_aborted:
msg = _("Connection to Lavalink node has failed")
desc = EmptyEmbed
desc = None
if await self.bot.is_owner(ctx.author):
desc = _("Please check your console or logs for details.")
return await self.send_embed_msg(ctx, title=msg, description=desc)
@@ -554,7 +553,7 @@ class PlayerCommands(MixinMeta, metaclass=CompositeMetaClass):
if not self._player_check(ctx):
if self.lavalink_connection_aborted:
msg = _("Connection to Lavalink node has failed")
desc = EmptyEmbed
desc = None
if await self.bot.is_owner(ctx.author):
desc = _("Please check your console or logs for details.")
return await self.send_embed_msg(ctx, title=msg, description=desc)
@@ -610,7 +609,7 @@ class PlayerCommands(MixinMeta, metaclass=CompositeMetaClass):
except DatabaseError:
notify_channel = player.fetch("notify_channel")
if notify_channel:
notify_channel = ctx.guild.get_channel(notify_channel)
notify_channel = ctx.guild.get_channel_or_thread(notify_channel)
await self.send_embed_msg(notify_channel, title=_("Couldn't get a valid track."))
return
except TrackEnqueueError:
@@ -636,7 +635,8 @@ class PlayerCommands(MixinMeta, metaclass=CompositeMetaClass):
@commands.command(name="search")
@commands.guild_only()
@commands.bot_has_permissions(embed_links=True, add_reactions=True)
@commands.bot_has_permissions(embed_links=True)
@commands.bot_can_react()
async def command_search(self, ctx: commands.Context, *, query: str):
"""Pick a track with a search.
@@ -678,7 +678,7 @@ class PlayerCommands(MixinMeta, metaclass=CompositeMetaClass):
if not self._player_check(ctx):
if self.lavalink_connection_aborted:
msg = _("Connection to Lavalink has failed")
desc = EmptyEmbed
desc = None
if await self.bot.is_owner(ctx.author):
desc = _("Please check your console or logs for details.")
return await self.send_embed_msg(ctx, title=msg, description=desc)

View File

@@ -38,7 +38,8 @@ _ = Translator("Audio", Path(__file__))
class PlaylistCommands(MixinMeta, metaclass=CompositeMetaClass):
@commands.group(name="playlist")
@commands.guild_only()
@commands.bot_has_permissions(embed_links=True, add_reactions=True)
@commands.bot_has_permissions(embed_links=True)
@commands.bot_can_react()
async def command_playlist(self, ctx: commands.Context):
"""Playlist configuration options.
@@ -263,9 +264,7 @@ class PlaylistCommands(MixinMeta, metaclass=CompositeMetaClass):
ctx,
title=_("Playlists Are Not Available"),
description=_("The playlist section of Audio is currently unavailable"),
footer=discord.Embed.Empty
if not await self.bot.is_owner(ctx.author)
else _("Check your logs."),
footer=None if not await self.bot.is_owner(ctx.author) else _("Check your logs."),
)
if scope_data is None:
scope_data = [
@@ -392,9 +391,7 @@ class PlaylistCommands(MixinMeta, metaclass=CompositeMetaClass):
ctx,
title=_("Playlists Are Not Available"),
description=_("The playlist section of Audio is currently unavailable"),
footer=discord.Embed.Empty
if not await self.bot.is_owner(ctx.author)
else _("Check your logs."),
footer=None if not await self.bot.is_owner(ctx.author) else _("Check your logs."),
)
if scope_data is None:
scope_data = [None, ctx.author, ctx.guild, False]
@@ -471,9 +468,7 @@ class PlaylistCommands(MixinMeta, metaclass=CompositeMetaClass):
ctx,
title=_("Playlists Are Not Available"),
description=_("The playlist section of Audio is currently unavailable"),
footer=discord.Embed.Empty
if not await self.bot.is_owner(ctx.author)
else _("Check your logs."),
footer=None if not await self.bot.is_owner(ctx.author) else _("Check your logs."),
)
if scope_data is None:
scope_data = [None, ctx.author, ctx.guild, False]
@@ -561,9 +556,7 @@ class PlaylistCommands(MixinMeta, metaclass=CompositeMetaClass):
ctx,
title=_("Playlists Are Not Available"),
description=_("The playlist section of Audio is currently unavailable"),
footer=discord.Embed.Empty
if not await self.bot.is_owner(ctx.author)
else _("Check your logs."),
footer=None if not await self.bot.is_owner(ctx.author) else _("Check your logs."),
)
async with ctx.typing():
if scope_data is None:
@@ -696,9 +689,7 @@ class PlaylistCommands(MixinMeta, metaclass=CompositeMetaClass):
ctx,
title=_("Playlists Are Not Available"),
description=_("The playlist section of Audio is currently unavailable"),
footer=discord.Embed.Empty
if not await self.bot.is_owner(ctx.author)
else _("Check your logs."),
footer=None if not await self.bot.is_owner(ctx.author) else _("Check your logs."),
)
if scope_data is None:
scope_data = [None, ctx.author, ctx.guild, False]
@@ -829,9 +820,7 @@ class PlaylistCommands(MixinMeta, metaclass=CompositeMetaClass):
ctx,
title=_("Playlists Are Not Available"),
description=_("The playlist section of Audio is currently unavailable"),
footer=discord.Embed.Empty
if not await self.bot.is_owner(ctx.author)
else _("Check your logs."),
footer=None if not await self.bot.is_owner(ctx.author) else _("Check your logs."),
)
if scope_data is None:
scope_data = [None, ctx.author, ctx.guild, False]
@@ -952,9 +941,7 @@ class PlaylistCommands(MixinMeta, metaclass=CompositeMetaClass):
ctx,
title=_("Playlists Are Not Available"),
description=_("The playlist section of Audio is currently unavailable"),
footer=discord.Embed.Empty
if not await self.bot.is_owner(ctx.author)
else _("Check your logs."),
footer=None if not await self.bot.is_owner(ctx.author) else _("Check your logs."),
)
if scope_data is None:
scope_data = [None, ctx.author, ctx.guild, False]
@@ -1107,9 +1094,7 @@ class PlaylistCommands(MixinMeta, metaclass=CompositeMetaClass):
ctx,
title=_("Playlists Are Not Available"),
description=_("The playlist section of Audio is currently unavailable"),
footer=discord.Embed.Empty
if not await self.bot.is_owner(ctx.author)
else _("Check your logs."),
footer=None if not await self.bot.is_owner(ctx.author) else _("Check your logs."),
)
async with ctx.typing():
if scope_data is None:
@@ -1217,9 +1202,7 @@ class PlaylistCommands(MixinMeta, metaclass=CompositeMetaClass):
ctx,
title=_("Playlists Are Not Available"),
description=_("The playlist section of Audio is currently unavailable"),
footer=discord.Embed.Empty
if not await self.bot.is_owner(ctx.author)
else _("Check your logs."),
footer=None if not await self.bot.is_owner(ctx.author) else _("Check your logs."),
)
if scope_data is None:
scope_data = [None, ctx.author, ctx.guild, False]
@@ -1335,9 +1318,7 @@ class PlaylistCommands(MixinMeta, metaclass=CompositeMetaClass):
ctx,
title=_("Playlists Are Not Available"),
description=_("The playlist section of Audio is currently unavailable"),
footer=discord.Embed.Empty
if not await self.bot.is_owner(ctx.author)
else _("Check your logs."),
footer=None if not await self.bot.is_owner(ctx.author) else _("Check your logs."),
)
if scope_data is None:
scope_data = [None, ctx.author, ctx.guild, False]
@@ -1461,9 +1442,7 @@ class PlaylistCommands(MixinMeta, metaclass=CompositeMetaClass):
ctx,
title=_("Playlists Are Not Available"),
description=_("The playlist section of Audio is currently unavailable"),
footer=discord.Embed.Empty
if not await self.bot.is_owner(ctx.author)
else _("Check your logs."),
footer=None if not await self.bot.is_owner(ctx.author) else _("Check your logs."),
)
if scope_data is None:
scope_data = [None, ctx.author, ctx.guild, False]
@@ -1639,9 +1618,7 @@ class PlaylistCommands(MixinMeta, metaclass=CompositeMetaClass):
ctx,
title=_("Playlists Are Not Available"),
description=_("The playlist section of Audio is currently unavailable"),
footer=discord.Embed.Empty
if not await self.bot.is_owner(ctx.author)
else _("Check your logs."),
footer=None if not await self.bot.is_owner(ctx.author) else _("Check your logs."),
)
if scope_data is None:
scope_data = [None, ctx.author, ctx.guild, False]
@@ -1810,9 +1787,7 @@ class PlaylistCommands(MixinMeta, metaclass=CompositeMetaClass):
ctx,
title=_("Playlists Are Not Available"),
description=_("The playlist section of Audio is currently unavailable"),
footer=discord.Embed.Empty
if not await self.bot.is_owner(ctx.author)
else _("Check your logs."),
footer=None if not await self.bot.is_owner(ctx.author) else _("Check your logs."),
)
if scope_data is None:
scope_data = [None, ctx.author, ctx.guild, False]
@@ -1976,9 +1951,7 @@ class PlaylistCommands(MixinMeta, metaclass=CompositeMetaClass):
ctx,
title=_("Playlists Are Not Available"),
description=_("The playlist section of Audio is currently unavailable"),
footer=discord.Embed.Empty
if not await self.bot.is_owner(ctx.author)
else _("Check your logs."),
footer=None if not await self.bot.is_owner(ctx.author) else _("Check your logs."),
)
if scope_data is None:
scope_data = [None, ctx.author, ctx.guild, False]

View File

@@ -33,7 +33,8 @@ _ = Translator("Audio", Path(__file__))
class QueueCommands(MixinMeta, metaclass=CompositeMetaClass):
@commands.group(name="queue", invoke_without_command=True)
@commands.guild_only()
@commands.bot_has_permissions(embed_links=True, add_reactions=True)
@commands.bot_has_permissions(embed_links=True)
@commands.bot_can_react()
async def command_queue(self, ctx: commands.Context, *, page: int = 1):
"""List the songs in the queue."""

View File

@@ -197,7 +197,7 @@ class AudioEvents(MixinMeta, metaclass=CompositeMetaClass):
):
if not guild:
return
notify_channel = guild.get_channel(player.fetch("notify_channel"))
notify_channel = guild.get_channel_or_thread(player.fetch("notify_channel"))
has_perms = self._has_notify_perms(notify_channel)
tries = 0
while not player._is_playing:

View File

@@ -18,6 +18,7 @@ from lavalink import NodeNotFound, PlayerNotFound
from redbot.core import commands
from redbot.core.i18n import Translator
from redbot.core.utils import can_user_send_messages_in
from redbot.core.utils.antispam import AntiSpam
from redbot.core.utils.chat_formatting import box, humanize_list, underline, bold
@@ -61,6 +62,16 @@ HUMANIZED_PERM = {
"manage_roles": _("Manage Roles"),
"manage_webhooks": _("Manage Webhooks"),
"manage_emojis": _("Manage Emojis"),
"use_slash_commands": _("Use Slash Commands"),
"request_to_speak": _("Request to Speak"),
"manage_events": _("Manage Events"),
"manage_threads": _("Manage Threads"),
"create_public_threads": _("Create Public Threads"),
"create_private_threads": _("Create Private Threads"),
"external_stickers": _("Use External Stickers"),
"send_messages_in_threads": _("Send Messages in Threads"),
"start_embedded_activities": _("Start Activities"),
"moderate_members": _("Moderate Member"),
}
DANGEROUS_COMMANDS = {
@@ -175,13 +186,31 @@ class DpyEvents(MixinMeta, metaclass=CompositeMetaClass):
"Not running Audio command due to invalid machine architecture for the managed Lavalink node."
)
current_perms = ctx.channel.permissions_for(ctx.me)
surpass_ignore = (
isinstance(ctx.channel, discord.abc.PrivateChannel)
or await ctx.bot.is_owner(ctx.author)
or await ctx.bot.is_admin(ctx.author)
)
guild = ctx.guild
if guild and not can_user_send_messages_in(ctx.me, ctx.channel):
log.debug(
"Missing perms to send messages in %d, Owner ID: %d",
guild.id,
guild.owner.id,
)
if not surpass_ignore:
text = _(
"I'm missing permissions to send messages in this server. "
"Please address this as soon as possible."
)
log.info(
"Missing write permission in %d, Owner ID: %d",
guild.id,
guild.owner.id,
)
raise CheckFailure(message=text)
current_perms = ctx.channel.permissions_for(ctx.me)
if guild and not current_perms.is_superset(self.permission_cache):
current_perms_set = set(iter(current_perms))
expected_perms_set = set(iter(self.permission_cache))
@@ -207,14 +236,7 @@ class DpyEvents(MixinMeta, metaclass=CompositeMetaClass):
perm=_(HUMANIZED_PERM.get(perm, perm)),
)
text = text.strip()
if current_perms.send_messages and current_perms.read_messages:
await ctx.send(box(text=text, lang="ini"))
else:
log.info(
"Missing write permission in %s, Owner ID: %s",
ctx.guild.id,
ctx.guild.owner.id,
)
await ctx.send(box(text=text, lang="ini"))
raise CheckFailure(message=text)
with contextlib.suppress(Exception):
@@ -312,7 +334,7 @@ class DpyEvents(MixinMeta, metaclass=CompositeMetaClass):
)
if error.send_cmd_help:
await ctx.send_help()
elif isinstance(error, commands.ConversionFailure):
elif isinstance(error, commands.BadArgument):
handled = True
if error.args:
if match := RE_CONVERSION.search(error.args[0]):
@@ -390,10 +412,10 @@ class DpyEvents(MixinMeta, metaclass=CompositeMetaClass):
if not handled:
await self.bot.on_command_error(ctx, error, unhandled_by_cog=True)
def cog_unload(self) -> None:
async def cog_unload(self) -> None:
if not self.cog_cleaned_up:
self.bot.dispatch("red_audio_unload", self)
self.session.detach()
await self.session.close()
if self.player_automated_timer_task:
self.player_automated_timer_task.cancel()
@@ -408,10 +430,10 @@ class DpyEvents(MixinMeta, metaclass=CompositeMetaClass):
lavalink.unregister_event_listener(self.lavalink_event_handler)
lavalink.unregister_update_listener(self.lavalink_update_handler)
asyncio.create_task(lavalink.close(self.bot))
asyncio.create_task(self._close_database())
await lavalink.close(self.bot)
await self._close_database()
if self.managed_node_controller is not None:
asyncio.create_task(self.managed_node_controller.shutdown())
await self.managed_node_controller.shutdown()
self.cog_cleaned_up = True

View File

@@ -165,14 +165,14 @@ class LavalinkEvents(MixinMeta, metaclass=CompositeMetaClass):
try:
await self.api_interface.autoplay(player, self.playlist_api)
except DatabaseError:
notify_channel = guild.get_channel(notify_channel_id)
notify_channel = guild.get_channel_or_thread(notify_channel_id)
if notify_channel and self._has_notify_perms(notify_channel):
await self.send_embed_msg(
notify_channel, title=_("Couldn't get a valid track.")
)
return
except TrackEnqueueError:
notify_channel = guild.get_channel(notify_channel_id)
notify_channel = guild.get_channel_or_thread(notify_channel_id)
if notify_channel and self._has_notify_perms(notify_channel):
await self.send_embed_msg(
notify_channel,
@@ -185,7 +185,7 @@ class LavalinkEvents(MixinMeta, metaclass=CompositeMetaClass):
return
if event_type == lavalink.LavalinkEvents.TRACK_START and notify:
notify_channel_id = player.fetch("notify_channel")
notify_channel = guild.get_channel(notify_channel_id)
notify_channel = guild.get_channel_or_thread(notify_channel_id)
if notify_channel and self._has_notify_perms(notify_channel):
if player.fetch("notify_message") is not None:
with contextlib.suppress(discord.HTTPException):
@@ -226,7 +226,7 @@ class LavalinkEvents(MixinMeta, metaclass=CompositeMetaClass):
if event_type == lavalink.LavalinkEvents.QUEUE_END:
if not autoplay:
notify_channel_id = player.fetch("notify_channel")
notify_channel = guild.get_channel(notify_channel_id)
notify_channel = guild.get_channel_or_thread(notify_channel_id)
if notify_channel and notify and self._has_notify_perms(notify_channel):
await self.send_embed_msg(notify_channel, title=_("Queue ended."))
if disconnect:
@@ -282,7 +282,7 @@ class LavalinkEvents(MixinMeta, metaclass=CompositeMetaClass):
self._ll_guild_updates.discard(guild_id)
self.bot.dispatch("red_audio_audio_disconnect", guild)
if message_channel:
message_channel = guild.get_channel(message_channel)
message_channel = guild.get_channel_or_thread(message_channel)
if early_exit:
log.warning(
"Audio detected multiple continuous errors during playback "

View File

@@ -267,14 +267,14 @@ class StartUpTasks(MixinMeta, metaclass=CompositeMetaClass):
try:
await self.api_interface.autoplay(player, self.playlist_api)
except DatabaseError:
notify_channel = guild.get_channel(notify_channel)
notify_channel = guild.get_channel_or_thread(notify_channel)
if notify_channel:
await self.send_embed_msg(
notify_channel, title=_("Couldn't get a valid track.")
)
return
except TrackEnqueueError:
notify_channel = guild.get_channel(notify_channel)
notify_channel = guild.get_channel_or_thread(notify_channel)
if notify_channel:
await self.send_embed_msg(
notify_channel,

View File

@@ -9,7 +9,6 @@ import discord
import lavalink
from red_commons.logging import getLogger
from discord.embeds import EmptyEmbed
from lavalink import NodeNotFound
from redbot.core import commands
@@ -94,7 +93,7 @@ class FormattingUtilities(MixinMeta, metaclass=CompositeMetaClass):
if not self._player_check(ctx):
if self.lavalink_connection_aborted:
msg = _("Connection to Lavalink node has failed")
description = EmptyEmbed
description = None
if await self.bot.is_owner(ctx.author):
description = _("Please check your console or logs for details.")
return await self.send_embed_msg(ctx, title=msg, description=description)

View File

@@ -10,13 +10,12 @@ from typing import Any, Final, Mapping, MutableMapping, Pattern, Union, cast
import discord
import lavalink
from discord.embeds import EmptyEmbed
from red_commons.logging import getLogger
from redbot.core import bank, commands
from redbot.core.commands import Context
from redbot.core.i18n import Translator
from redbot.core.utils import AsyncIter
from redbot.core.utils import AsyncIter, can_user_send_messages_in
from redbot.core.utils.chat_formatting import humanize_number
from ...apis.playlist_interface import get_all_playlist_for_migration23
@@ -65,10 +64,10 @@ class MiscellaneousUtilities(MixinMeta, metaclass=CompositeMetaClass):
self, ctx: commands.Context, author: Mapping[str, str] = None, **kwargs
) -> discord.Message:
colour = kwargs.get("colour") or kwargs.get("color") or await self.bot.get_embed_color(ctx)
title = kwargs.get("title", EmptyEmbed) or EmptyEmbed
title = kwargs.get("title") or None
_type = kwargs.get("type", "rich") or "rich"
url = kwargs.get("url", EmptyEmbed) or EmptyEmbed
description = kwargs.get("description", EmptyEmbed) or EmptyEmbed
url = kwargs.get("url") or None
description = kwargs.get("description") or None
timestamp = kwargs.get("timestamp")
footer = kwargs.get("footer")
thumbnail = kwargs.get("thumbnail")
@@ -84,7 +83,6 @@ class MiscellaneousUtilities(MixinMeta, metaclass=CompositeMetaClass):
embed = discord.Embed.from_dict(contents)
embed.color = colour
if timestamp and isinstance(timestamp, datetime.datetime):
timestamp = timestamp.replace(tzinfo=datetime.timezone.utc)
embed.timestamp = timestamp
else:
embed.timestamp = datetime.datetime.now(tz=datetime.timezone.utc)
@@ -101,9 +99,9 @@ class MiscellaneousUtilities(MixinMeta, metaclass=CompositeMetaClass):
embed.set_author(name=name)
return await ctx.send(embed=embed)
def _has_notify_perms(self, channel: discord.TextChannel) -> bool:
def _has_notify_perms(self, channel: Union[discord.TextChannel, discord.Thread]) -> bool:
perms = channel.permissions_for(channel.guild.me)
return all((perms.send_messages, perms.embed_links))
return all((can_user_send_messages_in(channel.guild.me, channel), perms.embed_links))
async def maybe_run_pending_db_tasks(self, ctx: commands.Context) -> None:
if self.api_interface is not None:

View File

@@ -8,7 +8,6 @@ import discord
import lavalink
from red_commons.logging import getLogger
from discord.embeds import EmptyEmbed
from lavalink import NodeNotFound, PlayerNotFound
from redbot.core import commands
@@ -585,7 +584,7 @@ class PlayerUtilities(MixinMeta, metaclass=CompositeMetaClass):
except IndexError:
self.update_player_lock(ctx, False)
title = _("Nothing found")
desc = EmptyEmbed
desc = None
if await self.bot.is_owner(ctx.author):
desc = _("Please check your console or logs for details.")
return await self.send_embed_msg(ctx, title=title, description=desc)

View File

@@ -12,7 +12,6 @@ from typing import List, MutableMapping, Optional, Tuple, Union
import aiohttp
import discord
import lavalink
from discord.embeds import EmptyEmbed
from lavalink import NodeNotFound
from red_commons.logging import getLogger
@@ -525,7 +524,7 @@ class PlaylistUtilities(MixinMeta, metaclass=CompositeMetaClass):
if not self._player_check(ctx):
if self.lavalink_connection_aborted:
msg = _("Connection to Lavalink node has failed")
desc = EmptyEmbed
desc = None
if await self.bot.is_owner(ctx.author):
desc = _("Please check your console or logs for details.")
await self.send_embed_msg(ctx, title=msg, description=desc)

View File

@@ -60,7 +60,7 @@ class ValidationUtilities(MixinMeta, metaclass=CompositeMetaClass):
async def is_query_allowed(
self,
config: Config,
ctx_or_channel: Optional[Union[Context, discord.TextChannel]],
ctx_or_channel: Optional[Union[Context, discord.TextChannel, discord.Thread]],
query: str,
query_obj: Query,
) -> bool:

View File

@@ -2,5 +2,5 @@ from .cleanup import Cleanup
from redbot.core.bot import Red
def setup(bot: Red):
bot.add_cog(Cleanup(bot))
async def setup(bot: Red) -> None:
await bot.add_cog(Cleanup(bot))

View File

@@ -1,6 +1,6 @@
import contextlib
import logging
from datetime import datetime, timedelta
from datetime import datetime, timedelta, timezone
from typing import Callable, List, Optional, Set, Union
import discord
@@ -75,7 +75,7 @@ class Cleanup(commands.Cog):
@staticmethod
async def get_messages_for_deletion(
*,
channel: Union[discord.TextChannel, discord.DMChannel],
channel: Union[discord.TextChannel, discord.DMChannel, discord.Thread],
number: Optional[PositiveInt] = None,
check: Callable[[discord.Message], bool] = lambda x: True,
limit: Optional[PositiveInt] = None,
@@ -99,7 +99,7 @@ class Cleanup(commands.Cog):
"""
# This isn't actually two weeks ago to allow some wiggle room on API limits
two_weeks_ago = datetime.utcnow() - timedelta(days=14, minutes=-5)
two_weeks_ago = datetime.now(timezone.utc) - timedelta(days=14, minutes=-5)
def message_filter(message):
return (
@@ -129,7 +129,7 @@ class Cleanup(commands.Cog):
async def send_optional_notification(
self,
num: int,
channel: Union[discord.TextChannel, discord.DMChannel],
channel: Union[discord.TextChannel, discord.DMChannel, discord.Thread],
*,
subtract_invoking: bool = False,
) -> None:
@@ -149,7 +149,7 @@ class Cleanup(commands.Cog):
@staticmethod
async def get_message_from_reference(
channel: discord.TextChannel, reference: discord.MessageReference
channel: Union[discord.TextChannel, discord.Thread], reference: discord.MessageReference
) -> Optional[discord.Message]:
message = None
resolved = reference.resolved
@@ -696,7 +696,12 @@ class Cleanup(commands.Cog):
def check(m):
if m.attachments:
return False
c = (m.author.id, m.content, [e.to_dict() for e in m.embeds])
c = (
m.author.id,
m.content,
[embed.to_dict() for embed in m.embeds],
[sticker.id for sticker in m.stickers],
)
if c in msgs:
spam.append(m)
return True

View File

@@ -1,5 +1,7 @@
from redbot.core.bot import Red
from .customcom import CustomCommands
def setup(bot):
bot.add_cog(CustomCommands(bot))
async def setup(bot: Red) -> None:
await bot.add_cog(CustomCommands(bot))

View File

@@ -3,7 +3,6 @@ import re
import random
from datetime import datetime, timedelta
from inspect import Parameter
from collections import OrderedDict
from typing import Iterable, List, Mapping, Tuple, Dict, Set, Literal, Union
from urllib.parse import quote_plus
@@ -540,7 +539,7 @@ class CustomCommands(commands.Cog):
)
@customcom.command(name="list")
@checks.bot_has_permissions(add_reactions=True)
@commands.bot_can_react()
async def cc_list(self, ctx: commands.Context):
"""List all available custom commands.
@@ -636,12 +635,15 @@ class CustomCommands(commands.Cog):
@commands.Cog.listener()
async def on_message_without_command(self, message):
is_private = isinstance(message.channel, discord.abc.PrivateChannel)
is_private = message.guild is None
# user_allowed check, will be replaced with self.bot.user_allowed or
# something similar once it's added
user_allowed = True
if isinstance(message.channel, discord.PartialMessageable):
return
if len(message.content) < 2 or is_private or not user_allowed or message.author.bot:
return
@@ -705,9 +707,8 @@ class CustomCommands(commands.Cog):
@staticmethod
def prepare_args(raw_response) -> Mapping[str, Parameter]:
args = re.findall(r"{(\d+)[^:}]*(:[^.}]*)?[^}]*\}", raw_response)
default = [("ctx", Parameter("ctx", Parameter.POSITIONAL_OR_KEYWORD))]
if not args:
return OrderedDict(default)
return {}
allowed_builtins = {
"bool": bool,
"complex": complex,
@@ -775,9 +776,7 @@ class CustomCommands(commands.Cog):
i if i < high else "final",
)
fin[i] = fin[i].replace(name=name)
# insert ctx parameter for discord.py parsing
fin = default + [(p.name, p) for p in fin]
return OrderedDict(fin)
return dict((p.name, p) for p in fin)
def test_cooldowns(self, ctx, command, cooldowns):
now = datetime.utcnow()

View File

@@ -1,7 +1,9 @@
from redbot.core.bot import Red
from .downloader import Downloader
async def setup(bot):
async def setup(bot: Red) -> None:
cog = Downloader(bot)
bot.add_cog(cog)
await bot.add_cog(cog)
cog.create_init_task()

View File

@@ -13,6 +13,7 @@ from redbot.core import checks, commands, Config, version_info as red_version_in
from redbot.core.bot import Red
from redbot.core.data_manager import cog_data_path
from redbot.core.i18n import Translator, cog_i18n
from redbot.core.utils import can_user_react_in
from redbot.core.utils.chat_formatting import box, pagify, humanize_list, inline
from redbot.core.utils.menus import start_adding_reactions
from redbot.core.utils.predicates import MessagePredicate, ReactionPredicate
@@ -933,7 +934,7 @@ class Downloader(commands.Cog):
poss_installed_path = (await self.cog_install_path()) / real_name
if poss_installed_path.exists():
with contextlib.suppress(commands.ExtensionNotLoaded):
ctx.bot.unload_extension(real_name)
await ctx.bot.unload_extension(real_name)
await ctx.bot.remove_loaded_package(real_name)
await self._delete_cog(poss_installed_path)
uninstalled_cogs.append(inline(real_name))
@@ -1665,7 +1666,7 @@ class Downloader(commands.Cog):
if len(updated_cognames) > 1
else _("Would you like to reload the updated cog?")
)
can_react = ctx.channel.permissions_for(ctx.me).add_reactions
can_react = can_user_react_in(ctx.me, ctx.channel)
if not can_react:
message += " (yes/no)"
query: discord.Message = await ctx.send(message)

View File

@@ -2,5 +2,5 @@ from redbot.core.bot import Red
from .economy import Economy
def setup(bot: Red):
bot.add_cog(Economy(bot))
async def setup(bot: Red) -> None:
await bot.add_cog(Economy(bot))

View File

@@ -434,11 +434,11 @@ class Economy(commands.Cog):
if show_global and await bank.is_global():
# show_global is only applicable if bank is global
bank_sorted = await bank.get_leaderboard(positions=top, guild=None)
base_embed.set_author(name=ctx.bot.user.name, icon_url=ctx.bot.user.avatar_url)
base_embed.set_author(name=ctx.bot.user.name, icon_url=ctx.bot.user.display_avatar)
else:
bank_sorted = await bank.get_leaderboard(positions=top, guild=guild)
if guild:
base_embed.set_author(name=guild.name, icon_url=guild.icon_url)
base_embed.set_author(name=guild.name, icon_url=guild.icon)
try:
bal_len = len(humanize_number(bank_sorted[0][1]["balance"]))

View File

@@ -3,6 +3,4 @@ from redbot.core.bot import Red
async def setup(bot: Red) -> None:
cog = Filter(bot)
await cog.initialize()
bot.add_cog(cog)
await bot.add_cog(Filter(bot))

View File

@@ -56,7 +56,7 @@ class Filter(commands.Cog):
if user_id in guild_data:
await self.config.member_from_ids(guild_id, user_id).clear()
async def initialize(self) -> None:
async def cog_load(self) -> None:
await self.register_casetypes()
@staticmethod
@@ -205,6 +205,14 @@ class Filter(commands.Cog):
async def _channel_clear(self, ctx: commands.Context):
"""Clears this channel's filter list."""
channel = ctx.channel
if isinstance(channel, discord.Thread):
await ctx.send(
_(
"Threads can't have a filter list set up. If you want to clear this list for"
" the parent channel, send the command in that channel."
)
)
return
author = ctx.author
filter_list = await self.config.channel(channel).filter()
if not filter_list:
@@ -228,7 +236,7 @@ class Filter(commands.Cog):
@_filter_channel.command(name="list")
async def _channel_list(self, ctx: commands.Context):
"""Send a list of the channel's filtered words."""
channel = ctx.channel
channel = ctx.channel.parent if isinstance(ctx.channel, discord.Thread) else ctx.channel
author = ctx.author
word_list = await self.config.channel(channel).filter()
if not word_list:
@@ -257,6 +265,14 @@ class Filter(commands.Cog):
- `[words...]` The words or sentences to filter.
"""
channel = ctx.channel
if isinstance(channel, discord.Thread):
await ctx.send(
_(
"Threads can't have a filter list set up. If you want to add words to"
" the list of the parent channel, send the command in that channel."
)
)
return
added = await self.add_to_filter(channel, words)
if added:
self.invalidate_cache(ctx.guild, ctx.channel)
@@ -279,6 +295,14 @@ class Filter(commands.Cog):
- `[words...]` The words or sentences to no longer filter.
"""
channel = ctx.channel
if isinstance(channel, discord.Thread):
await ctx.send(
_(
"Threads can't have a filter list set up. If you want to remove words from"
" the list of the parent channel, send the command in that channel."
)
)
return
removed = await self.remove_from_filter(channel, words)
if removed:
await ctx.send(_("Words removed from filter."))
@@ -397,14 +421,19 @@ class Filter(commands.Cog):
return removed
async def filter_hits(
self, text: str, server_or_channel: Union[discord.Guild, discord.TextChannel]
self,
text: str,
server_or_channel: Union[discord.Guild, discord.TextChannel, discord.Thread],
) -> Set[str]:
try:
guild = server_or_channel.guild
channel = server_or_channel
except AttributeError:
if isinstance(server_or_channel, discord.Guild):
guild = server_or_channel
channel = None
else:
guild = server_or_channel.guild
if isinstance(server_or_channel, discord.Thread):
channel = server_or_channel.parent
else:
channel = server_or_channel
hits: Set[str] = set()
@@ -437,7 +466,7 @@ class Filter(commands.Cog):
filter_time = guild_data["filterban_time"]
user_count = member_data["filter_count"]
next_reset_time = member_data["next_reset_time"]
created_at = message.created_at.replace(tzinfo=timezone.utc)
created_at = message.created_at
if filter_count > 0 and filter_time > 0:
if created_at.timestamp() >= next_reset_time:
@@ -451,10 +480,16 @@ class Filter(commands.Cog):
hits = await self.filter_hits(message.content, message.channel)
if hits:
# modlog doesn't accept PartialMessageable
channel = (
None
if isinstance(message.channel, discord.PartialMessageable)
else message.channel
)
await modlog.create_case(
bot=self.bot,
guild=guild,
created_at=message.created_at.replace(tzinfo=timezone.utc),
created_at=created_at,
action_type="filterhit",
user=author,
moderator=guild.me,
@@ -463,7 +498,7 @@ class Filter(commands.Cog):
if len(hits) > 1
else _("Filtered word used: {word}").format(word=list(hits)[0])
),
channel=message.channel,
channel=channel,
)
try:
await message.delete()
@@ -484,7 +519,7 @@ class Filter(commands.Cog):
await modlog.create_case(
self.bot,
guild,
message.created_at.replace(tzinfo=timezone.utc),
message.created_at,
"filterban",
author,
guild.me,

View File

@@ -1,5 +1,7 @@
from redbot.core.bot import Red
from .general import General
def setup(bot):
bot.add_cog(General())
async def setup(bot: Red) -> None:
await bot.add_cog(General(bot))

View File

@@ -7,6 +7,7 @@ import urllib.parse
import aiohttp
import discord
from redbot.core import commands
from redbot.core.bot import Red
from redbot.core.i18n import Translator, cog_i18n
from redbot.core.utils.menus import menu, DEFAULT_CONTROLS
from redbot.core.utils.chat_formatting import (
@@ -72,8 +73,9 @@ class General(commands.Cog):
]
_ = T_
def __init__(self):
def __init__(self, bot: Red) -> None:
super().__init__()
self.bot = bot
self.stopwatches = {}
async def red_delete_data_for_user(self, **kwargs):
@@ -254,18 +256,22 @@ class General(commands.Cog):
Default to False.
"""
guild = ctx.guild
created_at = _("Created on <t:{0}>. That's <t:{0}:R>!").format(
int(guild.created_at.replace(tzinfo=datetime.timezone.utc).timestamp()),
created_at = _("Created on {date_and_time}. That's {relative_time}!").format(
date_and_time=discord.utils.format_dt(guild.created_at),
relative_time=discord.utils.format_dt(guild.created_at, "R"),
)
online = humanize_number(
len([m.status for m in guild.members if m.status != discord.Status.offline])
)
total_users = humanize_number(guild.member_count)
total_users = guild.member_count and humanize_number(guild.member_count)
text_channels = humanize_number(len(guild.text_channels))
voice_channels = humanize_number(len(guild.voice_channels))
if not details:
data = discord.Embed(description=created_at, colour=await ctx.embed_colour())
data.add_field(name=_("Users online"), value=f"{online}/{total_users}")
data.add_field(
name=_("Users online"),
value=f"{online}/{total_users}" if total_users else _("Not available"),
)
data.add_field(name=_("Text Channels"), value=text_channels)
data.add_field(name=_("Voice Channels"), value=voice_channels)
data.add_field(name=_("Roles"), value=humanize_number(len(guild.roles)))
@@ -277,9 +283,9 @@ class General(commands.Cog):
command=f"{ctx.clean_prefix}serverinfo 1"
)
)
if guild.icon_url:
data.set_author(name=guild.name, url=guild.icon_url)
data.set_thumbnail(url=guild.icon_url)
if guild.icon:
data.set_author(name=guild.name, url=guild.icon)
data.set_thumbnail(url=guild.icon)
else:
data.set_author(name=guild.name)
else:
@@ -342,7 +348,7 @@ class General(commands.Cog):
"low": _("1 - Low"),
"medium": _("2 - Medium"),
"high": _("3 - High"),
"extreme": _("4 - Extreme"),
"highest": _("4 - Highest"),
}
features = {
@@ -389,10 +395,10 @@ class General(commands.Cog):
if "VERIFIED" in guild.features
else "https://cdn.discordapp.com/emojis/508929941610430464.png"
if "PARTNERED" in guild.features
else discord.Embed.Empty,
else None,
)
if guild.icon_url:
data.set_thumbnail(url=guild.icon_url)
if guild.icon:
data.set_thumbnail(url=guild.icon)
data.add_field(name=_("Members:"), value=member_msg)
data.add_field(
name=_("Channels:"),
@@ -444,7 +450,7 @@ class General(commands.Cog):
)
data.add_field(name=_("Nitro Boost:"), value=nitro_boost)
if guild.splash:
data.set_image(url=guild.splash_url_as(format="png"))
data.set_image(url=guild.splash.replace(format="png"))
data.set_footer(text=joined_on)
await ctx.send(embed=data)

View File

@@ -1,7 +1,7 @@
from redbot.core.bot import Red
from .image import Image
async def setup(bot):
cog = Image(bot)
await cog.initialize()
bot.add_cog(cog)
async def setup(bot: Red) -> None:
await bot.add_cog(Image(bot))

View File

@@ -24,14 +24,7 @@ class Image(commands.Cog):
self.session = aiohttp.ClientSession()
self.imgur_base_url = "https://api.imgur.com/3/"
def cog_unload(self):
self.session.detach()
async def red_delete_data_for_user(self, **kwargs):
"""Nothing to delete"""
return
async def initialize(self) -> None:
async def cog_load(self) -> None:
"""Move the API keys from cog stored config to core bot config if they exist."""
imgur_token = await self.config.imgur_client_id()
if imgur_token is not None:
@@ -39,6 +32,13 @@ class Image(commands.Cog):
await self.bot.set_shared_api_tokens("imgur", client_id=imgur_token)
await self.config.imgur_client_id.clear()
async def cog_unload(self):
await self.session.close()
async def red_delete_data_for_user(self, **kwargs):
"""Nothing to delete"""
return
@commands.group(name="imgur")
async def _imgur(self, ctx):
"""Retrieve pictures from Imgur.

View File

@@ -2,7 +2,5 @@ from redbot.core.bot import Red
from .mod import Mod
async def setup(bot: Red):
cog = Mod(bot)
bot.add_cog(cog)
await cog.initialize()
async def setup(bot: Red) -> None:
await bot.add_cog(Mod(bot))

View File

@@ -64,7 +64,7 @@ class Events(MixinMeta):
await modlog.create_case(
self.bot,
guild,
message.created_at.replace(tzinfo=timezone.utc),
message.created_at,
"ban",
author,
guild.me,
@@ -88,7 +88,7 @@ class Events(MixinMeta):
await modlog.create_case(
self.bot,
guild,
message.created_at.replace(tzinfo=timezone.utc),
message.created_at,
"kick",
author,
guild.me,
@@ -120,7 +120,7 @@ class Events(MixinMeta):
await modlog.create_case(
self.bot,
guild,
message.created_at.replace(tzinfo=timezone.utc),
message.created_at,
"warning",
author,
guild.me,

View File

@@ -29,23 +29,13 @@ class KickBanMixin(MixinMeta):
"""
@staticmethod
async def get_invite_for_reinvite(ctx: commands.Context, max_age: int = 86400):
"""Handles the reinvite logic for getting an invite
to send the newly unbanned user
:returns: :class:`Invite`"""
async def get_invite_for_reinvite(ctx: commands.Context, max_age: int = 86400) -> str:
"""Handles the reinvite logic for getting an invite to send the newly unbanned user"""
guild = ctx.guild
my_perms: discord.Permissions = guild.me.guild_permissions
if my_perms.manage_guild or my_perms.administrator:
if "VANITY_URL" in guild.features:
# guild has a vanity url so use it as the one to send
try:
return await guild.vanity_invite()
except discord.NotFound:
# If a guild has the vanity url feature,
# but does not have it set up,
# this prevents the command from failing
# and defaults back to another regular invite.
pass
if guild.vanity_url is not None:
return guild.vanity_url
invites = await guild.invites()
else:
invites = []
@@ -55,22 +45,22 @@ class KickBanMixin(MixinMeta):
# has unlimited uses, doesn't expire, and
# doesn't grant temporary membership
# (i.e. they won't be kicked on disconnect)
return inv
return inv.url
else: # No existing invite found that is valid
channels_and_perms = zip(
guild.text_channels, map(guild.me.permissions_in, guild.text_channels)
channels_and_perms = (
(channel, channel.permissions_for(guild.me)) for channel in guild.text_channels
)
channel = next(
(channel for channel, perms in channels_and_perms if perms.create_instant_invite),
None,
)
if channel is None:
return
return ""
try:
# Create invite that expires after max_age
return await channel.create_invite(max_age=max_age)
return (await channel.create_invite(max_age=max_age)).url
except discord.HTTPException:
return
return ""
@staticmethod
async def _voice_perm_check(
@@ -220,7 +210,7 @@ class KickBanMixin(MixinMeta):
await modlog.create_case(
self.bot,
guild,
ctx.message.created_at.replace(tzinfo=timezone.utc),
ctx.message.created_at,
ban_type,
user,
author,
@@ -356,7 +346,7 @@ class KickBanMixin(MixinMeta):
await modlog.create_case(
self.bot,
guild,
ctx.message.created_at.replace(tzinfo=timezone.utc),
ctx.message.created_at,
"kick",
member,
author,
@@ -566,7 +556,7 @@ class KickBanMixin(MixinMeta):
await modlog.create_case(
self.bot,
guild,
ctx.message.created_at.replace(tzinfo=timezone.utc),
ctx.message.created_at,
"hackban",
user_id,
author,
@@ -636,8 +626,6 @@ class KickBanMixin(MixinMeta):
await ctx.send(_("Invalid days. Must be between 0 and 7."))
return
invite = await self.get_invite_for_reinvite(ctx, int(duration.total_seconds() + 86400))
if invite is None:
invite = ""
await self.config.member(member).banned_until.set(unban_time.timestamp())
async with self.config.guild(guild).current_tempbans() as current_tempbans:
@@ -646,7 +634,7 @@ class KickBanMixin(MixinMeta):
with contextlib.suppress(discord.HTTPException):
# We don't want blocked DMs preventing us from banning
msg = _("You have been temporarily banned from {server_name} until {date}.").format(
server_name=guild.name, date=f"<t:{int(unban_time.timestamp())}>"
server_name=guild.name, date=discord.utils.format_dt(unban_time)
)
if guild_data["dm_on_kickban"] and reason:
msg += _("\n\n**Reason:** {reason}").format(reason=reason)
@@ -668,7 +656,7 @@ class KickBanMixin(MixinMeta):
await modlog.create_case(
self.bot,
guild,
ctx.message.created_at.replace(tzinfo=timezone.utc),
ctx.message.created_at,
"tempban",
member,
author,
@@ -706,8 +694,6 @@ class KickBanMixin(MixinMeta):
audit_reason = get_audit_reason(author, reason, shorten=True)
invite = await self.get_invite_for_reinvite(ctx)
if invite is None:
invite = ""
try: # We don't want blocked DMs preventing us from banning
msg = await member.send(
@@ -750,7 +736,7 @@ class KickBanMixin(MixinMeta):
await modlog.create_case(
self.bot,
guild,
ctx.message.created_at.replace(tzinfo=timezone.utc),
ctx.message.created_at,
"softban",
member,
author,
@@ -797,7 +783,7 @@ class KickBanMixin(MixinMeta):
await modlog.create_case(
self.bot,
guild,
ctx.message.created_at.replace(tzinfo=timezone.utc),
ctx.message.created_at,
"vkick",
member,
author,
@@ -840,7 +826,7 @@ class KickBanMixin(MixinMeta):
await modlog.create_case(
self.bot,
guild,
ctx.message.created_at.replace(tzinfo=timezone.utc),
ctx.message.created_at,
"voiceunban",
member,
author,
@@ -881,7 +867,7 @@ class KickBanMixin(MixinMeta):
await modlog.create_case(
self.bot,
guild,
ctx.message.created_at.replace(tzinfo=timezone.utc),
ctx.message.created_at,
"voiceban",
member,
author,
@@ -921,7 +907,7 @@ class KickBanMixin(MixinMeta):
await modlog.create_case(
self.bot,
guild,
ctx.message.created_at.replace(tzinfo=timezone.utc),
ctx.message.created_at,
"unban",
ban_entry.user,
author,
@@ -946,7 +932,7 @@ class KickBanMixin(MixinMeta):
_(
"You've been unbanned from {server}.\n"
"Here is an invite for that server: {invite_link}"
).format(server=guild.name, invite_link=invite.url)
).format(server=guild.name, invite_link=invite)
)
except discord.Forbidden:
await ctx.send(
@@ -954,12 +940,12 @@ class KickBanMixin(MixinMeta):
"I failed to send an invite to that user. "
"Perhaps you may be able to send it for me?\n"
"Here's the invite link: {invite_link}"
).format(invite_link=invite.url)
).format(invite_link=invite)
)
except discord.HTTPException:
await ctx.send(
_(
"Something went wrong when attempting to send that user "
"an invite. Here's the link so you can try: {invite_link}"
).format(invite_link=invite.url)
).format(invite_link=invite)
)

View File

@@ -84,8 +84,6 @@ class Mod(
self.tban_expiry_task = asyncio.create_task(self.tempban_expirations_task())
self.last_case: dict = defaultdict(dict)
self._ready = asyncio.Event()
async def red_delete_data_for_user(
self,
*,
@@ -114,12 +112,8 @@ class Mod(
pass
# possible with a context switch between here and getting all guilds
async def initialize(self):
async def cog_load(self) -> None:
await self._maybe_update_config()
self._ready.set()
async def cog_before_invoke(self, ctx: commands.Context) -> None:
await self._ready.wait()
def cog_unload(self):
self.tban_expiry_task.cancel()

View File

@@ -195,9 +195,8 @@ class ModInfo(MixinMeta):
if is_special:
joined_at = special_date
elif joined_at := member.joined_at:
joined_at = joined_at.replace(tzinfo=datetime.timezone.utc)
user_created = int(member.created_at.replace(tzinfo=datetime.timezone.utc).timestamp())
else:
joined_at = member.joined_at
voice_state = member.voice
member_number = (
sorted(guild.members, key=lambda m: m.joined_at or ctx.message.created_at).index(
@@ -206,9 +205,15 @@ class ModInfo(MixinMeta):
+ 1
)
created_on = "<t:{0}>\n(<t:{0}:R>)".format(user_created)
created_on = (
f"{discord.utils.format_dt(member.created_at)}\n"
f"{discord.utils.format_dt(member.created_at, 'R')}"
)
if joined_at is not None:
joined_on = "<t:{0}>\n(<t:{0}:R>)".format(int(joined_at.timestamp()))
joined_on = (
f"{discord.utils.format_dt(joined_at)}\n"
f"{discord.utils.format_dt(joined_at, 'R')}"
)
else:
joined_on = _("Unknown")
@@ -296,7 +301,7 @@ class ModInfo(MixinMeta):
name = " ~ ".join((name, member.nick)) if member.nick else name
name = filter_invites(name)
avatar = member.avatar_url_as(static_format="png")
avatar = member.display_avatar.replace(static_format="png")
data.set_author(name=f"{statusemoji} {name}", url=avatar)
data.set_thumbnail(url=avatar)

View File

@@ -14,8 +14,8 @@ class Slowmode(MixinMeta):
@commands.command()
@commands.guild_only()
@commands.bot_has_permissions(manage_channels=True)
@checks.admin_or_permissions(manage_channels=True)
@commands.bot_can_manage_channel()
@commands.admin_or_can_manage_channel()
async def slowmode(
self,
ctx,
@@ -24,7 +24,7 @@ class Slowmode(MixinMeta):
minimum=timedelta(seconds=0), maximum=timedelta(hours=6), default_unit="seconds"
) = timedelta(seconds=0),
):
"""Changes channel's slowmode setting.
"""Changes thread's or channel's slowmode setting.
Interval can be anything from 0 seconds to 6 hours.
Use without parameters to disable.

View File

@@ -2,5 +2,5 @@ from redbot.core.bot import Red
from .modlog import ModLog
def setup(bot: Red):
bot.add_cog(ModLog(bot))
async def setup(bot: Red) -> None:
await bot.add_cog(ModLog(bot))

View File

@@ -40,7 +40,11 @@ class ModLog(commands.Cog):
if await ctx.embed_requested():
await ctx.send(embed=await case.message_content(embed=True))
else:
message = f"{await case.message_content(embed=False)}\n{bold(_('Timestamp:'))} <t:{int(case.created_at)}>"
created_at = datetime.fromtimestamp(case.created_at, tz=timezone.utc)
message = (
f"{await case.message_content(embed=False)}\n"
f"{bold(_('Timestamp:'))} {discord.utils.format_dt(created_at)}"
)
await ctx.send(message)
@commands.command()
@@ -73,7 +77,11 @@ class ModLog(commands.Cog):
else:
rendered_cases = []
for case in cases:
message = f"{await case.message_content(embed=False)}\n{bold(_('Timestamp:'))} <t:{int(case.created_at)}>"
created_at = datetime.fromtimestamp(case.created_at, tz=timezone.utc)
message = (
f"{await case.message_content(embed=False)}\n"
f"{bold(_('Timestamp:'))} {discord.utils.format_dt(created_at)}"
)
rendered_cases.append(message)
await menu(ctx, rendered_cases, DEFAULT_CONTROLS)
@@ -104,7 +112,11 @@ class ModLog(commands.Cog):
rendered_cases = []
message = ""
for case in cases:
message += f"{await case.message_content(embed=False)}\n{bold(_('Timestamp:'))} <t:{int(case.created_at)}>"
created_at = datetime.fromtimestamp(case.created_at, tz=timezone.utc)
message += (
f"{await case.message_content(embed=False)}\n"
f"{bold(_('Timestamp:'))} {discord.utils.format_dt(created_at)}"
)
for page in pagify(message, ["\n\n", "\n"], priority=True):
rendered_cases.append(page)
await menu(ctx, rendered_cases, DEFAULT_CONTROLS)
@@ -143,7 +155,7 @@ class ModLog(commands.Cog):
to_modify = {"reason": reason}
if case_obj.moderator != author:
to_modify["amended_by"] = author
to_modify["modified_at"] = ctx.message.created_at.replace(tzinfo=timezone.utc).timestamp()
to_modify["modified_at"] = ctx.message.created_at.timestamp()
await case_obj.edit(to_modify)
await ctx.send(
_("Reason for case #{num} has been updated.").format(num=case_obj.case_number)

View File

@@ -2,6 +2,7 @@ from redbot.core.bot import Red
from .mutes import Mutes
def setup(bot: Red):
async def setup(bot: Red) -> None:
cog = Mutes(bot)
bot.add_cog(cog)
await bot.add_cog(cog)
cog.create_init_task()

View File

@@ -12,7 +12,7 @@ from .voicemutes import VoiceMutes
from redbot.core.bot import Red
from redbot.core import commands, checks, i18n, modlog, Config
from redbot.core.utils import AsyncIter, bounded_gather
from redbot.core.utils import AsyncIter, bounded_gather, can_user_react_in
from redbot.core.utils.chat_formatting import (
bold,
humanize_timedelta,
@@ -87,28 +87,42 @@ class Mutes(VoiceMutes, commands.Cog, metaclass=CompositeMetaClass):
"dm": False,
"show_mod": False,
}
self.config.register_global(force_role_mutes=True, schema_version=0)
# Tbh I would rather force everyone to use role mutes.
# I also honestly think everyone would agree they're the
# way to go. If for whatever reason someone wants to
# enable channel overwrite mutes for their bot they can.
# Channel overwrite logic still needs to be in place
# for channel mutes methods.
self.config.register_global(force_role_mutes=True, schema_version=0)
self.config.register_guild(**default_guild)
self.config.register_member(perms_cache={})
self.config.register_channel(muted_users={})
self._server_mutes: Dict[int, Dict[int, dict]] = {}
self._channel_mutes: Dict[int, Dict[int, dict]] = {}
self._ready = asyncio.Event()
self._unmute_tasks: Dict[str, asyncio.Task] = {}
self._unmute_task = None
self._unmute_task: Optional[asyncio.Task] = None
self.mute_role_cache: Dict[int, int] = {}
self._channel_mute_events: Dict[int, asyncio.Event] = {}
# this is a dict of guild ID's and asyncio.Events
# to wait for a guild to finish channel unmutes before
# checking for manual overwrites
self._channel_mute_events: Dict[int, asyncio.Event] = {}
self._ready = asyncio.Event()
self._init_task: Optional[asyncio.Task] = None
self._ready_raised = False
self._init_task = asyncio.create_task(self._initialize())
def create_init_task(self) -> None:
def _done_callback(task: asyncio.Task) -> None:
exc = task.exception()
if exc is not None:
log.error(
"An unexpected error occurred during Mutes's initialization.",
exc_info=exc,
)
self._ready_raised = True
self._ready.set()
self._init_task = asyncio.create_task(self.initialize())
self._init_task.add_done_callback(_done_callback)
async def red_delete_data_for_user(
self,
@@ -125,13 +139,17 @@ class Mutes(VoiceMutes, commands.Cog, metaclass=CompositeMetaClass):
return
await self._ready.wait()
if self._ready_raised:
raise RuntimeError(
"Mutes cog is in a bad state, can't proceed with data deletion request."
)
all_members = await self.config.all_members()
for g_id, data in all_members.items():
for m_id, mutes in data.items():
if m_id == user_id:
await self.config.member_from_ids(g_id, m_id).clear()
async def _initialize(self):
async def initialize(self):
await self.bot.wait_until_red_ready()
await self._maybe_update_config()
@@ -184,11 +202,21 @@ class Mutes(VoiceMutes, commands.Cog, metaclass=CompositeMetaClass):
)
async def cog_before_invoke(self, ctx: commands.Context):
await self._ready.wait()
if not self._ready.is_set():
async with ctx.typing():
await self._ready.wait()
if self._ready_raised:
await ctx.send(
"There was an error during Mutes's initialization."
" Check logs for more information."
)
raise commands.CheckFailure()
def cog_unload(self):
self._init_task.cancel()
self._unmute_task.cancel()
if self._init_task is not None:
self._init_task.cancel()
if self._unmute_task is not None:
self._unmute_task.cancel()
for task in self._unmute_tasks.values():
task.cancel()
@@ -208,6 +236,8 @@ class Mutes(VoiceMutes, commands.Cog, metaclass=CompositeMetaClass):
"""
await self.bot.wait_until_red_ready()
await self._ready.wait()
if self._ready_raised:
raise RuntimeError("Mutes cog is in a bad state, cancelling automatic unmute task.")
while True:
await self._clean_tasks()
try:
@@ -533,7 +563,7 @@ class Mutes(VoiceMutes, commands.Cog, metaclass=CompositeMetaClass):
if duration:
duration_str = humanize_timedelta(timedelta=duration)
until = datetime.now(timezone.utc) + duration
until_str = f"<t:{int(until.timestamp())}>"
until_str = discord.utils.format_dt(until)
if moderator is None:
moderator_str = _("Unknown")
@@ -549,7 +579,7 @@ class Mutes(VoiceMutes, commands.Cog, metaclass=CompositeMetaClass):
description=reason,
color=await self.bot.get_embed_color(user),
)
em.timestamp = datetime.utcnow()
em.timestamp = datetime.now(timezone.utc)
if duration:
em.add_field(name=_("Until"), value=until_str)
em.add_field(name=_("Duration"), value=duration_str)
@@ -663,21 +693,18 @@ class Mutes(VoiceMutes, commands.Cog, metaclass=CompositeMetaClass):
}
to_del: List[int] = []
for user_id in self._channel_mutes[after.id].keys():
send_messages = False
speak = False
unmuted = False
if user_id in after_perms:
send_messages = (
after_perms[user_id]["send_messages"] is None
or after_perms[user_id]["send_messages"] is True
)
speak = (
after_perms[user_id]["speak"] is None
or after_perms[user_id]["speak"] is True
)
for perm_name in (
"send_messages",
"send_messages_in_threads",
"create_public_threads",
"create_private_threads",
"speak",
):
unmuted = unmuted or after_perms[user_id][perm_name] is not False
# explicit is better than implicit :thinkies:
if user_id in before_perms and (
user_id not in after_perms or any((send_messages, speak))
):
if user_id in before_perms and (user_id not in after_perms or unmuted):
user = after.guild.get_member(user_id)
send_dm_notification = True
if not user:
@@ -900,7 +927,14 @@ class Mutes(VoiceMutes, commands.Cog, metaclass=CompositeMetaClass):
)
async with ctx.typing():
perms = discord.Permissions()
perms.update(send_messages=False, speak=False, add_reactions=False)
perms.update(
send_messages=False,
send_messages_in_threads=False,
create_public_threads=False,
create_private_threads=False,
speak=False,
add_reactions=False,
)
try:
role = await ctx.guild.create_role(
name=name, permissions=perms, reason=_("Mute role setup")
@@ -942,6 +976,9 @@ class Mutes(VoiceMutes, commands.Cog, metaclass=CompositeMetaClass):
return channel.mention
overs = discord.PermissionOverwrite()
overs.send_messages = False
overs.send_messages_in_threads = False
overs.create_public_threads = False
overs.create_private_threads = False
overs.add_reactions = False
overs.speak = False
try:
@@ -1007,7 +1044,7 @@ class Mutes(VoiceMutes, commands.Cog, metaclass=CompositeMetaClass):
"Role mutes do not have this issue.\n\n"
"Are you sure you want to continue with channel overwrites? "
)
can_react = ctx.channel.permissions_for(ctx.me).add_reactions
can_react = can_user_react_in(ctx.me, ctx.channel)
if can_react:
msg += _(
"Reacting with \N{WHITE HEAVY CHECK MARK} will continue "
@@ -1178,7 +1215,7 @@ class Mutes(VoiceMutes, commands.Cog, metaclass=CompositeMetaClass):
await modlog.create_case(
self.bot,
guild,
ctx.message.created_at.replace(tzinfo=timezone.utc),
ctx.message.created_at,
"smute",
user,
author,
@@ -1233,7 +1270,7 @@ class Mutes(VoiceMutes, commands.Cog, metaclass=CompositeMetaClass):
"Some users could not be properly muted. Would you like to see who, where, and why?"
)
can_react = ctx.channel.permissions_for(ctx.me).add_reactions
can_react = can_user_react_in(ctx.me, ctx.channel)
if not can_react:
message += " (y/n)"
query: discord.Message = await ctx.send(message)
@@ -1279,7 +1316,7 @@ class Mutes(VoiceMutes, commands.Cog, metaclass=CompositeMetaClass):
*,
time_and_reason: MuteTime = {},
):
"""Mute a user in the current text channel.
"""Mute a user in the current text channel (or in the parent of the current thread).
`<users...>` is a space separated list of usernames, ID's, or mentions.
`[time_and_reason]` is the time to mute for and reason. Time is
@@ -1313,6 +1350,8 @@ class Mutes(VoiceMutes, commands.Cog, metaclass=CompositeMetaClass):
)
author = ctx.message.author
channel = ctx.message.channel
if isinstance(channel, discord.Thread):
channel = channel.parent
guild = ctx.guild
audit_reason = get_audit_reason(author, reason, shorten=True)
issue_list = []
@@ -1327,7 +1366,7 @@ class Mutes(VoiceMutes, commands.Cog, metaclass=CompositeMetaClass):
await modlog.create_case(
self.bot,
guild,
ctx.message.created_at.replace(tzinfo=timezone.utc),
ctx.message.created_at,
"cmute",
user,
author,
@@ -1347,7 +1386,7 @@ class Mutes(VoiceMutes, commands.Cog, metaclass=CompositeMetaClass):
msg = _("{users} has been muted in this channel{time}.")
if len(success_list) > 1:
msg = _("{users} have been muted in this channel{time}.")
await channel.send(
await ctx.send(
msg.format(users=humanize_list([f"{u}" for u in success_list]), time=time)
)
if issue_list:
@@ -1397,7 +1436,7 @@ class Mutes(VoiceMutes, commands.Cog, metaclass=CompositeMetaClass):
await modlog.create_case(
self.bot,
guild,
ctx.message.created_at.replace(tzinfo=timezone.utc),
ctx.message.created_at,
"sunmute",
user,
author,
@@ -1435,7 +1474,7 @@ class Mutes(VoiceMutes, commands.Cog, metaclass=CompositeMetaClass):
*,
reason: Optional[str] = None,
):
"""Unmute a user in this channel.
"""Unmute a user in this channel (or in the parent of this thread).
`<users...>` is a space separated list of usernames, ID's, or mentions.
`[reason]` is the reason for the unmute.
@@ -1448,6 +1487,8 @@ class Mutes(VoiceMutes, commands.Cog, metaclass=CompositeMetaClass):
return await ctx.send(_("You cannot unmute yourself."))
async with ctx.typing():
channel = ctx.channel
if isinstance(channel, discord.Thread):
channel = channel.parent
author = ctx.author
guild = ctx.guild
audit_reason = get_audit_reason(author, reason, shorten=True)
@@ -1463,7 +1504,7 @@ class Mutes(VoiceMutes, commands.Cog, metaclass=CompositeMetaClass):
await modlog.create_case(
self.bot,
guild,
ctx.message.created_at.replace(tzinfo=timezone.utc),
ctx.message.created_at,
"cunmute",
user,
author,
@@ -1654,7 +1695,14 @@ class Mutes(VoiceMutes, commands.Cog, metaclass=CompositeMetaClass):
new_overs: dict = {}
move_channel = False
new_overs.update(send_messages=False, add_reactions=False, speak=False)
new_overs.update(
send_messages=False,
send_messages_in_threads=False,
create_public_threads=False,
create_private_threads=False,
add_reactions=False,
speak=False,
)
send_reason = None
if user.voice and user.voice.channel:
if channel.permissions_for(guild.me).move_members:
@@ -1756,7 +1804,14 @@ class Mutes(VoiceMutes, commands.Cog, metaclass=CompositeMetaClass):
if channel.id in perms_cache:
old_values = perms_cache[channel.id]
else:
old_values = {"send_messages": None, "add_reactions": None, "speak": None}
old_values = {
"send_messages": None,
"send_messages_in_threads": None,
"create_public_threads": None,
"create_private_threads": None,
"add_reactions": None,
"speak": None,
}
if user.voice and user.voice.channel:
if channel.permissions_for(guild.me).move_members:

View File

@@ -135,7 +135,7 @@ class VoiceMutes(MixinMeta):
await modlog.create_case(
self.bot,
guild,
ctx.message.created_at.replace(tzinfo=timezone.utc),
ctx.message.created_at,
"vmute",
user,
author,
@@ -211,7 +211,7 @@ class VoiceMutes(MixinMeta):
await modlog.create_case(
self.bot,
guild,
ctx.message.created_at.replace(tzinfo=timezone.utc),
ctx.message.created_at,
"vunmute",
user,
author,

View File

@@ -1,7 +1,9 @@
from redbot.core.bot import Red
from .permissions import Permissions
async def setup(bot):
async def setup(bot: Red) -> None:
cog = Permissions(bot)
await cog.initialize()
# We should add the rules for the Permissions cog and its own commands *before* adding the cog.
@@ -9,4 +11,4 @@ async def setup(bot):
await cog._on_cog_add(cog)
for command in cog.__cog_commands__:
await cog._on_command_add(command)
bot.add_cog(cog)
await bot.add_cog(cog)

View File

@@ -31,7 +31,7 @@ class GlobalUniqueObjectFinder(commands.Converter):
if guild is not None:
return guild
channel: discord.abc.GuildChannel = bot.get_channel(_id)
if channel is not None:
if channel is not None and not isinstance(channel, discord.Thread):
return channel
user: discord.User = bot.get_user(_id)

View File

@@ -10,6 +10,7 @@ from schema import And, Or, Schema, SchemaError, Optional as UseOptional
from redbot.core import checks, commands, config
from redbot.core.bot import Red
from redbot.core.i18n import Translator, cog_i18n
from redbot.core.utils import can_user_react_in
from redbot.core.utils.chat_formatting import box
from redbot.core.utils.menus import start_adding_reactions
from redbot.core.utils.predicates import ReactionPredicate, MessagePredicate
@@ -221,7 +222,7 @@ class Permissions(commands.Cog):
"multiple global or server rules apply to the case, the order they are checked in is:\n"
" 1. Rules about a user.\n"
" 2. Rules about the voice channel a user is in.\n"
" 3. Rules about the text channel a command was issued in.\n"
" 3. Rules about the text channel or a parent of the thread a command was issued in.\n"
" 4. Rules about a role the user has (The highest role they have with a rule will be "
"used).\n"
" 5. Rules about the server a user is in (Global rules only).\n\n"
@@ -686,7 +687,7 @@ class Permissions(commands.Cog):
@staticmethod
async def _confirm(ctx: commands.Context) -> bool:
"""Ask "Are you sure?" and get the response as a bool."""
if ctx.guild is None or ctx.guild.me.permissions_in(ctx.channel).add_reactions:
if ctx.guild is None or can_user_react_in(ctx.guild.me, ctx.channel):
msg = await ctx.send(_("Are you sure?"))
# noinspection PyAsyncCall
task = start_adding_reactions(msg, ReactionPredicate.YES_OR_NO_EMOJIS)
@@ -815,8 +816,8 @@ class Permissions(commands.Cog):
elif rule is False:
cog_or_command.deny_to(model_id, guild_id=guild_id)
def cog_unload(self) -> None:
asyncio.create_task(self._unload_all_rules())
async def cog_unload(self) -> None:
await self._unload_all_rules()
async def _unload_all_rules(self) -> None:
"""Unload all rules set by this cog.

View File

@@ -2,5 +2,5 @@ from redbot.core.bot import Red
from .reports import Reports
def setup(bot: Red):
bot.add_cog(Reports(bot))
async def setup(bot: Red) -> None:
await bot.add_cog(Reports(bot))

View File

@@ -212,7 +212,7 @@ class Reports(commands.Cog):
name=_("Report from {author}{maybe_nick}").format(
author=author, maybe_nick=(f" ({author.nick})" if author.nick else "")
),
icon_url=author.avatar_url,
icon_url=author.display_avatar,
)
em.set_footer(text=_("Report #{}").format(ticket_number))
send_content = None
@@ -391,7 +391,7 @@ class Reports(commands.Cog):
async def response(self, ctx, ticket_number: int):
"""Open a message tunnel.
This tunnel will forward things you say in this channel
This tunnel will forward things you say in this channel or thread
to the ticket opener's direct messages.
Tunnels do not persist across bot restarts.

View File

@@ -1,6 +1,7 @@
from redbot.core.bot import Red
from .streams import Streams
def setup(bot):
cog = Streams(bot)
bot.add_cog(cog)
async def setup(bot: Red) -> None:
await bot.add_cog(Streams(bot))

View File

@@ -79,9 +79,6 @@ class Streams(commands.Cog):
self.yt_cid_pattern = re.compile("^UC[-_A-Za-z0-9]{21}[AQgw]$")
self._ready_event: asyncio.Event = asyncio.Event()
self._init_task: asyncio.Task = asyncio.create_task(self.initialize())
async def red_delete_data_for_user(self, **kwargs):
"""Nothing to delete"""
return
@@ -92,10 +89,8 @@ class Streams(commands.Cog):
return True
return False
async def initialize(self) -> None:
async def cog_load(self) -> None:
"""Should be called straight after cog instantiation."""
await self.bot.wait_until_ready()
try:
await self.move_api_keys()
await self.get_twitch_bearer_token()
@@ -104,16 +99,11 @@ class Streams(commands.Cog):
except Exception as error:
log.exception("Failed to initialize Streams cog:", exc_info=error)
self._ready_event.set()
@commands.Cog.listener()
async def on_red_api_tokens_update(self, service_name, api_tokens):
if service_name == "twitch":
await self.get_twitch_bearer_token(api_tokens)
async def cog_before_invoke(self, ctx: commands.Context):
await self._ready_event.wait()
async def move_api_keys(self) -> None:
"""Move the API keys from cog stored config to core bot config if they exist."""
tokens = await self.config.tokens()
@@ -127,6 +117,29 @@ class Streams(commands.Cog):
await self.bot.set_shared_api_tokens("twitch", client_id=token)
await self.config.tokens.clear()
async def _notify_owner_about_missing_twitch_secret(self) -> None:
message = _(
"You need a client secret key if you want to use the Twitch API on this cog.\n"
"Follow these steps:\n"
"1. Go to this page: {link}.\n"
'2. Click "Manage" on your application.\n'
'3. Click on "New secret".\n'
"5. Copy your client ID and your client secret into:\n"
"{command}"
"\n\n"
"Note: These tokens are sensitive and should only be used in a private channel "
"or in DM with the bot."
).format(
link="https://dev.twitch.tv/console/apps",
command=inline(
"[p]set api twitch client_id {} client_secret {}".format(
_("<your_client_id_here>"), _("<your_client_secret_here>")
)
),
)
await send_to_owners_with_prefix_replaced(self.bot, message)
await self.config.notified_owner_missing_twitch_secret.set(True)
async def get_twitch_bearer_token(self, api_tokens: Optional[Dict] = None) -> None:
tokens = (
await self.bot.get_shared_api_tokens("twitch") if api_tokens is None else api_tokens
@@ -140,28 +153,8 @@ class Streams(commands.Cog):
if notified_owner_missing_twitch_secret is True:
await self.config.notified_owner_missing_twitch_secret.set(False)
except KeyError:
message = _(
"You need a client secret key if you want to use the Twitch API on this cog.\n"
"Follow these steps:\n"
"1. Go to this page: {link}.\n"
'2. Click "Manage" on your application.\n'
'3. Click on "New secret".\n'
"5. Copy your client ID and your client secret into:\n"
"{command}"
"\n\n"
"Note: These tokens are sensitive and should only be used in a private channel "
"or in DM with the bot."
).format(
link="https://dev.twitch.tv/console/apps",
command=inline(
"[p]set api twitch client_id {} client_secret {}".format(
_("<your_client_id_here>"), _("<your_client_secret_here>")
)
),
)
if notified_owner_missing_twitch_secret is False:
await send_to_owners_with_prefix_replaced(self.bot, message)
await self.config.notified_owner_missing_twitch_secret.set(True)
asyncio.create_task(self._notify_owner_about_missing_twitch_secret())
async with aiohttp.ClientSession() as session:
async with session.post(
"https://id.twitch.tv/oauth2/token",
@@ -391,6 +384,9 @@ class Streams(commands.Cog):
await ctx.send(page)
async def stream_alert(self, ctx: commands.Context, _class, channel_name):
if isinstance(ctx.channel, discord.Thread):
await ctx.send("Stream alerts cannot be set up in threads.")
return
stream = self.get_stream(_class, channel_name)
if not stream:
token = await self.bot.get_shared_api_tokens(_class.token_name)

View File

@@ -1,10 +1,11 @@
"""Package for Trivia cog."""
from redbot.core.bot import Red
from .trivia import *
from .session import *
from .log import *
def setup(bot):
async def setup(bot: Red) -> None:
"""Load Trivia."""
cog = Trivia()
bot.add_cog(cog)
await bot.add_cog(Trivia(bot))

View File

@@ -249,7 +249,9 @@ class TriviaSession:
answers = tuple(s.lower() for s in answers)
def _pred(message: discord.Message):
early_exit = message.channel != self.ctx.channel or message.author == self.ctx.guild.me
early_exit = (
message.channel.id != self.ctx.channel.id or message.author == self.ctx.guild.me
)
if early_exit:
return False

View File

@@ -3,7 +3,7 @@ import asyncio
import math
import pathlib
from collections import Counter
from typing import Any, Dict, List, Literal
from typing import Any, Dict, List, Literal, Union
from schema import Schema, Optional, Or, SchemaError
import io
@@ -11,9 +11,10 @@ import yaml
import discord
from redbot.core import Config, commands, checks, bank
from redbot.core.bot import Red
from redbot.core.data_manager import cog_data_path
from redbot.core.i18n import Translator, cog_i18n
from redbot.core.utils import AsyncIter
from redbot.core.utils import AsyncIter, can_user_react_in
from redbot.core.utils.chat_formatting import box, pagify, bold
from redbot.core.utils.menus import start_adding_reactions
from redbot.core.utils.predicates import MessagePredicate, ReactionPredicate
@@ -54,8 +55,9 @@ class InvalidListError(Exception):
class Trivia(commands.Cog):
"""Play trivia with friends!"""
def __init__(self):
def __init__(self, bot: Red) -> None:
super().__init__()
self.bot = bot
self.trivia_sessions = []
self.config = Config.get_conf(self, identifier=UNIQUE_ID, force_registration=True)
@@ -672,7 +674,7 @@ class Trivia(commands.Cog):
filename=filename
)
can_react = ctx.channel.permissions_for(ctx.me).add_reactions
can_react = can_user_react_in(ctx.me, ctx.channel)
if not can_react:
overwrite_message += " (yes/no)"
@@ -706,7 +708,9 @@ class Trivia(commands.Cog):
fp.write(buffer.read())
await ctx.send(_("Saved Trivia list as {filename}.").format(filename=filename))
def _get_trivia_session(self, channel: discord.TextChannel) -> TriviaSession:
def _get_trivia_session(
self, channel: Union[discord.TextChannel, discord.Thread]
) -> TriviaSession:
return next(
(session for session in self.trivia_sessions if session.ctx.channel == channel), None
)

View File

@@ -1,5 +1,7 @@
from redbot.core.bot import Red
from .warnings import Warnings
def setup(bot):
bot.add_cog(Warnings(bot))
async def setup(bot: Red) -> None:
await bot.add_cog(Warnings(bot))

View File

@@ -47,7 +47,9 @@ class Warnings(commands.Cog):
self.config.register_guild(**self.default_guild)
self.config.register_member(**self.default_member)
self.bot = bot
self.registration_task = asyncio.create_task(self.register_warningtype())
async def cog_load(self) -> None:
await self.register_warningtype()
async def red_delete_data_for_user(
self,
@@ -508,7 +510,7 @@ class Warnings(commands.Cog):
await modlog.create_case(
self.bot,
ctx.guild,
ctx.message.created_at.replace(tzinfo=timezone.utc),
ctx.message.created_at,
"warning",
member,
ctx.message.author,
@@ -632,7 +634,7 @@ class Warnings(commands.Cog):
await modlog.create_case(
self.bot,
ctx.guild,
ctx.message.created_at.replace(tzinfo=timezone.utc),
ctx.message.created_at,
"unwarned",
member,
ctx.message.author,