Merge branch 'V3/develop' into cog_guide_core

# Conflicts:
#	redbot/core/core_commands.py
This commit is contained in:
bobloy
2021-01-25 17:17:06 -05:00
19 changed files with 607 additions and 153 deletions

View File

@@ -35,6 +35,17 @@ You can add additional Red based arguments after the instance, such as :code:`--
<Red Instance>
The name of your Red instance.
If you used :code:`pyenv virtualenv` to create your virtual environment, please make the following changes to the above generated command
.. code-block:: none
<Location to your Python Interpreter>
Run the following instead to get your Python interpreter
pyenv which python
Replace the `redbot` part of `pm2 start redbot` with the output of the following (when ran inside your activated venv)
pyenv which redbot
------------------------------
Ensuring that PM2 stays online
------------------------------

View File

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

View File

@@ -37,7 +37,6 @@ class Filter(commands.Cog):
self.config.register_guild(**default_guild_settings)
self.config.register_member(**default_member_settings)
self.config.register_channel(**default_channel_settings)
self.register_task = self.bot.loop.create_task(self.register_filterban())
self.pattern_cache = {}
async def red_delete_data_for_user(
@@ -55,17 +54,27 @@ class Filter(commands.Cog):
if user_id in guild_data:
await self.config.member_from_ids(guild_id, user_id).clear()
def cog_unload(self):
self.register_task.cancel()
async def initialize(self) -> None:
await self.register_casetypes()
@staticmethod
async def register_filterban():
try:
await modlog.register_casetype(
"filterban", False, ":filing_cabinet: :hammer:", "Filter ban"
)
except RuntimeError:
pass
async def register_casetypes() -> None:
await modlog.register_casetypes(
[
{
"name": "filterban",
"default_setting": False,
"image": "\N{FILE CABINET}\N{VARIATION SELECTOR-16} \N{HAMMER}",
"case_str": "Filter ban",
},
{
"name": "filterhit",
"default_setting": False,
"image": "\N{FILE CABINET}\N{VARIATION SELECTOR-16}",
"case_str": "Filter hit",
},
]
)
@commands.group()
@commands.guild_only()
@@ -183,7 +192,7 @@ class Filter(commands.Cog):
except discord.Forbidden:
await ctx.send(_("I can't send direct messages to you."))
@_filter_channel.command("add")
@_filter_channel.command(name="add", require_var_positional=True)
async def filter_channel_add(self, ctx: commands.Context, *words: str):
"""Add words to the filter.
@@ -205,7 +214,7 @@ class Filter(commands.Cog):
else:
await ctx.send(_("Words already in the filter."))
@_filter_channel.command("remove")
@_filter_channel.command(name="delete", aliases=["remove", "del"], require_var_positional=True)
async def filter_channel_remove(self, ctx: commands.Context, *words: str):
"""Remove words from the filter.
@@ -227,7 +236,7 @@ class Filter(commands.Cog):
else:
await ctx.send(_("Those words weren't in the filter."))
@_filter.command(name="add")
@_filter.command(name="add", require_var_positional=True)
async def filter_add(self, ctx: commands.Context, *words: str):
"""Add words to the filter.
@@ -249,7 +258,7 @@ class Filter(commands.Cog):
else:
await ctx.send(_("Those words were already in the filter."))
@_filter.command(name="delete", aliases=["remove", "del"])
@_filter.command(name="delete", aliases=["remove", "del"], require_var_positional=True)
async def filter_remove(self, ctx: commands.Context, *words: str):
"""Remove words from the filter.
@@ -391,6 +400,20 @@ class Filter(commands.Cog):
hits = await self.filter_hits(message.content, message.channel)
if hits:
await modlog.create_case(
bot=self.bot,
guild=guild,
created_at=message.created_at.replace(tzinfo=timezone.utc),
action_type="filterhit",
user=author,
moderator=guild.me,
reason=(
_("Filtered words used: {words}").format(words=humanize_list(list(hits)))
if len(hits) > 1
else _("Filtered word used: {word}").format(word=list(hits)[0])
),
channel=message.channel,
)
try:
await message.delete()
except discord.HTTPException:
@@ -471,7 +494,6 @@ class Filter(commands.Cog):
await set_contextual_locales_from_guild(self.bot, guild)
if await self.filter_hits(member.display_name, member.guild):
name_to_use = guild_data["filter_default_name"]
reason = _("Filtered nickname") if member.nick else _("Filtered name")
try:

View File

@@ -80,7 +80,7 @@ class General(commands.Cog):
""" Nothing to delete """
return
@commands.command()
@commands.command(usage="<choice> <choices...>")
async def choose(self, ctx, *choices):
"""Choose between multiple options.

View File

@@ -270,7 +270,14 @@ class KickBanMixin(MixinMeta):
@commands.bot_has_permissions(kick_members=True)
@checks.admin_or_permissions(kick_members=True)
async def kick(self, ctx: commands.Context, user: discord.Member, *, reason: str = None):
"""Kick a user.
"""
Kick a user.
Examples:
- `[p]kick 428675506947227648 wanted to be kicked.`
This will kick Twentysix from the server.
- `[p]kick @Twentysix wanted to be kicked.`
This will kick Twentysix from the server.
If a reason is specified, it will be the reason that shows up
in the audit log.
@@ -349,11 +356,18 @@ class KickBanMixin(MixinMeta):
):
"""Ban a user from this server and optionally delete days of messages.
`days` is the amount of days of messages to cleanup on ban.
Examples:
- `[p]ban 428675506947227648 7 Continued to spam after told to stop.`
This will ban Twentysix and it will delete 7 days worth of messages.
- `[p]ban @Twentysix 7 Continued to spam after told to stop.`
This will ban Twentysix and it will delete 7 days worth of messages.
A user ID should be provided if the user is not a member of this server.
If days is not a number, it's treated as the first word of the reason.
Minimum 0 days, maximum 7. If not specified, defaultdays setting will be used instead."""
Minimum 0 days, maximum 7. If not specified, the defaultdays setting will be used instead.
"""
guild = ctx.guild
if days is None:
days = await self.config.guild(guild).default_days()
@@ -380,8 +394,15 @@ class KickBanMixin(MixinMeta):
):
"""Mass bans user(s) from the server.
`days` is the amount of days of messages to cleanup on massban.
Example:
- `[p]massban 345628097929936898 57287406247743488 7 they broke all rules.`
This will ban all the added userids and delete 7 days of worth messages.
User IDs need to be provided in order to ban
using this command."""
using this command.
"""
banned = []
errors = {}
upgrades = []
@@ -543,7 +564,19 @@ class KickBanMixin(MixinMeta):
*,
reason: str = None,
):
"""Temporarily ban a user from this server."""
"""Temporarily ban a user from this server.
`duration` is the amount of time the user should be banned for.
`days` is the amount of days of messages to cleanup on tempban.
Examples:
- `[p]tempban @Twentysix Because I say so`
This will ban Twentysix for the default amount of time set by an administrator.
- `[p]tempban @Twentysix 15m You need a timeout`
This will ban Twentysix for 15 minutes.
- `[p]tempban 428675506947227648 1d2h15m 5 Evil person`
This will ban the user for 1 day 2 hours 15 minutes and will delete the last 5 days of their messages.
"""
guild = ctx.guild
author = ctx.author

View File

@@ -10,4 +10,4 @@ async def is_allowed_by_hierarchy(
if not await config.guild(guild).respect_hierarchy():
return True
is_special = mod == guild.owner or await bot.is_owner(mod)
return mod.top_role.position > user.top_role.position or is_special
return mod.top_role > user.top_role or is_special

View File

@@ -7,7 +7,7 @@ import discord
from redbot.core import checks, commands, modlog
from redbot.core.bot import Red
from redbot.core.i18n import Translator, cog_i18n
from redbot.core.utils.chat_formatting import box
from redbot.core.utils.chat_formatting import box, pagify
from redbot.core.utils.menus import DEFAULT_CONTROLS, menu
_ = Translator("ModLog", __file__)
@@ -129,39 +129,76 @@ class ModLog(commands.Cog):
@commands.guild_only()
async def casesfor(self, ctx: commands.Context, *, member: Union[discord.Member, int]):
"""Display cases for the specified member."""
try:
if isinstance(member, int):
cases = await modlog.get_cases_for_member(
bot=ctx.bot, guild=ctx.guild, member_id=member
async with ctx.typing():
try:
if isinstance(member, int):
cases = await modlog.get_cases_for_member(
bot=ctx.bot, guild=ctx.guild, member_id=member
)
else:
cases = await modlog.get_cases_for_member(
bot=ctx.bot, guild=ctx.guild, member=member
)
except discord.NotFound:
return await ctx.send(_("That user does not exist."))
except discord.HTTPException:
return await ctx.send(
_("Something unexpected went wrong while fetching that user by ID.")
)
else:
cases = await modlog.get_cases_for_member(
bot=ctx.bot, guild=ctx.guild, member=member
if not cases:
return await ctx.send(_("That user does not have any cases."))
embed_requested = await ctx.embed_requested()
if embed_requested:
rendered_cases = [await case.message_content(embed=True) for case in cases]
elif not embed_requested:
rendered_cases = []
for case in cases:
message = _("{case}\n**Timestamp:** {timestamp}").format(
case=await case.message_content(embed=False),
timestamp=datetime.utcfromtimestamp(case.created_at).strftime(
"%Y-%m-%d %H:%M:%S UTC"
),
)
rendered_cases.append(message)
await menu(ctx, rendered_cases, DEFAULT_CONTROLS)
@commands.command()
@commands.guild_only()
async def listcases(self, ctx: commands.Context, *, member: Union[discord.Member, int]):
"""List cases for the specified member."""
async with ctx.typing():
try:
if isinstance(member, int):
cases = await modlog.get_cases_for_member(
bot=ctx.bot, guild=ctx.guild, member_id=member
)
else:
cases = await modlog.get_cases_for_member(
bot=ctx.bot, guild=ctx.guild, member=member
)
except discord.NotFound:
return await ctx.send(_("That user does not exist."))
except discord.HTTPException:
return await ctx.send(
_("Something unexpected went wrong while fetching that user by ID.")
)
except discord.NotFound:
return await ctx.send(_("That user does not exist."))
except discord.HTTPException:
return await ctx.send(
_("Something unexpected went wrong while fetching that user by ID.")
)
if not cases:
return await ctx.send(_("That user does not have any cases."))
if not cases:
return await ctx.send(_("That user does not have any cases."))
embed_requested = await ctx.embed_requested()
if embed_requested:
rendered_cases = [await case.message_content(embed=True) for case in cases]
elif not embed_requested:
rendered_cases = []
message = ""
for case in cases:
message = _("{case}\n**Timestamp:** {timestamp}").format(
message += _("{case}\n**Timestamp:** {timestamp}\n\n").format(
case=await case.message_content(embed=False),
timestamp=datetime.utcfromtimestamp(case.created_at).strftime(
"%Y-%m-%d %H:%M:%S UTC"
),
)
rendered_cases.append(message)
for page in pagify(message, ["\n\n", "\n"], priority=True):
rendered_cases.append(page)
await menu(ctx, rendered_cases, DEFAULT_CONTROLS)
@commands.command()

View File

@@ -1,5 +1,5 @@
from abc import ABC, abstractmethod
from typing import List, Tuple, Optional, Dict
from typing import List, Tuple, Optional, Dict, Union
from datetime import datetime
import discord
@@ -25,3 +25,15 @@ class MixinMeta(ABC):
ctx: commands.Context, user_voice_state: Optional[discord.VoiceState], **perms: bool
) -> bool:
raise NotImplementedError()
@abstractmethod
async def _send_dm_notification(
self,
user: Union[discord.User, discord.Member],
moderator: Optional[Union[discord.User, discord.Member]],
guild: discord.Guild,
mute_type: str,
reason: Optional[str],
duration=None,
):
raise NotImplementedError()

View File

@@ -51,5 +51,5 @@ class MuteTime(Converter):
time_data[k] = int(v)
if time_data:
result["duration"] = timedelta(**time_data)
result["reason"] = argument
result["reason"] = argument.strip()
return result

View File

@@ -13,7 +13,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 bounded_gather
from redbot.core.utils.chat_formatting import humanize_timedelta, humanize_list, pagify
from redbot.core.utils.chat_formatting import bold, humanize_timedelta, humanize_list, pagify
from redbot.core.utils.mod import get_audit_reason
from redbot.core.utils.menus import start_adding_reactions
from redbot.core.utils.predicates import MessagePredicate, ReactionPredicate
@@ -28,6 +28,9 @@ MUTE_UNMUTE_ISSUES = {
"hierarchy_problem": _(
"I cannot let you do that. You are not higher than the user in the role hierarchy."
),
"assigned_role_hierarchy_problem": _(
"I cannot let you do that. You are not higher than the mute role in the role hierarchy."
),
"is_admin": _("That user cannot be muted, as they have the Administrator permission."),
"permissions_issue_role": _(
"Failed to mute or unmute user. I need the Manage Roles "
@@ -74,6 +77,8 @@ class Mutes(VoiceMutes, commands.Cog, metaclass=CompositeMetaClass):
"notification_channel": None,
"muted_users": {},
"default_time": 0,
"dm": False,
"show_mod": False,
}
self.config.register_global(force_role_mutes=True)
# Tbh I would rather force everyone to use role mutes.
@@ -145,7 +150,7 @@ class Mutes(VoiceMutes, commands.Cog, metaclass=CompositeMetaClass):
self, guild: discord.Guild, mod: discord.Member, user: discord.Member
):
is_special = mod == guild.owner or await self.bot.is_owner(mod)
return mod.top_role.position > user.top_role.position or is_special
return mod.top_role > user.top_role or is_special
async def _handle_automatic_unmute(self):
"""This is the core task creator and loop
@@ -253,6 +258,9 @@ class Mutes(VoiceMutes, commands.Cog, metaclass=CompositeMetaClass):
_("Automatic unmute"),
until=None,
)
await self._send_dm_notification(
member, author, guild, _("Server unmute"), _("Automatic unmute")
)
else:
chan_id = await self.config.guild(guild).notification_channel()
notification_channel = guild.get_channel(chan_id)
@@ -360,6 +368,9 @@ class Mutes(VoiceMutes, commands.Cog, metaclass=CompositeMetaClass):
modlog_reason,
until=None,
)
await self._send_dm_notification(
member, author, guild, _("Server unmute"), _("Automatic unmute")
)
self._channel_mute_events[guild.id].set()
if any(results):
reasons = {}
@@ -421,8 +432,10 @@ class Mutes(VoiceMutes, commands.Cog, metaclass=CompositeMetaClass):
if create_case:
if isinstance(channel, discord.VoiceChannel):
unmute_type = "vunmute"
notification_title = _("Voice unmute")
else:
unmute_type = "cunmute"
notification_title = _("Channel unmute")
await modlog.create_case(
self.bot,
channel.guild,
@@ -434,6 +447,9 @@ class Mutes(VoiceMutes, commands.Cog, metaclass=CompositeMetaClass):
until=None,
channel=channel,
)
await self._send_dm_notification(
member, author, channel.guild, notification_title, _("Automatic unmute")
)
return None
else:
error_msg = _(
@@ -454,6 +470,72 @@ class Mutes(VoiceMutes, commands.Cog, metaclass=CompositeMetaClass):
else:
return (member, channel, success["reason"])
async def _send_dm_notification(
self,
user: Union[discord.User, discord.Member],
moderator: Optional[Union[discord.User, discord.Member]],
guild: discord.Guild,
mute_type: str,
reason: Optional[str],
duration=None,
):
if not await self.config.guild(guild).dm():
return
show_mod = await self.config.guild(guild).show_mod()
title = bold(mute_type)
if duration:
duration_str = humanize_timedelta(timedelta=duration)
until = datetime.now(timezone.utc) + duration
until_str = until.strftime("%Y-%m-%d %H:%M:%S UTC")
if moderator is None:
moderator_str = _("Unknown")
else:
moderator_str = str(moderator)
if not reason:
reason = _("No reason provided.")
# okay, this is some poor API to require PrivateChannel here...
if await self.bot.embed_requested(await user.create_dm(), user):
em = discord.Embed(
title=title,
description=reason,
color=await self.bot.get_embed_color(user),
)
em.timestamp = datetime.utcnow()
if duration:
em.add_field(name=_("Until"), value=until_str)
em.add_field(name=_("Duration"), value=duration_str)
em.add_field(name=_("Guild"), value=guild.name, inline=False)
if show_mod:
em.add_field(name=_("Moderator"), value=moderator_str)
try:
await user.send(embed=em)
except discord.Forbidden:
pass
else:
message = f"{title}\n>>> "
message += reason
message += (
_("\n**Moderator**: {moderator}").format(moderator=moderator_str)
if show_mod
else ""
)
message += (
_("\n**Until**: {until}\n**Duration**: {duration}").format(
until=until_str, duration=duration_str
)
if duration
else ""
)
message += _("\n**Guild**: {guild_name}").format(guild_name=guild.name)
try:
await user.send(message)
except discord.Forbidden:
pass
@commands.Cog.listener()
async def on_member_update(self, before: discord.Member, after: discord.Member):
"""
@@ -491,6 +573,9 @@ class Mutes(VoiceMutes, commands.Cog, metaclass=CompositeMetaClass):
)
del self._server_mutes[guild.id][after.id]
should_save = True
await self._send_dm_notification(
after, None, guild, _("Server unmute"), _("Manually removed mute role")
)
elif mute_role in roles_added:
# send modlog case for mute and add to cache
if guild.id not in self._server_mutes:
@@ -512,6 +597,9 @@ class Mutes(VoiceMutes, commands.Cog, metaclass=CompositeMetaClass):
"until": None,
}
should_save = True
await self._send_dm_notification(
after, None, guild, _("Server mute"), _("Manually applied mute role")
)
if should_save:
await self.config.guild(guild).muted_users.set(self._server_mutes[guild.id])
@@ -552,15 +640,27 @@ class Mutes(VoiceMutes, commands.Cog, metaclass=CompositeMetaClass):
user_id not in after_perms or any((send_messages, speak))
):
user = after.guild.get_member(user_id)
send_dm_notification = True
if not user:
send_dm_notification = False
user = discord.Object(id=user_id)
log.debug(f"{user} - {type(user)}")
to_del.append(user_id)
log.debug("creating case")
if isinstance(after, discord.VoiceChannel):
unmute_type = "vunmute"
notification_title = _("Voice unmute")
else:
unmute_type = "cunmute"
notification_title = _("Channel unmute")
if send_dm_notification:
await self._send_dm_notification(
user,
None,
after.guild,
notification_title,
_("Manually removed channel overwrites"),
)
await modlog.create_case(
self.bot,
after.guild,
@@ -611,6 +711,34 @@ class Mutes(VoiceMutes, commands.Cog, metaclass=CompositeMetaClass):
"""Mute settings."""
pass
@muteset.command()
@commands.guild_only()
async def senddm(self, ctx: commands.Context, true_or_false: bool):
"""Set whether mute notifications should be sent to users in DMs."""
await self.config.guild(ctx.guild).dm.set(true_or_false)
if true_or_false:
await ctx.send(_("I will now try to send mute notifications to users DMs."))
else:
await ctx.send(_("Mute notifications will no longer be sent to users DMs."))
@muteset.command()
@commands.guild_only()
async def showmoderator(self, ctx, true_or_false: bool):
"""Decide whether the name of the moderator muting a user should be included in the DM to that user."""
await self.config.guild(ctx.guild).show_mod.set(true_or_false)
if true_or_false:
await ctx.send(
_(
"I will include the name of the moderator who issued the mute when sending a DM to a user."
)
)
else:
await ctx.send(
_(
"I will not include the name of the moderator who issued the mute when sending a DM to a user."
)
)
@muteset.command(name="forcerole")
@commands.is_owner()
async def force_role_mutes(self, ctx: commands.Context, force_role_mutes: bool):
@@ -635,11 +763,17 @@ class Mutes(VoiceMutes, commands.Cog, metaclass=CompositeMetaClass):
notification_channel = ctx.guild.get_channel(data["notification_channel"])
default_time = timedelta(seconds=data["default_time"])
msg = _(
"Mute Role: {role}\nNotification Channel: {channel}\n" "Default Time: {time}"
"Mute Role: {role}\n"
"Notification Channel: {channel}\n"
"Default Time: {time}\n"
"Send DM: {dm}\n"
"Show moderator: {show_mod}"
).format(
role=mute_role.mention if mute_role else _("None"),
channel=notification_channel.mention if notification_channel else _("None"),
time=humanize_timedelta(timedelta=default_time) if default_time else _("None"),
dm=data["dm"],
show_mod=data["show_mod"],
)
await ctx.maybe_send_embed(msg)
@@ -684,6 +818,11 @@ class Mutes(VoiceMutes, commands.Cog, metaclass=CompositeMetaClass):
# removed the mute role
await ctx.send(_("Channel overwrites will be used for mutes instead."))
else:
if role >= ctx.author.top_role:
await ctx.send(
_("You can't set this role as it is not lower than you in the role hierarchy.")
)
return
await self.config.guild(ctx.guild).mute_role.set(role.id)
self.mute_role_cache[ctx.guild.id] = role.id
await ctx.send(_("Mute role set to {role}").format(role=role.name))
@@ -999,6 +1138,9 @@ class Mutes(VoiceMutes, commands.Cog, metaclass=CompositeMetaClass):
until=until,
channel=None,
)
await self._send_dm_notification(
user, author, guild, _("Server mute"), reason, duration
)
else:
issue_list.append(success)
if success_list:
@@ -1143,6 +1285,9 @@ class Mutes(VoiceMutes, commands.Cog, metaclass=CompositeMetaClass):
until=until,
channel=channel,
)
await self._send_dm_notification(
user, author, guild, _("Channel mute"), reason, duration
)
async with self.config.member(user).perms_cache() as cache:
cache[channel.id] = success["old_overs"]
else:
@@ -1209,6 +1354,9 @@ class Mutes(VoiceMutes, commands.Cog, metaclass=CompositeMetaClass):
reason,
until=None,
)
await self._send_dm_notification(
user, author, guild, _("Server unmute"), reason
)
else:
issue_list.append(success)
self._channel_mute_events[guild.id].set()
@@ -1273,6 +1421,9 @@ class Mutes(VoiceMutes, commands.Cog, metaclass=CompositeMetaClass):
until=None,
channel=channel,
)
await self._send_dm_notification(
user, author, guild, _("Channel unmute"), reason
)
else:
issue_list.append((user, success["reason"]))
if success_list:
@@ -1331,6 +1482,9 @@ class Mutes(VoiceMutes, commands.Cog, metaclass=CompositeMetaClass):
if not role:
ret["reason"] = _(MUTE_UNMUTE_ISSUES["role_missing"])
return ret
if author != guild.owner and role >= author.top_role:
ret["reason"] = _(MUTE_UNMUTE_ISSUES["assigned_role_hierarchy_problem"])
return ret
if not guild.me.guild_permissions.manage_roles or role >= guild.me.top_role:
ret["reason"] = _(MUTE_UNMUTE_ISSUES["permissions_issue_role"])
return ret

View File

@@ -1,10 +1,11 @@
from typing import Optional, Tuple
from typing import Optional, Tuple, Union
from datetime import timezone, timedelta, datetime
from .abc import MixinMeta
import discord
from redbot.core import commands, checks, i18n, modlog
from redbot.core.utils.chat_formatting import (
bold,
humanize_timedelta,
humanize_list,
pagify,
@@ -143,6 +144,9 @@ class VoiceMutes(MixinMeta):
until=until,
channel=channel,
)
await self._send_dm_notification(
user, author, guild, _("Voice mute"), reason, duration
)
async with self.config.member(user).perms_cache() as cache:
cache[channel.id] = success["old_overs"]
else:
@@ -216,6 +220,9 @@ class VoiceMutes(MixinMeta):
until=None,
channel=channel,
)
await self._send_dm_notification(
user, author, guild, _("Voice unmute"), reason
)
else:
issue_list.append((user, success["reason"]))
if success_list:

View File

@@ -4,7 +4,7 @@ import logging
from dateutil.parser import parse as parse_time
from random import choice
from string import ascii_letters
from datetime import datetime, timedelta
from datetime import datetime, timedelta, timezone
import xml.etree.ElementTree as ET
from typing import ClassVar, Optional, List, Tuple
@@ -35,7 +35,7 @@ YOUTUBE_CHANNEL_RSS = "https://www.youtube.com/feeds/videos.xml?channel_id={chan
_ = Translator("Streams", __file__)
log = logging.getLogger("redbot.cogs.Streams")
log = logging.getLogger("red.core.cogs.Streams")
def rnd(url):
@@ -138,9 +138,7 @@ class YoutubeStream(Stream):
scheduled = stream_data.get("scheduledStartTime", None)
if scheduled is not None and actual_start_time is None:
scheduled = parse_time(scheduled)
if (
scheduled.replace(tzinfo=None) - datetime.now()
).total_seconds() < -3600:
if (scheduled - datetime.now(timezone.utc)).total_seconds() < -3600:
continue
elif actual_start_time is None:
continue
@@ -178,7 +176,7 @@ class YoutubeStream(Stream):
if vid_data["liveStreamingDetails"].get("scheduledStartTime", None) is not None:
if "actualStartTime" not in vid_data["liveStreamingDetails"]:
start_time = parse_time(vid_data["liveStreamingDetails"]["scheduledStartTime"])
start_in = start_time.replace(tzinfo=None) - datetime.now()
start_in = start_time - datetime.now(timezone.utc)
if start_in.total_seconds() > 0:
embed.description = _("This stream will start in {time}").format(
time=humanize_timedelta(

View File

@@ -110,6 +110,7 @@ class RedBase(
help__delete_delay=0,
help__use_menus=False,
help__show_hidden=False,
help__show_aliases=True,
help__verify_checks=True,
help__verify_exists=False,
help__tagline="",
@@ -281,6 +282,92 @@ class RedBase(
"""
self._help_formatter = commands.help.RedHelpFormatter()
def add_dev_env_value(self, name: str, value: Callable[[commands.Context], Any]):
"""
Add a custom variable to the dev environment (``[p]debug``, ``[p]eval``, and ``[p]repl`` commands).
If dev mode is disabled, nothing will happen.
Example
-------
.. code-block:: python
class MyCog(commands.Cog):
def __init__(self, bot):
self.bot = bot
bot.add_dev_env_value("mycog", lambda ctx: self)
bot.add_dev_env_value("mycogdata", lambda ctx: self.settings[ctx.guild.id])
def cog_unload(self):
self.bot.remove_dev_env_value("mycog")
self.bot.remove_dev_env_value("mycogdata")
Once your cog is loaded, the custom variables ``mycog`` and ``mycogdata``
will be included in the environment of dev commands.
Parameters
----------
name: str
The name of your custom variable.
value: Callable[[commands.Context], Any]
The function returning the value of the variable.
It must take a `commands.Context` as its sole parameter
Raises
------
TypeError
``value`` argument isn't a callable.
ValueError
The passed callable takes no or more than one argument.
RuntimeError
The name of the custom variable is either reserved by a variable
from the default environment or already taken by some other custom variable.
"""
signature = inspect.signature(value)
if len(signature.parameters) != 1:
raise ValueError("Callable must take exactly one argument for context")
dev = self.get_cog("Dev")
if dev is None:
return
if name in [
"bot",
"ctx",
"channel",
"author",
"guild",
"message",
"asyncio",
"aiohttp",
"discord",
"commands",
"_",
"__name__",
"__builtins__",
]:
raise RuntimeError(f"The name {name} is reserved for default environement.")
if name in dev.env_extensions:
raise RuntimeError(f"The name {name} is already used.")
dev.env_extensions[name] = value
def remove_dev_env_value(self, name: str):
"""
Remove a custom variable from the dev environment.
Parameters
----------
name: str
The name of the custom variable.
Raises
------
KeyError
The custom variable was never set.
"""
dev = self.get_cog("Dev")
if dev is None:
return
del dev.env_extensions[name]
def get_command(self, name: str) -> Optional[commands.Command]:
com = super().get_command(name)
assert com is None or isinstance(com, commands.Command)
@@ -383,6 +470,12 @@ class RedBase(
self._red_before_invoke_objs.add(coro)
return coro
async def before_identify_hook(self, shard_id, *, initial=False):
"""A hook that is called before IDENTIFYing a session.
Same as in discord.py, but also dispatches "on_red_identify" bot event."""
self.dispatch("red_before_identify", shard_id, initial)
return await super().before_identify_hook(shard_id, initial=initial)
@property
def cog_mgr(self) -> NoReturn:
raise AttributeError("Please don't mess with the cog manager internals.")

View File

@@ -42,7 +42,7 @@ from ..i18n import Translator
from ..utils import menus
from ..utils.mod import mass_purge
from ..utils._internal_utils import fuzzy_command_search, format_fuzzy_results
from ..utils.chat_formatting import box, pagify, humanize_timedelta
from ..utils.chat_formatting import box, humanize_list, humanize_number, humanize_timedelta, pagify
__all__ = ["red_help", "RedHelpFormatter", "HelpSettings", "HelpFormatterABC"]
@@ -72,6 +72,7 @@ class HelpSettings:
max_pages_in_guild: int = 2
use_menus: bool = False
show_hidden: bool = False
show_aliases: bool = True
verify_checks: bool = True
verify_exists: bool = False
tagline: str = ""
@@ -300,10 +301,42 @@ class RedHelpFormatter(HelpFormatterABC):
tagline = (help_settings.tagline) or self.get_default_tagline(ctx)
signature = _(
"`Syntax: {ctx.clean_prefix}{command.qualified_name} {command.signature}`"
"Syntax: {ctx.clean_prefix}{command.qualified_name} {command.signature}"
).format(ctx=ctx, command=command)
subcommands = None
aliases = command.aliases
if help_settings.show_aliases and aliases:
alias_fmt = _("Aliases") if len(command.aliases) > 1 else _("Alias")
aliases = sorted(aliases, key=len)
a_counter = 0
valid_alias_list = []
for alias in aliases:
if (a_counter := a_counter + len(alias)) < 500:
valid_alias_list.append(alias)
else:
break
a_diff = len(aliases) - len(valid_alias_list)
aliases_list = [
f"{ctx.clean_prefix}{command.parent.qualified_name + ' ' if command.parent else ''}{alias}"
for alias in valid_alias_list
]
if len(valid_alias_list) < 10:
aliases_content = humanize_list(aliases_list)
else:
aliases_formatted_list = ", ".join(aliases_list)
if a_diff > 1:
aliases_content = _("{aliases} and {number} more aliases.").format(
aliases=aliases_formatted_list, number=humanize_number(a_diff)
)
else:
aliases_content = _("{aliases} and one more alias.").format(
aliases=aliases_formatted_list
)
signature += f"\n{alias_fmt}: {aliases_content}"
subcommands = None
if hasattr(command, "all_commands"):
grp = cast(commands.Group, command)
subcommands = await self.get_group_help_mapping(ctx, grp, help_settings=help_settings)
@@ -315,7 +348,7 @@ class RedHelpFormatter(HelpFormatterABC):
emb["embed"]["title"] = f"*{description[:250]}*"
emb["footer"]["text"] = tagline
emb["embed"]["description"] = signature
emb["embed"]["description"] = box(signature)
command_help = command.format_help_for_context(ctx)
if command_help:
@@ -375,7 +408,7 @@ class RedHelpFormatter(HelpFormatterABC):
None,
(
description,
signature[1:-1],
signature,
command.format_help_for_context(ctx),
subtext_header,
subtext,

View File

@@ -172,6 +172,20 @@ class CoreLogic:
except errors.CogLoadError as e:
failed_with_reason_packages.append((name, str(e)))
except Exception as e:
if isinstance(e, commands.CommandRegistrationError):
if e.alias_conflict:
error_message = _(
"Alias {alias_name} is already an existing command"
" or alias in one of the loaded cogs."
).format(alias_name=inline(e.name))
else:
error_message = _(
"Command {command_name} is already an existing command"
" or alias in one of the loaded cogs."
).format(command_name=inline(e.name))
failed_with_reason_packages.append((name, error_message))
continue
log.exception("Package loading failed", exc_info=e)
exception_log = "Exception during loading of package\n"
@@ -2094,7 +2108,7 @@ class Core(commands.commands._RuleDropper, commands.Cog, CoreLogic):
if game:
if len(game) > 128:
await ctx.send("The maximum length of game descriptions is 128 characters.")
await ctx.send(_("The maximum length of game descriptions is 128 characters."))
return
game = discord.Game(name=game)
else:
@@ -2126,6 +2140,11 @@ class Core(commands.commands._RuleDropper, commands.Cog, CoreLogic):
status = ctx.bot.guilds[0].me.status if len(ctx.bot.guilds) > 0 else discord.Status.online
if listening:
if len(listening) > 128:
await ctx.send(
_("The maximum length of listening descriptions is 128 characters.")
)
return
activity = discord.Activity(name=listening, type=discord.ActivityType.listening)
else:
activity = None
@@ -2157,6 +2176,9 @@ class Core(commands.commands._RuleDropper, commands.Cog, CoreLogic):
status = ctx.bot.guilds[0].me.status if len(ctx.bot.guilds) > 0 else discord.Status.online
if watching:
if len(watching) > 128:
await ctx.send(_("The maximum length of watching descriptions is 128 characters."))
return
activity = discord.Activity(name=watching, type=discord.ActivityType.watching)
else:
activity = None
@@ -2186,6 +2208,11 @@ class Core(commands.commands._RuleDropper, commands.Cog, CoreLogic):
status = ctx.bot.guilds[0].me.status if len(ctx.bot.guilds) > 0 else discord.Status.online
if competing:
if len(competing) > 128:
await ctx.send(
_("The maximum length of competing descriptions is 128 characters.")
)
return
activity = discord.Activity(name=competing, type=discord.ActivityType.competing)
else:
activity = None
@@ -2234,11 +2261,13 @@ class Core(commands.commands._RuleDropper, commands.Cog, CoreLogic):
await ctx.bot.change_presence(status=status, activity=game)
await ctx.send(_("Status changed to {}.").format(status))
@_set.command(name="streaming", aliases=["stream"], usage="[(<streamer> <stream_title>)]")
@_set.command(
name="streaming", aliases=["stream", "twitch"], usage="[(<streamer> <stream_title>)]"
)
@checks.bot_in_a_guild()
@checks.is_owner()
async def stream(self, ctx: commands.Context, streamer=None, *, stream_title=None):
"""Sets [botname]'s streaming status.
"""Sets [botname]'s streaming status to a twitch stream.
This will appear as `Streaming <stream_title>` or `LIVE ON TWITCH` depending on the context.
It will also include a `Watch` button with a twitch.tv url for the provided streamer.
@@ -2262,6 +2291,12 @@ class Core(commands.commands._RuleDropper, commands.Cog, CoreLogic):
stream_title = stream_title.strip()
if "twitch.tv/" not in streamer:
streamer = "https://www.twitch.tv/" + streamer
if len(streamer) > 511:
await ctx.send(_("The maximum length of the streamer url is 511 characters."))
return
if len(stream_title) > 128:
await ctx.send(_("The maximum length of the stream title is 128 characters."))
return
activity = discord.Streaming(url=streamer, name=stream_title)
await ctx.bot.change_presence(status=status, activity=activity)
elif streamer is not None:
@@ -2782,6 +2817,22 @@ class Core(commands.commands._RuleDropper, commands.Cog, CoreLogic):
else:
await ctx.send(_("Help will filter hidden commands."))
@helpset.command(name="showaliases")
async def helpset_showaliases(self, ctx: commands.Context, show_aliases: bool = None):
"""
This allows the help command to show existing commands aliases if there is any.
This defaults to True.
Using this without a setting will toggle.
"""
if show_aliases is None:
show_aliases = not await ctx.bot._config.help.show_aliases()
await ctx.bot._config.help.show_aliases.set(show_aliases)
if show_aliases:
await ctx.send(_("Help will show commands aliases."))
else:
await ctx.send(_("Help will not show commands aliases."))
@helpset.command(name="usetick")
async def helpset_usetick(self, ctx: commands.Context, use_tick: bool = None):
"""
@@ -3275,8 +3326,10 @@ class Core(commands.commands._RuleDropper, commands.Cog, CoreLogic):
"""
uids = {getattr(user, "id", user) for user in users}
await self.bot._whiteblacklist_cache.add_to_whitelist(None, uids)
await ctx.send(_("Users added to allowlist."))
if len(users) > 1:
await ctx.send(_("Users have been added to the allowlist."))
else:
await ctx.send(_("User has been added to the allowlist."))
@allowlist.command(name="list")
async def allowlist_list(self, ctx: commands.Context):
@@ -3291,8 +3344,10 @@ class Core(commands.commands._RuleDropper, commands.Cog, CoreLogic):
if not curr_list:
await ctx.send("Allowlist is empty.")
return
msg = _("Users on allowlist:")
if len(curr_list) > 1:
msg = _("Users on the allowlist:")
else:
msg = _("User on the allowlist:")
for user in curr_list:
msg += "\n\t- {}".format(user)
@@ -3316,8 +3371,10 @@ class Core(commands.commands._RuleDropper, commands.Cog, CoreLogic):
"""
uids = {getattr(user, "id", user) for user in users}
await self.bot._whiteblacklist_cache.remove_from_whitelist(None, uids)
await ctx.send(_("Users have been removed from the allowlist."))
if len(users) > 1:
await ctx.send(_("Users have been removed from the allowlist."))
else:
await ctx.send(_("User has been removed from the allowlist."))
@allowlist.command(name="clear")
async def allowlist_clear(self, ctx: commands.Context):
@@ -3366,8 +3423,10 @@ class Core(commands.commands._RuleDropper, commands.Cog, CoreLogic):
uids = {getattr(user, "id", user) for user in users}
await self.bot._whiteblacklist_cache.add_to_blacklist(None, uids)
await ctx.send(_("User added to blocklist."))
if len(users) > 1:
await ctx.send(_("Users have been added to the blocklist."))
else:
await ctx.send(_("User has been added to the blocklist."))
@blocklist.command(name="list")
async def blocklist_list(self, ctx: commands.Context):
@@ -3382,8 +3441,10 @@ class Core(commands.commands._RuleDropper, commands.Cog, CoreLogic):
if not curr_list:
await ctx.send("Blocklist is empty.")
return
msg = _("Users on blocklist:")
if len(curr_list) > 1:
msg = _("Users on the blocklist:")
else:
msg = _("User on the blocklist:")
for user in curr_list:
msg += "\n\t- {}".format(user)
@@ -3405,8 +3466,10 @@ class Core(commands.commands._RuleDropper, commands.Cog, CoreLogic):
"""
uids = {getattr(user, "id", user) for user in users}
await self.bot._whiteblacklist_cache.remove_from_blacklist(None, uids)
await ctx.send(_("Users have been removed from blocklist."))
if len(users) > 1:
await ctx.send(_("Users have been removed from the blocklist."))
else:
await ctx.send(_("User has been removed from the blocklist."))
@blocklist.command(name="clear")
async def blocklist_clear(self, ctx: commands.Context):
@@ -3468,7 +3531,7 @@ class Core(commands.commands._RuleDropper, commands.Cog, CoreLogic):
@localallowlist.command(name="list")
async def localallowlist_list(self, ctx: commands.Context):
"""
Lists users and roles on the server allowlist.
Lists users and roles on the server allowlist.
Example:
- `[p]localallowlist list`
@@ -3478,8 +3541,10 @@ class Core(commands.commands._RuleDropper, commands.Cog, CoreLogic):
if not curr_list:
await ctx.send("Server allowlist is empty.")
return
msg = _("Whitelisted Users and roles:")
if len(curr_list) > 1:
msg = _("Allowed users and/or roles:")
else:
msg = _("Allowed user or role:")
for obj in curr_list:
msg += "\n\t- {}".format(obj)
@@ -3593,8 +3658,10 @@ class Core(commands.commands._RuleDropper, commands.Cog, CoreLogic):
if not curr_list:
await ctx.send("Server blocklist is empty.")
return
msg = _("Blacklisted Users and Roles:")
if len(curr_list) > 1:
msg = _("Blocked users and/or roles:")
else:
msg = _("Blocked user or role:")
for obj in curr_list:
msg += "\n\t- {}".format(obj)

View File

@@ -45,6 +45,7 @@ class Dev(commands.Cog):
super().__init__()
self._last_result = None
self.sessions = {}
self.env_extensions = {}
@staticmethod
def async_compile(source, filename, mode):
@@ -92,6 +93,29 @@ class Dev(commands.Cog):
token = ctx.bot.http.token
return re.sub(re.escape(token), "[EXPUNGED]", input_, re.I)
def get_environment(self, ctx: commands.Context) -> dict:
env = {
"bot": ctx.bot,
"ctx": ctx,
"channel": ctx.channel,
"author": ctx.author,
"guild": ctx.guild,
"message": ctx.message,
"asyncio": asyncio,
"aiohttp": aiohttp,
"discord": discord,
"commands": commands,
"_": self._last_result,
"__name__": "__main__",
}
for name, value in self.env_extensions.items():
try:
env[name] = value(ctx)
except Exception as e:
traceback.clear_frames(e.__traceback__)
env[name] = e
return env
@commands.command()
@checks.is_owner()
async def debug(self, ctx, *, code):
@@ -115,21 +139,7 @@ class Dev(commands.Cog):
commands - redbot.core.commands
_ - The result of the last dev command.
"""
env = {
"bot": ctx.bot,
"ctx": ctx,
"channel": ctx.channel,
"author": ctx.author,
"guild": ctx.guild,
"message": ctx.message,
"asyncio": asyncio,
"aiohttp": aiohttp,
"discord": discord,
"commands": commands,
"_": self._last_result,
"__name__": "__main__",
}
env = self.get_environment(ctx)
code = self.cleanup_code(code)
try:
@@ -169,21 +179,7 @@ class Dev(commands.Cog):
commands - redbot.core.commands
_ - The result of the last dev command.
"""
env = {
"bot": ctx.bot,
"ctx": ctx,
"channel": ctx.channel,
"author": ctx.author,
"guild": ctx.guild,
"message": ctx.message,
"asyncio": asyncio,
"aiohttp": aiohttp,
"discord": discord,
"commands": commands,
"_": self._last_result,
"__name__": "__main__",
}
env = self.get_environment(ctx)
body = self.cleanup_code(body)
stdout = io.StringIO()
@@ -224,19 +220,6 @@ class Dev(commands.Cog):
backtick. This includes codeblocks, and as such multiple lines can be
evaluated.
"""
variables = {
"ctx": ctx,
"bot": ctx.bot,
"message": ctx.message,
"guild": ctx.guild,
"channel": ctx.channel,
"author": ctx.author,
"asyncio": asyncio,
"_": None,
"__builtins__": __builtins__,
"__name__": "__main__",
}
if ctx.channel.id in self.sessions:
if self.sessions[ctx.channel.id]:
await ctx.send(
@@ -250,6 +233,9 @@ class Dev(commands.Cog):
)
return
env = self.get_environment(ctx)
env["__builtins__"] = __builtins__
env["_"] = None
self.sessions[ctx.channel.id] = True
await ctx.send(
_(
@@ -287,8 +273,7 @@ class Dev(commands.Cog):
await ctx.send(self.get_syntax_error(e))
continue
variables["message"] = response
env["message"] = response
stdout = io.StringIO()
msg = ""
@@ -296,9 +281,9 @@ class Dev(commands.Cog):
try:
with redirect_stdout(stdout):
if executor is None:
result = types.FunctionType(code, variables)()
result = types.FunctionType(code, env)()
else:
result = executor(code, variables)
result = executor(code, env)
result = await self.maybe_await(result)
except:
value = stdout.getvalue()
@@ -307,7 +292,7 @@ class Dev(commands.Cog):
value = stdout.getvalue()
if result is not None:
msg = "{}{}".format(value, result)
variables["_"] = result
env["_"] = result
elif value:
msg = "{}".format(value)

View File

@@ -108,7 +108,7 @@ async def is_allowed_by_hierarchy(
if not await settings.guild(guild).respect_hierarchy():
return True
is_special = mod == guild.owner or await bot.is_owner(mod)
return mod.top_role.position > user.top_role.position or is_special
return mod.top_role > user.top_role or is_special
async def is_mod_or_superior(

View File

@@ -67,7 +67,7 @@ class MessagePredicate(Callable[[discord.Message], bool]):
def same_context(
cls,
ctx: Optional[commands.Context] = None,
channel: Optional[discord.TextChannel] = None,
channel: Optional[Union[discord.TextChannel, discord.DMChannel]] = None,
user: Optional[discord.abc.User] = None,
) -> "MessagePredicate":
"""Match if the message fits the described context.
@@ -104,7 +104,7 @@ class MessagePredicate(Callable[[discord.Message], bool]):
def cancelled(
cls,
ctx: Optional[commands.Context] = None,
channel: Optional[discord.TextChannel] = None,
channel: Optional[Union[discord.TextChannel, discord.DMChannel]] = None,
user: Optional[discord.abc.User] = None,
) -> "MessagePredicate":
"""Match if the message is ``[p]cancel``.
@@ -133,7 +133,7 @@ class MessagePredicate(Callable[[discord.Message], bool]):
def yes_or_no(
cls,
ctx: Optional[commands.Context] = None,
channel: Optional[discord.TextChannel] = None,
channel: Optional[Union[discord.TextChannel, discord.DMChannel]] = None,
user: Optional[discord.abc.User] = None,
) -> "MessagePredicate":
"""Match if the message is "yes"/"y" or "no"/"n".
@@ -176,7 +176,7 @@ class MessagePredicate(Callable[[discord.Message], bool]):
def valid_int(
cls,
ctx: Optional[commands.Context] = None,
channel: Optional[discord.TextChannel] = None,
channel: Optional[Union[discord.TextChannel, discord.DMChannel]] = None,
user: Optional[discord.abc.User] = None,
) -> "MessagePredicate":
"""Match if the response is an integer.
@@ -216,7 +216,7 @@ class MessagePredicate(Callable[[discord.Message], bool]):
def valid_float(
cls,
ctx: Optional[commands.Context] = None,
channel: Optional[discord.TextChannel] = None,
channel: Optional[Union[discord.TextChannel, discord.DMChannel]] = None,
user: Optional[discord.abc.User] = None,
) -> "MessagePredicate":
"""Match if the response is a float.
@@ -256,7 +256,7 @@ class MessagePredicate(Callable[[discord.Message], bool]):
def positive(
cls,
ctx: Optional[commands.Context] = None,
channel: Optional[discord.TextChannel] = None,
channel: Optional[Union[discord.TextChannel, discord.DMChannel]] = None,
user: Optional[discord.abc.User] = None,
) -> "MessagePredicate":
"""Match if the response is a positive number.
@@ -492,7 +492,7 @@ class MessagePredicate(Callable[[discord.Message], bool]):
cls,
value: str,
ctx: Optional[commands.Context] = None,
channel: Optional[discord.TextChannel] = None,
channel: Optional[Union[discord.TextChannel, discord.DMChannel]] = None,
user: Optional[discord.abc.User] = None,
) -> "MessagePredicate":
"""Match if the response is equal to the specified value.
@@ -522,7 +522,7 @@ class MessagePredicate(Callable[[discord.Message], bool]):
cls,
value: str,
ctx: Optional[commands.Context] = None,
channel: Optional[discord.TextChannel] = None,
channel: Optional[Union[discord.TextChannel, discord.DMChannel]] = None,
user: Optional[discord.abc.User] = None,
) -> "MessagePredicate":
"""Match if the response *as lowercase* is equal to the specified value.
@@ -552,7 +552,7 @@ class MessagePredicate(Callable[[discord.Message], bool]):
cls,
value: Union[int, float],
ctx: Optional[commands.Context] = None,
channel: Optional[discord.TextChannel] = None,
channel: Optional[Union[discord.TextChannel, discord.DMChannel]] = None,
user: Optional[discord.abc.User] = None,
) -> "MessagePredicate":
"""Match if the response is less than the specified value.
@@ -583,7 +583,7 @@ class MessagePredicate(Callable[[discord.Message], bool]):
cls,
value: Union[int, float],
ctx: Optional[commands.Context] = None,
channel: Optional[discord.TextChannel] = None,
channel: Optional[Union[discord.TextChannel, discord.DMChannel]] = None,
user: Optional[discord.abc.User] = None,
) -> "MessagePredicate":
"""Match if the response is greater than the specified value.
@@ -614,7 +614,7 @@ class MessagePredicate(Callable[[discord.Message], bool]):
cls,
length: int,
ctx: Optional[commands.Context] = None,
channel: Optional[discord.TextChannel] = None,
channel: Optional[Union[discord.TextChannel, discord.DMChannel]] = None,
user: Optional[discord.abc.User] = None,
) -> "MessagePredicate":
"""Match if the response's length is less than the specified length.
@@ -644,7 +644,7 @@ class MessagePredicate(Callable[[discord.Message], bool]):
cls,
length: int,
ctx: Optional[commands.Context] = None,
channel: Optional[discord.TextChannel] = None,
channel: Optional[Union[discord.TextChannel, discord.DMChannel]] = None,
user: Optional[discord.abc.User] = None,
) -> "MessagePredicate":
"""Match if the response's length is greater than the specified length.
@@ -674,7 +674,7 @@ class MessagePredicate(Callable[[discord.Message], bool]):
cls,
collection: Sequence[str],
ctx: Optional[commands.Context] = None,
channel: Optional[discord.TextChannel] = None,
channel: Optional[Union[discord.TextChannel, discord.DMChannel]] = None,
user: Optional[discord.abc.User] = None,
) -> "MessagePredicate":
"""Match if the response is contained in the specified collection.
@@ -718,7 +718,7 @@ class MessagePredicate(Callable[[discord.Message], bool]):
cls,
collection: Sequence[str],
ctx: Optional[commands.Context] = None,
channel: Optional[discord.TextChannel] = None,
channel: Optional[Union[discord.TextChannel, discord.DMChannel]] = None,
user: Optional[discord.abc.User] = None,
) -> "MessagePredicate":
"""Same as :meth:`contained_in`, but the response is set to lowercase before matching.
@@ -759,7 +759,7 @@ class MessagePredicate(Callable[[discord.Message], bool]):
cls,
pattern: Union[Pattern[str], str],
ctx: Optional[commands.Context] = None,
channel: Optional[discord.TextChannel] = None,
channel: Optional[Union[discord.TextChannel, discord.DMChannel]] = None,
user: Optional[discord.abc.User] = None,
) -> "MessagePredicate":
"""Match if the response matches the specified regex pattern.

View File

@@ -58,7 +58,7 @@ class RotatingFileHandler(logging.handlers.RotatingFileHandler):
self.baseStem = stem
self.directory = directory.resolve()
# Scan for existing files in directory, append to last part of existing log
log_part_re = re.compile(rf"{stem}-part(?P<partnum>\d+).log")
log_part_re = re.compile(rf"{stem}-part(?P<partnum>\d)\.log")
highest_part = 0
for path in directory.iterdir():
match = log_part_re.match(path.name)
@@ -86,7 +86,7 @@ class RotatingFileHandler(logging.handlers.RotatingFileHandler):
initial_path.replace(self.directory / f"{self.baseStem}-part1.log")
match = re.match(
rf"{self.baseStem}(?:-part(?P<part>\d+)?)?.log", pathlib.Path(self.baseFilename).name
rf"{self.baseStem}(?:-part(?P<part>\d))?\.log", pathlib.Path(self.baseFilename).name
)
latest_part_num = int(match.groupdict(default="1").get("part", "1"))
if self.backupCount < 1:
@@ -95,7 +95,7 @@ class RotatingFileHandler(logging.handlers.RotatingFileHandler):
elif latest_part_num > self.backupCount:
# Rotate files down one
# red-part2.log becomes red-part1.log etc, a new log is added at the end.
for i in range(1, self.backupCount):
for i in range(1, self.backupCount + 1):
next_log = self.directory / f"{self.baseStem}-part{i + 1}.log"
if next_log.exists():
prev_log = self.directory / f"{self.baseStem}-part{i}.log"