From d14f83a4bc174a9c706552ba9b674cc1d9895efb Mon Sep 17 00:00:00 2001 From: vcokltfre Date: Wed, 7 Apr 2021 03:28:05 +0100 Subject: add custom command checks tag --- bot/resources/tags/customchecks.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 bot/resources/tags/customchecks.md diff --git a/bot/resources/tags/customchecks.md b/bot/resources/tags/customchecks.md new file mode 100644 index 000000000..4f0d62c8d --- /dev/null +++ b/bot/resources/tags/customchecks.md @@ -0,0 +1,21 @@ +**Custom Command Checks in discord.py** + +You may find yourself in need of a decorator to do something that doesn't exist in discord.py by default, but fear not, you can make your own! Using discord.py you can use `discord.ext.commands.check` to create you own decorators like this: +```py +from discord.ext.commands import check, Context + +def in_channel(*channels): + async def predicate(ctx: Context): + return ctx.channel.id in channels + return check(predicate) +``` +There's a fair bit to break down here, so let's start with what we're trying to achieve with this decorator. As you can probably guess from the name it's locking a command to a list of channels. The inner function named `predicate` is used to perform the actual check on the command context. Here you can do anything that requires a `Context` object. This inner function should return `True` if the check is **successful** or `False` if the check **fails**. + +Here's how we might use our new decorator: +```py +@bot.command(name="ping") +@in_channel(728343273562701984) +async def ping(ctx: Context): + ... +``` +This would lock the `ping` command to only be used in the channel `728343273562701984`. -- cgit v1.2.3 From 029f4aaeb627326e2b34a1e88b8a3108f5565426 Mon Sep 17 00:00:00 2001 From: vcokltfre Date: Wed, 7 Apr 2021 03:40:04 +0100 Subject: update wording to emphasise checks not decorators --- bot/resources/tags/customchecks.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bot/resources/tags/customchecks.md b/bot/resources/tags/customchecks.md index 4f0d62c8d..b4eb90872 100644 --- a/bot/resources/tags/customchecks.md +++ b/bot/resources/tags/customchecks.md @@ -1,6 +1,6 @@ **Custom Command Checks in discord.py** -You may find yourself in need of a decorator to do something that doesn't exist in discord.py by default, but fear not, you can make your own! Using discord.py you can use `discord.ext.commands.check` to create you own decorators like this: +You may find yourself in need of a check decorator to do something that doesn't exist in discord.py by default, but fear not, you can make your own! Using discord.py you can use `discord.ext.commands.check` to create you own checks like this: ```py from discord.ext.commands import check, Context @@ -9,9 +9,9 @@ def in_channel(*channels): return ctx.channel.id in channels return check(predicate) ``` -There's a fair bit to break down here, so let's start with what we're trying to achieve with this decorator. As you can probably guess from the name it's locking a command to a list of channels. The inner function named `predicate` is used to perform the actual check on the command context. Here you can do anything that requires a `Context` object. This inner function should return `True` if the check is **successful** or `False` if the check **fails**. +There's a fair bit to break down here, so let's start with what we're trying to achieve with this check. As you can probably guess from the name it's locking a command to a list of channels. The inner function named `predicate` is used to perform the actual check on the command context. Here you can do anything that requires a `Context` object. This inner function should return `True` if the check is **successful** or `False` if the check **fails**. -Here's how we might use our new decorator: +Here's how we might use our new check: ```py @bot.command(name="ping") @in_channel(728343273562701984) -- cgit v1.2.3 From fddfc7610a1402afaae3b1f5084b0735fa75afcf Mon Sep 17 00:00:00 2001 From: vcokltfre Date: Wed, 7 Apr 2021 13:27:01 +0100 Subject: rename function to in_any_channel in accordance with d.py naming --- bot/resources/tags/customchecks.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/bot/resources/tags/customchecks.md b/bot/resources/tags/customchecks.md index b4eb90872..96f833430 100644 --- a/bot/resources/tags/customchecks.md +++ b/bot/resources/tags/customchecks.md @@ -4,18 +4,18 @@ You may find yourself in need of a check decorator to do something that doesn't ```py from discord.ext.commands import check, Context -def in_channel(*channels): +def in_any_channel(*channels): async def predicate(ctx: Context): return ctx.channel.id in channels return check(predicate) ``` -There's a fair bit to break down here, so let's start with what we're trying to achieve with this check. As you can probably guess from the name it's locking a command to a list of channels. The inner function named `predicate` is used to perform the actual check on the command context. Here you can do anything that requires a `Context` object. This inner function should return `True` if the check is **successful** or `False` if the check **fails**. +There's a fair bit to break down here, so let's start with what we're trying to achieve with this check. As you can probably guess from the name it's locking a command to a **list of channels**. The inner function named `predicate` is used to perform the actual check on the command context. Here you can do anything that requires a `Context` object. This inner function should return `True` if the check is **successful** or `False` if the check **fails**. Here's how we might use our new check: ```py @bot.command(name="ping") -@in_channel(728343273562701984) +@in_any_channel(728343273562701984) async def ping(ctx: Context): ... ``` -This would lock the `ping` command to only be used in the channel `728343273562701984`. +This would lock the `ping` command to only be used in the channel `728343273562701984`. If this check function fails it will raise a `CheckFailure` exception, which can be handled in your error handler. -- cgit v1.2.3 From b38e645a66b76693ebc0cf0febc63187ab7a8b2f Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Fri, 9 Apr 2021 18:52:01 -0700 Subject: AntiSpam: prevent attempts to punish a user multiple times A user may manage to send multiple message that violate filters before the mute is applied. Because of a race condition, subsequent punish attempts did not detect the mute role exists and therefore proceeded to apply another mute. To avoid the race condition, abort any subsequent punish attempts while one is already ongoing for a given user. It could be possible to wait instead of abort, but the first attempt failing very likely means subsequent attempts would fail too. Fixes #902 --- bot/exts/filters/antispam.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/bot/exts/filters/antispam.py b/bot/exts/filters/antispam.py index af8528a68..c9052b138 100644 --- a/bot/exts/filters/antispam.py +++ b/bot/exts/filters/antispam.py @@ -3,7 +3,7 @@ import logging from collections.abc import Mapping from dataclasses import dataclass, field from datetime import datetime, timedelta -from operator import itemgetter +from operator import attrgetter, itemgetter from typing import Dict, Iterable, List, Set from discord import Colour, Member, Message, NotFound, Object, TextChannel @@ -18,6 +18,7 @@ from bot.constants import ( ) from bot.converters import Duration from bot.exts.moderation.modlog import ModLog +from bot.utils import lock from bot.utils.messages import format_user, send_attachments @@ -211,6 +212,7 @@ class AntiSpam(Cog): await self.maybe_delete_messages(channel, relevant_messages) break + @lock.lock_arg("antispam.punish", "member", attrgetter("id")) async def punish(self, msg: Message, member: Member, reason: str) -> None: """Punishes the given member for triggering an antispam rule.""" if not any(role.id == self.muted_role.id for role in member.roles): -- cgit v1.2.3 From 73b49b5b4d8f545da4d42b644907a34826757b3e Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Fri, 9 Apr 2021 18:59:49 -0700 Subject: AntiSpam: create tasks in a safer manner Name the tasks and use `scheduling.create_task` to ensure exceptions are caught. --- bot/exts/filters/antispam.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/bot/exts/filters/antispam.py b/bot/exts/filters/antispam.py index c9052b138..7555e25a2 100644 --- a/bot/exts/filters/antispam.py +++ b/bot/exts/filters/antispam.py @@ -18,7 +18,7 @@ from bot.constants import ( ) from bot.converters import Duration from bot.exts.moderation.modlog import ModLog -from bot.utils import lock +from bot.utils import lock, scheduling from bot.utils.messages import format_user, send_attachments @@ -115,7 +115,7 @@ class AntiSpam(Cog): self.message_deletion_queue = dict() - self.bot.loop.create_task(self.alert_on_validation_error()) + self.bot.loop.create_task(self.alert_on_validation_error(), name="AntiSpam.alert_on_validation_error") @property def mod_log(self) -> ModLog: @@ -192,7 +192,10 @@ class AntiSpam(Cog): if channel.id not in self.message_deletion_queue: log.trace(f"Creating queue for channel `{channel.id}`") self.message_deletion_queue[message.channel.id] = DeletionContext(channel) - self.bot.loop.create_task(self._process_deletion_context(message.channel.id)) + scheduling.create_task( + self._process_deletion_context(message.channel.id), + name=f"AntiSpam._process_deletion_context({message.channel.id})" + ) # Add the relevant of this trigger to the Deletion Context await self.message_deletion_queue[message.channel.id].add( @@ -202,11 +205,9 @@ class AntiSpam(Cog): ) for member in members: - - # Fire it off as a background task to ensure - # that the sleep doesn't block further tasks - self.bot.loop.create_task( - self.punish(message, member, full_reason) + scheduling.create_task( + self.punish(message, member, full_reason), + name=f"AntiSpam.punish(message={message.id}, member={member.id}, rule={rule_name})" ) await self.maybe_delete_messages(channel, relevant_messages) -- cgit v1.2.3 From 9ab05cbe3f23d442b5bc73311e0c3e8b075e396e Mon Sep 17 00:00:00 2001 From: kwzrd Date: Tue, 13 Apr 2021 19:05:45 +0200 Subject: Branding: use tz-aware datetime representation Using `datetime.utcnow` produces a tz-naive object. When converting the object into a POSIX timestamp (L212), the library then converts the naive object into UTC, which will offset it unless the local timezone is UTC. We prevent this behaviour by using an Arrow repr instead, which is by default tz-aware. Since the object already knows it is in UTC, it does not shift when converting to a timestamp. Because L233 used `fromtimestamp` rather than `utcfromtimestamp`, the timestamp then got converted back into local time, canceling the previous error. Therefore, the bug wasn't observable from logs, as the times looked correct, but were being stored incorrectly. By using `Arrow.utcfromtimestamp`, the created object will be aware of being UTC again, which is more safe. --- bot/exts/backend/branding/_cog.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/bot/exts/backend/branding/_cog.py b/bot/exts/backend/branding/_cog.py index 0a4ddcc88..fdc4a4167 100644 --- a/bot/exts/backend/branding/_cog.py +++ b/bot/exts/backend/branding/_cog.py @@ -3,12 +3,13 @@ import contextlib import logging import random import typing as t -from datetime import datetime, time, timedelta +from datetime import timedelta from enum import Enum from operator import attrgetter import async_timeout import discord +from arrow import Arrow from async_rediscache import RedisCache from discord.ext import commands, tasks @@ -208,7 +209,7 @@ class Branding(commands.Cog): if success: await self.cache_icons.increment(next_icon) # Push the icon into the next iteration. - timestamp = datetime.utcnow().timestamp() + timestamp = Arrow.utcnow().timestamp() await self.cache_information.set("last_rotation_timestamp", timestamp) return success @@ -229,8 +230,8 @@ class Branding(commands.Cog): await self.rotate_icons() return - last_rotation = datetime.fromtimestamp(last_rotation_timestamp) - difference = (datetime.utcnow() - last_rotation) + timedelta(minutes=5) + last_rotation = Arrow.utcfromtimestamp(last_rotation_timestamp) + difference = (Arrow.utcnow() - last_rotation) + timedelta(minutes=5) log.trace(f"Icons last rotated at {last_rotation} (difference: {difference}).") @@ -485,11 +486,11 @@ class Branding(commands.Cog): await self.daemon_loop() log.trace("Daemon before: calculating time to sleep before loop begins.") - now = datetime.utcnow() + now = Arrow.utcnow() # The actual midnight moment is offset into the future to prevent issues with imprecise sleep. - tomorrow = now + timedelta(days=1) - midnight = datetime.combine(tomorrow, time(minute=1)) + tomorrow = now.shift(days=1) + midnight = tomorrow.replace(hour=0, minute=1, second=0, microsecond=0) sleep_secs = (midnight - now).total_seconds() log.trace(f"Daemon before: sleeping {sleep_secs} seconds before next-up midnight: {midnight}.") -- cgit v1.2.3 From d29e98ba6808104d10b519ea6bf062242d682f16 Mon Sep 17 00:00:00 2001 From: kwzrd Date: Tue, 13 Apr 2021 21:56:52 +0200 Subject: Branding: adjust duration string for 1-day events Instead of: 'January 1 - January 1' Do: 'January 1' --- bot/exts/backend/branding/_cog.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/bot/exts/backend/branding/_cog.py b/bot/exts/backend/branding/_cog.py index fdc4a4167..47c379a34 100644 --- a/bot/exts/backend/branding/_cog.py +++ b/bot/exts/backend/branding/_cog.py @@ -58,6 +58,8 @@ def extract_event_duration(event: Event) -> str: Extract a human-readable, year-agnostic duration string from `event`. In the case that `event` is a fallback event, resolves to 'Fallback'. + + For 1-day events, only the single date is shown, instead of a period. """ if event.meta.is_fallback: return "Fallback" @@ -66,6 +68,9 @@ def extract_event_duration(event: Event) -> str: start_date = event.meta.start_date.strftime(fmt) end_date = event.meta.end_date.strftime(fmt) + if start_date == end_date: + return start_date + return f"{start_date} - {end_date}" -- cgit v1.2.3 From 475bd2124d56f6a59933b79b2f22c2b6c8896a25 Mon Sep 17 00:00:00 2001 From: Chris Date: Wed, 14 Apr 2021 00:19:25 +0100 Subject: Use a paginated embed to output multiple snowflakes Previously each snowflake passed to the command would have their own embed, which may cause the bot to send many embeds if a staff unknowingly passed it a bunch of snowflakes. This change makes sure that we don't run into rate limits on the bot by sending all of the snowflakes in one embed. --- bot/exts/utils/utils.py | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/bot/exts/utils/utils.py b/bot/exts/utils/utils.py index cae7f2593..60383996d 100644 --- a/bot/exts/utils/utils.py +++ b/bot/exts/utils/utils.py @@ -162,17 +162,26 @@ class Utils(Cog): if len(snowflakes) > 1 and await has_no_roles_check(ctx, *STAFF_ROLES): raise BadArgument("Cannot process more than one snowflake in one invocation.") + embed = Embed( + colour=Colour.blue() + ) + embed.set_author( + name=f"Snowflake{'s'[:len(snowflakes)^1]}", # Deals with pluralisation + icon_url="https://github.com/twitter/twemoji/blob/master/assets/72x72/2744.png?raw=true" + ) + + lines = [] for snowflake in snowflakes: created_at = snowflake_time(snowflake) - embed = Embed( - description=f"**Created at {created_at}** ({time_since(created_at, max_units=3)}).", - colour=Colour.blue() - ) - embed.set_author( - name=f"Snowflake: {snowflake}", - icon_url="https://github.com/twitter/twemoji/blob/master/assets/72x72/2744.png?raw=true" - ) - await ctx.send(embed=embed) + lines.append(f"**{snowflake}**\nCreated at {created_at} ({time_since(created_at, max_units=3)}).") + + await LinePaginator.paginate( + lines, + ctx=ctx, + embed=embed, + max_lines=5, + max_size=1000 + ) @command(aliases=("poll",)) @has_any_role(*MODERATION_ROLES, Roles.project_leads, Roles.domain_leads) -- cgit v1.2.3 From f9fb8631ce8568e0c9f15ea4ff0977e722ede3ba Mon Sep 17 00:00:00 2001 From: Chris Date: Wed, 14 Apr 2021 10:23:38 +0100 Subject: Require at least one snowflake to be provided. --- bot/exts/utils/utils.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/bot/exts/utils/utils.py b/bot/exts/utils/utils.py index 60383996d..0fe0cab78 100644 --- a/bot/exts/utils/utils.py +++ b/bot/exts/utils/utils.py @@ -162,6 +162,9 @@ class Utils(Cog): if len(snowflakes) > 1 and await has_no_roles_check(ctx, *STAFF_ROLES): raise BadArgument("Cannot process more than one snowflake in one invocation.") + if not snowflakes: + raise BadArgument("At least one snowflake must be provided.") + embed = Embed( colour=Colour.blue() ) -- cgit v1.2.3 From 347927cc6d86e852959db716b6de31c6a886640d Mon Sep 17 00:00:00 2001 From: Chris Date: Wed, 14 Apr 2021 16:54:27 +0100 Subject: Catch NotFound errors when trying to delete the invocation message when cleaning This often happens during a raid, when an int e script is added to ban & clean messages. Since the invocation message will be deleted on the first run, we should except subsequent NotFound errors. --- bot/exts/utils/clean.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/bot/exts/utils/clean.py b/bot/exts/utils/clean.py index 8acaf9131..cb662e852 100644 --- a/bot/exts/utils/clean.py +++ b/bot/exts/utils/clean.py @@ -3,7 +3,7 @@ import random import re from typing import Iterable, Optional -from discord import Colour, Embed, Message, TextChannel, User +from discord import Colour, Embed, Message, TextChannel, User, errors from discord.ext import commands from discord.ext.commands import Cog, Context, group, has_any_role @@ -115,7 +115,11 @@ class Clean(Cog): # Delete the invocation first self.mod_log.ignore(Event.message_delete, ctx.message.id) - await ctx.message.delete() + try: + await ctx.message.delete() + except errors.NotFound: + # Invocation message has already been deleted + log.info("Tried to delete invocation message, but it was already deleted.") messages = [] message_ids = [] -- cgit v1.2.3 From 56ff78ae70986dfbc01878e666a6aaf753739668 Mon Sep 17 00:00:00 2001 From: Chris Date: Wed, 14 Apr 2021 17:48:49 +0100 Subject: Refactor embed to use just one line --- bot/exts/utils/utils.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/bot/exts/utils/utils.py b/bot/exts/utils/utils.py index 0fe0cab78..8d9d27c64 100644 --- a/bot/exts/utils/utils.py +++ b/bot/exts/utils/utils.py @@ -165,9 +165,7 @@ class Utils(Cog): if not snowflakes: raise BadArgument("At least one snowflake must be provided.") - embed = Embed( - colour=Colour.blue() - ) + embed = Embed(colour=Colour.blue()) embed.set_author( name=f"Snowflake{'s'[:len(snowflakes)^1]}", # Deals with pluralisation icon_url="https://github.com/twitter/twemoji/blob/master/assets/72x72/2744.png?raw=true" -- cgit v1.2.3 From 15aa872d6ff7de253e3383380013aa7e52bab6c0 Mon Sep 17 00:00:00 2001 From: vcokltfre Date: Thu, 15 Apr 2021 06:40:57 +0100 Subject: chore: update wording as requested --- bot/resources/tags/customchecks.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bot/resources/tags/customchecks.md b/bot/resources/tags/customchecks.md index 96f833430..23ff7a66f 100644 --- a/bot/resources/tags/customchecks.md +++ b/bot/resources/tags/customchecks.md @@ -1,6 +1,6 @@ **Custom Command Checks in discord.py** -You may find yourself in need of a check decorator to do something that doesn't exist in discord.py by default, but fear not, you can make your own! Using discord.py you can use `discord.ext.commands.check` to create you own checks like this: +Often you may find the need to use checks that don't exist by default in discord.py. Fortunately, discord.py provides `discord.ext.commands.check` which allows you to create you own checks like this: ```py from discord.ext.commands import check, Context @@ -9,9 +9,9 @@ def in_any_channel(*channels): return ctx.channel.id in channels return check(predicate) ``` -There's a fair bit to break down here, so let's start with what we're trying to achieve with this check. As you can probably guess from the name it's locking a command to a **list of channels**. The inner function named `predicate` is used to perform the actual check on the command context. Here you can do anything that requires a `Context` object. This inner function should return `True` if the check is **successful** or `False` if the check **fails**. +This check is to check whether the invoked command is in a given set of channels. The inner function, named `predicate` here, is used to perform the actual check on the command, and check logic should go in this function. It must be an async function, and always provides a single `commands.Context` argument which you can use to create check logic. This check function should return a boolean value indicating whether the check passed (return `True`) or failed (return `False`). -Here's how we might use our new check: +The check can now be used like any other commands check as a decorator of a command, such as this: ```py @bot.command(name="ping") @in_any_channel(728343273562701984) -- cgit v1.2.3 From 6a875a0b0a6aca8dd33e711d00d5e9b92095918e Mon Sep 17 00:00:00 2001 From: mbaruh Date: Thu, 15 Apr 2021 20:33:35 +0300 Subject: Allow eval almost everywhere Adds a check to blacklist a command only in a specific context, with an option for a role override. The check is applied to the eval command to blacklist it only from python-general. --- bot/decorators.py | 41 ++++++++++++++++++++++++++++++++++++++++- bot/exts/utils/snekbox.py | 8 ++++---- 2 files changed, 44 insertions(+), 5 deletions(-) diff --git a/bot/decorators.py b/bot/decorators.py index 1d30317ef..5a49d64fc 100644 --- a/bot/decorators.py +++ b/bot/decorators.py @@ -11,7 +11,7 @@ from discord.ext.commands import Cog, Context from bot.constants import Channels, DEBUG_MODE, RedirectOutput from bot.utils import function -from bot.utils.checks import in_whitelist_check +from bot.utils.checks import InWhitelistCheckFailure, in_whitelist_check from bot.utils.function import command_wraps log = logging.getLogger(__name__) @@ -45,6 +45,45 @@ def in_whitelist( return commands.check(predicate) +def not_in_blacklist( + *, + channels: t.Container[int] = (), + categories: t.Container[int] = (), + roles: t.Container[int] = (), + override_roles: t.Container[int] = (), + redirect: t.Optional[int] = Channels.bot_commands, + fail_silently: bool = False, +) -> t.Callable: + """ + Check if a command was not issued in a blacklisted context. + + The blacklists that can be provided are: + + - `channels`: a container with channel ids for blacklisted channels + - `categories`: a container with category ids for blacklisted categories + - `roles`: a container with role ids for blacklisted roles + + If the command was invoked in a context that was blacklisted, the member is either + redirected to the `redirect` channel that was passed (default: #bot-commands) or simply + told that they're not allowed to use this particular command (if `None` was passed). + + The blacklist can be overridden through the roles specified in `override_roles`. + """ + def predicate(ctx: Context) -> bool: + """Check if command was issued in a blacklisted context.""" + not_blacklisted = not in_whitelist_check(ctx, channels, categories, roles, fail_silently=True) + overridden = in_whitelist_check(ctx, roles=override_roles, fail_silently=True) + + success = not_blacklisted or overridden + + if not success and not fail_silently: + raise InWhitelistCheckFailure(redirect) + + return success + + return commands.check(predicate) + + def has_no_roles(*roles: t.Union[str, int]) -> t.Callable: """ Returns True if the user does not have any of the roles specified. diff --git a/bot/exts/utils/snekbox.py b/bot/exts/utils/snekbox.py index 9f480c067..6ea588888 100644 --- a/bot/exts/utils/snekbox.py +++ b/bot/exts/utils/snekbox.py @@ -13,7 +13,7 @@ from discord.ext.commands import Cog, Context, command, guild_only from bot.bot import Bot from bot.constants import Categories, Channels, Roles, URLs -from bot.decorators import in_whitelist +from bot.decorators import not_in_blacklist from bot.utils import send_to_paste_service from bot.utils.messages import wait_for_deletion @@ -39,8 +39,8 @@ RAW_CODE_REGEX = re.compile( MAX_PASTE_LEN = 10000 # `!eval` command whitelists -EVAL_CHANNELS = (Channels.bot_commands, Channels.esoteric) -EVAL_CATEGORIES = (Categories.help_available, Categories.help_in_use, Categories.voice) +NO_EVAL_CHANNELS = (Channels.python_general,) +NO_EVAL_CATEGORIES = () EVAL_ROLES = (Roles.helpers, Roles.moderators, Roles.admins, Roles.owners, Roles.python_community, Roles.partners) SIGKILL = 9 @@ -280,7 +280,7 @@ class Snekbox(Cog): @command(name="eval", aliases=("e",)) @guild_only() - @in_whitelist(channels=EVAL_CHANNELS, categories=EVAL_CATEGORIES, roles=EVAL_ROLES) + @not_in_blacklist(channels=NO_EVAL_CHANNELS, categories=NO_EVAL_CATEGORIES, override_roles=EVAL_ROLES) async def eval_command(self, ctx: Context, *, code: str = None) -> None: """ Run Python code and get the results. -- cgit v1.2.3 From 854b0f4944700cb7a5b6a032029e513cef390e7e Mon Sep 17 00:00:00 2001 From: mbaruh Date: Fri, 16 Apr 2021 00:06:31 +0300 Subject: Raise a new NotInBlacklistCheckFailure instead This creates a new baseclass called ContextCheckFailure, and the new error as well as InWhitelistCheckFailure now derive it. --- bot/decorators.py | 8 ++++++-- bot/exts/backend/error_handler.py | 4 ++-- bot/exts/utils/snekbox.py | 2 +- bot/utils/checks.py | 8 ++++++-- 4 files changed, 15 insertions(+), 7 deletions(-) diff --git a/bot/decorators.py b/bot/decorators.py index 5a49d64fc..e971a5bd3 100644 --- a/bot/decorators.py +++ b/bot/decorators.py @@ -11,7 +11,7 @@ from discord.ext.commands import Cog, Context from bot.constants import Channels, DEBUG_MODE, RedirectOutput from bot.utils import function -from bot.utils.checks import InWhitelistCheckFailure, in_whitelist_check +from bot.utils.checks import ContextCheckFailure, in_whitelist_check from bot.utils.function import command_wraps log = logging.getLogger(__name__) @@ -45,6 +45,10 @@ def in_whitelist( return commands.check(predicate) +class NotInBlacklistCheckFailure(ContextCheckFailure): + """Raised when the 'not_in_blacklist' check fails.""" + + def not_in_blacklist( *, channels: t.Container[int] = (), @@ -77,7 +81,7 @@ def not_in_blacklist( success = not_blacklisted or overridden if not success and not fail_silently: - raise InWhitelistCheckFailure(redirect) + raise NotInBlacklistCheckFailure(redirect) return success diff --git a/bot/exts/backend/error_handler.py b/bot/exts/backend/error_handler.py index 76ab7dfc2..da0e94a7e 100644 --- a/bot/exts/backend/error_handler.py +++ b/bot/exts/backend/error_handler.py @@ -12,7 +12,7 @@ from bot.bot import Bot from bot.constants import Colours, Icons, MODERATION_ROLES from bot.converters import TagNameConverter from bot.errors import InvalidInfractedUser, LockedResourceError -from bot.utils.checks import InWhitelistCheckFailure +from bot.utils.checks import ContextCheckFailure log = logging.getLogger(__name__) @@ -274,7 +274,7 @@ class ErrorHandler(Cog): await ctx.send( "Sorry, it looks like I don't have the permissions or roles I need to do that." ) - elif isinstance(e, (InWhitelistCheckFailure, errors.NoPrivateMessage)): + elif isinstance(e, (ContextCheckFailure, errors.NoPrivateMessage)): ctx.bot.stats.incr("errors.wrong_channel_or_dm_error") await ctx.send(e) diff --git a/bot/exts/utils/snekbox.py b/bot/exts/utils/snekbox.py index 6ea588888..da95240bb 100644 --- a/bot/exts/utils/snekbox.py +++ b/bot/exts/utils/snekbox.py @@ -38,7 +38,7 @@ RAW_CODE_REGEX = re.compile( MAX_PASTE_LEN = 10000 -# `!eval` command whitelists +# `!eval` command whitelists and blacklists. NO_EVAL_CHANNELS = (Channels.python_general,) NO_EVAL_CATEGORIES = () EVAL_ROLES = (Roles.helpers, Roles.moderators, Roles.admins, Roles.owners, Roles.python_community, Roles.partners) diff --git a/bot/utils/checks.py b/bot/utils/checks.py index 460a937d8..3d0c8a50c 100644 --- a/bot/utils/checks.py +++ b/bot/utils/checks.py @@ -20,8 +20,8 @@ from bot import constants log = logging.getLogger(__name__) -class InWhitelistCheckFailure(CheckFailure): - """Raised when the `in_whitelist` check fails.""" +class ContextCheckFailure(CheckFailure): + """Raised when a context-specific check fails.""" def __init__(self, redirect_channel: Optional[int]) -> None: self.redirect_channel = redirect_channel @@ -36,6 +36,10 @@ class InWhitelistCheckFailure(CheckFailure): super().__init__(error_message) +class InWhitelistCheckFailure(ContextCheckFailure): + """Raised when the `in_whitelist` check fails.""" + + def in_whitelist_check( ctx: Context, channels: Container[int] = (), -- cgit v1.2.3 From 94af3c07678f1f2dee722f4780a816426efd0851 Mon Sep 17 00:00:00 2001 From: Vivaan Verma Date: Sun, 18 Apr 2021 21:12:08 +0100 Subject: Added default duration of 1h to superstarify --- bot/exts/moderation/infraction/superstarify.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/moderation/infraction/superstarify.py b/bot/exts/moderation/infraction/superstarify.py index 704dddf9c..245f14905 100644 --- a/bot/exts/moderation/infraction/superstarify.py +++ b/bot/exts/moderation/infraction/superstarify.py @@ -109,7 +109,7 @@ class Superstarify(InfractionScheduler, Cog): self, ctx: Context, member: Member, - duration: Expiry, + duration: Expiry = "1h", *, reason: str = '', ) -> None: -- cgit v1.2.3 From 3126e00a28e498afc8ecef1ed87b356f0e4a38c4 Mon Sep 17 00:00:00 2001 From: Vivaan Verma Date: Sun, 18 Apr 2021 22:11:46 +0100 Subject: Make duration an optional arg and default it to 1 hour --- bot/exts/moderation/infraction/superstarify.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/bot/exts/moderation/infraction/superstarify.py b/bot/exts/moderation/infraction/superstarify.py index 245f14905..8a6d14d41 100644 --- a/bot/exts/moderation/infraction/superstarify.py +++ b/bot/exts/moderation/infraction/superstarify.py @@ -1,3 +1,4 @@ +import datetime import json import logging import random @@ -109,7 +110,7 @@ class Superstarify(InfractionScheduler, Cog): self, ctx: Context, member: Member, - duration: Expiry = "1h", + duration: t.Optional[Expiry], *, reason: str = '', ) -> None: @@ -134,6 +135,9 @@ class Superstarify(InfractionScheduler, Cog): if await _utils.get_active_infraction(ctx, member, "superstar"): return + # Set the duration to 1 hour if none was provided + duration = datetime.datetime.now() + datetime.timedelta(hours=1) + # Post the infraction to the API old_nick = member.display_name infraction_reason = f'Old nickname: {old_nick}. {reason}' -- cgit v1.2.3 From 7fc5e37ecd2e1589b77b7fa16af26ee42e72dcdc Mon Sep 17 00:00:00 2001 From: Vivaan Verma Date: Sun, 18 Apr 2021 22:17:27 +0100 Subject: Check if a duration was provided --- bot/exts/moderation/infraction/superstarify.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/bot/exts/moderation/infraction/superstarify.py b/bot/exts/moderation/infraction/superstarify.py index 8a6d14d41..f5d6259cd 100644 --- a/bot/exts/moderation/infraction/superstarify.py +++ b/bot/exts/moderation/infraction/superstarify.py @@ -136,8 +136,9 @@ class Superstarify(InfractionScheduler, Cog): return # Set the duration to 1 hour if none was provided - duration = datetime.datetime.now() + datetime.timedelta(hours=1) - + if not duration: + duration = datetime.datetime.now() + datetime.timedelta(hours=1) + # Post the infraction to the API old_nick = member.display_name infraction_reason = f'Old nickname: {old_nick}. {reason}' -- cgit v1.2.3 From 6169ed2b73a5f2d763a2758e69ba4983127a1373 Mon Sep 17 00:00:00 2001 From: Vivaan Verma <54081925+doublevcodes@users.noreply.github.com> Date: Sun, 18 Apr 2021 22:31:40 +0100 Subject: Fix linting errors --- bot/exts/moderation/infraction/superstarify.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/moderation/infraction/superstarify.py b/bot/exts/moderation/infraction/superstarify.py index f5d6259cd..6fa0d550f 100644 --- a/bot/exts/moderation/infraction/superstarify.py +++ b/bot/exts/moderation/infraction/superstarify.py @@ -138,7 +138,7 @@ class Superstarify(InfractionScheduler, Cog): # Set the duration to 1 hour if none was provided if not duration: duration = datetime.datetime.now() + datetime.timedelta(hours=1) - + # Post the infraction to the API old_nick = member.display_name infraction_reason = f'Old nickname: {old_nick}. {reason}' -- cgit v1.2.3 From cb253750a5597d8ca63e8742307bafc096c7e189 Mon Sep 17 00:00:00 2001 From: Chris Date: Mon, 19 Apr 2021 18:05:11 +0100 Subject: Require a mod role for stream commands Previously any staff member (including helpers) could use the stream commands. --- bot/exts/moderation/stream.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/bot/exts/moderation/stream.py b/bot/exts/moderation/stream.py index 12e195172..7ea7f635b 100644 --- a/bot/exts/moderation/stream.py +++ b/bot/exts/moderation/stream.py @@ -8,7 +8,7 @@ from async_rediscache import RedisCache from discord.ext import commands from bot.bot import Bot -from bot.constants import Colours, Emojis, Guild, Roles, STAFF_ROLES, VideoPermission +from bot.constants import Colours, Emojis, Guild, MODERATION_ROLES, Roles, VideoPermission from bot.converters import Expiry from bot.utils.scheduling import Scheduler from bot.utils.time import format_infraction_with_duration @@ -69,7 +69,7 @@ class Stream(commands.Cog): ) @commands.command(aliases=("streaming",)) - @commands.has_any_role(*STAFF_ROLES) + @commands.has_any_role(*MODERATION_ROLES) async def stream(self, ctx: commands.Context, member: discord.Member, duration: Expiry = None) -> None: """ Temporarily grant streaming permissions to a member for a given duration. @@ -126,7 +126,7 @@ class Stream(commands.Cog): log.debug(f"Successfully gave {member} ({member.id}) permission to stream until {revoke_time}.") @commands.command(aliases=("pstream",)) - @commands.has_any_role(*STAFF_ROLES) + @commands.has_any_role(*MODERATION_ROLES) async def permanentstream(self, ctx: commands.Context, member: discord.Member) -> None: """Permanently grants the given member the permission to stream.""" log.trace(f"Attempting to give permanent streaming permission to {member} ({member.id}).") @@ -153,7 +153,7 @@ class Stream(commands.Cog): log.debug(f"Successfully gave {member} ({member.id}) permanent streaming permission.") @commands.command(aliases=("unstream", "rstream")) - @commands.has_any_role(*STAFF_ROLES) + @commands.has_any_role(*MODERATION_ROLES) async def revokestream(self, ctx: commands.Context, member: discord.Member) -> None: """Revoke the permission to stream from the given member.""" log.trace(f"Attempting to remove streaming permission from {member} ({member.id}).") -- cgit v1.2.3 From 90ed28f4cb31b5b41f7a395abfe61f4f9e49e091 Mon Sep 17 00:00:00 2001 From: Chris Date: Mon, 19 Apr 2021 18:08:39 +0100 Subject: Add command to list users with streaming perms This is useful to audit users who still have the permission to stream. I have chosen to also sort and paginate the embed to make it easier to read. The sorting is based on how long until the user's streaming permissions are revoked, with permanent streamers at the end. --- bot/exts/moderation/stream.py | 44 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/bot/exts/moderation/stream.py b/bot/exts/moderation/stream.py index 7ea7f635b..5f3820748 100644 --- a/bot/exts/moderation/stream.py +++ b/bot/exts/moderation/stream.py @@ -1,5 +1,6 @@ import logging from datetime import timedelta, timezone +from operator import itemgetter import arrow import discord @@ -8,8 +9,9 @@ from async_rediscache import RedisCache from discord.ext import commands from bot.bot import Bot -from bot.constants import Colours, Emojis, Guild, MODERATION_ROLES, Roles, VideoPermission +from bot.constants import Colours, Emojis, Guild, MODERATION_ROLES, Roles, STAFF_ROLES, VideoPermission from bot.converters import Expiry +from bot.pagination import LinePaginator from bot.utils.scheduling import Scheduler from bot.utils.time import format_infraction_with_duration @@ -173,6 +175,46 @@ class Stream(commands.Cog): await ctx.send(f"{Emojis.cross_mark} This member doesn't have video permissions to remove!") log.debug(f"{member} ({member.id}) didn't have the streaming permission to remove!") + @commands.command(aliases=('lstream',)) + @commands.has_any_role(*MODERATION_ROLES) + async def liststream(self, ctx: commands.Context) -> None: + """Lists all non-staff users who have permission to stream.""" + non_staff_members_with_stream = [ + _member + for _member in ctx.guild.get_role(Roles.video).members + if not any(role.id in STAFF_ROLES for role in _member.roles) + ] + + # List of tuples (UtcPosixTimestamp, str) + # This is so that we can sort before outputting to the paginator + streamer_info = [] + for member in non_staff_members_with_stream: + if revoke_time := await self.task_cache.get(member.id): + # Member only has temporary streaming perms + revoke_delta = Arrow.utcfromtimestamp(revoke_time).humanize() + message = f"{member.mention} will have stream permissions revoked {revoke_delta}." + else: + message = f"{member.mention} has permanent streaming permissions." + + # If revoke_time is None use max timestamp to force sort to put them at the end + streamer_info.append( + (revoke_time or Arrow.max.timestamp(), message) + ) + + if streamer_info: + # Sort based on duration left of streaming perms + streamer_info.sort(key=itemgetter(0)) + + # Only output the message in the pagination + lines = [line[1] for line in streamer_info] + embed = discord.Embed( + title=f"Members who can stream (`{len(lines)}` total)", + colour=Colours.soft_green + ) + await LinePaginator.paginate(lines, ctx, embed, max_size=400, empty=False) + else: + await ctx.send("No members with stream permissions found.") + def setup(bot: Bot) -> None: """Loads the Stream cog.""" -- cgit v1.2.3 From a6b76092e6e6005fc98c9863db051804d7bb963a Mon Sep 17 00:00:00 2001 From: Chris Date: Mon, 19 Apr 2021 18:16:42 +0100 Subject: Update wording of comment to be clearer. --- bot/exts/moderation/stream.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/moderation/stream.py b/bot/exts/moderation/stream.py index 5f3820748..d9837b5ed 100644 --- a/bot/exts/moderation/stream.py +++ b/bot/exts/moderation/stream.py @@ -186,7 +186,7 @@ class Stream(commands.Cog): ] # List of tuples (UtcPosixTimestamp, str) - # This is so that we can sort before outputting to the paginator + # This is so that output can be sorted on [0] before passed it's to the paginator streamer_info = [] for member in non_staff_members_with_stream: if revoke_time := await self.task_cache.get(member.id): -- cgit v1.2.3 From 94db90b038574077beb2fafb4f17741061ee8152 Mon Sep 17 00:00:00 2001 From: Chris Date: Mon, 19 Apr 2021 18:23:34 +0100 Subject: Remove unnecessary _ in variable name --- bot/exts/moderation/stream.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bot/exts/moderation/stream.py b/bot/exts/moderation/stream.py index d9837b5ed..e541baeb2 100644 --- a/bot/exts/moderation/stream.py +++ b/bot/exts/moderation/stream.py @@ -180,9 +180,9 @@ class Stream(commands.Cog): async def liststream(self, ctx: commands.Context) -> None: """Lists all non-staff users who have permission to stream.""" non_staff_members_with_stream = [ - _member - for _member in ctx.guild.get_role(Roles.video).members - if not any(role.id in STAFF_ROLES for role in _member.roles) + member + for member in ctx.guild.get_role(Roles.video).members + if not any(role.id in STAFF_ROLES for role in member.roles) ] # List of tuples (UtcPosixTimestamp, str) -- cgit v1.2.3 From 131dab3754da9fc1c3cf770d76bb9deea46f2f8d Mon Sep 17 00:00:00 2001 From: ChrisJL Date: Mon, 19 Apr 2021 18:40:23 +0100 Subject: Improve the wording of the list streamers embed Co-authored-by: Matteo Bertucci --- bot/exts/moderation/stream.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/moderation/stream.py b/bot/exts/moderation/stream.py index e541baeb2..bd93ea492 100644 --- a/bot/exts/moderation/stream.py +++ b/bot/exts/moderation/stream.py @@ -208,7 +208,7 @@ class Stream(commands.Cog): # Only output the message in the pagination lines = [line[1] for line in streamer_info] embed = discord.Embed( - title=f"Members who can stream (`{len(lines)}` total)", + title=f"Members with streaming permission (`{len(lines)}` total)", colour=Colours.soft_green ) await LinePaginator.paginate(lines, ctx, embed, max_size=400, empty=False) -- cgit v1.2.3 From c001456cf29f944deb632b28130fb16a170092e9 Mon Sep 17 00:00:00 2001 From: Chris Date: Mon, 19 Apr 2021 18:49:09 +0100 Subject: Update comment in list stream for readibility --- bot/exts/moderation/stream.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/moderation/stream.py b/bot/exts/moderation/stream.py index bd93ea492..1dbb2a46b 100644 --- a/bot/exts/moderation/stream.py +++ b/bot/exts/moderation/stream.py @@ -186,7 +186,7 @@ class Stream(commands.Cog): ] # List of tuples (UtcPosixTimestamp, str) - # This is so that output can be sorted on [0] before passed it's to the paginator + # So that the list can be sorted on the UtcPosixTimestamp before the message is passed to the paginator. streamer_info = [] for member in non_staff_members_with_stream: if revoke_time := await self.task_cache.get(member.id): -- cgit v1.2.3 From b8b920bfa5c4d918d41bfe06d85b1e85f4bec0da Mon Sep 17 00:00:00 2001 From: Vivaan Verma <54081925+doublevcodes@users.noreply.github.com> Date: Mon, 19 Apr 2021 20:01:41 +0100 Subject: Inline duration assignment Co-authored-by: Rohan Reddy Alleti --- bot/exts/moderation/infraction/superstarify.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/bot/exts/moderation/infraction/superstarify.py b/bot/exts/moderation/infraction/superstarify.py index 6fa0d550f..3d880dec3 100644 --- a/bot/exts/moderation/infraction/superstarify.py +++ b/bot/exts/moderation/infraction/superstarify.py @@ -136,8 +136,7 @@ class Superstarify(InfractionScheduler, Cog): return # Set the duration to 1 hour if none was provided - if not duration: - duration = datetime.datetime.now() + datetime.timedelta(hours=1) + duration = duration or datetime.datetime.utcnow() + datetime.timedelta(hours=1) # Post the infraction to the API old_nick = member.display_name -- cgit v1.2.3 From ae5d1cb65ddec0e70df00a4051a5bf813d4e6e20 Mon Sep 17 00:00:00 2001 From: Vivaan Verma Date: Mon, 19 Apr 2021 21:06:15 +0100 Subject: Add default duration as constant and use Duration converter --- bot/exts/moderation/infraction/superstarify.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/bot/exts/moderation/infraction/superstarify.py b/bot/exts/moderation/infraction/superstarify.py index 3d880dec3..0bc2198c3 100644 --- a/bot/exts/moderation/infraction/superstarify.py +++ b/bot/exts/moderation/infraction/superstarify.py @@ -1,4 +1,3 @@ -import datetime import json import logging import random @@ -12,7 +11,7 @@ from discord.utils import escape_markdown from bot import constants from bot.bot import Bot -from bot.converters import Expiry +from bot.converters import Duration from bot.exts.moderation.infraction import _utils from bot.exts.moderation.infraction._scheduler import InfractionScheduler from bot.utils.messages import format_user @@ -20,6 +19,7 @@ from bot.utils.time import format_infraction log = logging.getLogger(__name__) NICKNAME_POLICY_URL = "https://pythondiscord.com/pages/rules/#nickname-policy" +SUPERSTARIFY_DEFAULT_DURATION = "1h" with Path("bot/resources/stars.json").open(encoding="utf-8") as stars_file: STAR_NAMES = json.load(stars_file) @@ -110,7 +110,7 @@ class Superstarify(InfractionScheduler, Cog): self, ctx: Context, member: Member, - duration: t.Optional[Expiry], + duration: t.Optional[Duration], *, reason: str = '', ) -> None: @@ -136,7 +136,7 @@ class Superstarify(InfractionScheduler, Cog): return # Set the duration to 1 hour if none was provided - duration = duration or datetime.datetime.utcnow() + datetime.timedelta(hours=1) + duration = duration or await Duration().convert(ctx, SUPERSTARIFY_DEFAULT_DURATION) # Post the infraction to the API old_nick = member.display_name -- cgit v1.2.3 From 03f909df6758a10c95f0b63df487f1acd97ec36d Mon Sep 17 00:00:00 2001 From: Vivaan Verma Date: Mon, 19 Apr 2021 21:15:11 +0100 Subject: Change type hint from duration to expiry --- bot/exts/moderation/infraction/superstarify.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bot/exts/moderation/infraction/superstarify.py b/bot/exts/moderation/infraction/superstarify.py index 0bc2198c3..ef88fb43f 100644 --- a/bot/exts/moderation/infraction/superstarify.py +++ b/bot/exts/moderation/infraction/superstarify.py @@ -11,7 +11,7 @@ from discord.utils import escape_markdown from bot import constants from bot.bot import Bot -from bot.converters import Duration +from bot.converters import Duration, Expiry from bot.exts.moderation.infraction import _utils from bot.exts.moderation.infraction._scheduler import InfractionScheduler from bot.utils.messages import format_user @@ -110,7 +110,7 @@ class Superstarify(InfractionScheduler, Cog): self, ctx: Context, member: Member, - duration: t.Optional[Duration], + duration: t.Optional[Expiry], *, reason: str = '', ) -> None: -- cgit v1.2.3 From 91bdf9415ec88715fadf2e0a56b900b376b638db Mon Sep 17 00:00:00 2001 From: Vivaan Verma <54081925+doublevcodes@users.noreply.github.com> Date: Mon, 19 Apr 2021 22:02:45 +0100 Subject: Update bot/exts/moderation/infraction/superstarify.py Co-authored-by: Boris Muratov <8bee278@gmail.com> --- bot/exts/moderation/infraction/superstarify.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/moderation/infraction/superstarify.py b/bot/exts/moderation/infraction/superstarify.py index ef88fb43f..07e79b9fe 100644 --- a/bot/exts/moderation/infraction/superstarify.py +++ b/bot/exts/moderation/infraction/superstarify.py @@ -135,7 +135,7 @@ class Superstarify(InfractionScheduler, Cog): if await _utils.get_active_infraction(ctx, member, "superstar"): return - # Set the duration to 1 hour if none was provided + # Set to default duration if none was provided. duration = duration or await Duration().convert(ctx, SUPERSTARIFY_DEFAULT_DURATION) # Post the infraction to the API -- cgit v1.2.3