From 776825d09530be6b57759201795c436823002007 Mon Sep 17 00:00:00 2001 From: Thomas Petersson Date: Mon, 5 Oct 2020 18:11:34 +0200 Subject: fix(statsd): Gracefully handle gaierro Per issue #1185 the bot might go down if the statsd client fails to connect during instantiation. This can be caused by an outage on their part, or network issues. If this happens getaddrinfo will raise a gaierror. This PR catched the error, sets self.stats to None for the time being, and handles that elsewhere. In addition a fallback logic was added to attempt to reconnect, in the off-chance it's a temporary outage --- bot/bot.py | 34 ++++++++++++++++++++++++++++++---- 1 file changed, 30 insertions(+), 4 deletions(-) diff --git a/bot/bot.py b/bot/bot.py index b2e5237fe..0b842d07a 100644 --- a/bot/bot.py +++ b/bot/bot.py @@ -46,7 +46,25 @@ class Bot(commands.Bot): # will effectively disable stats. statsd_url = "127.0.0.1" - self.stats = AsyncStatsClient(self.loop, statsd_url, 8125, prefix="bot") + try: + self.stats = AsyncStatsClient(self.loop, statsd_url, 8125, prefix="bot") + except socket.gaierror as socket_error: + self.stats = None + self.loop.call_later(30, self.retry_statsd_connection, statsd_url) + log.warning(f"Statsd client failed to instantiate with error:\n{socket_error}") + + def retry_statsd_connection(self, statsd_url: str, retry_after: int = 30, attempt: int = 1) -> None: + """Callback used to retry a connection to statsd if it should fail.""" + if attempt >= 10: + log.error("Reached 10 attempts trying to reconnect AsyncStatsClient. Aborting") + return + + try: + self.stats = AsyncStatsClient(self.loop, statsd_url, 8125, prefix="bot") + except socket.gaierror: + log.warning(f"Statsd client failed to reconnect (Retry attempt: {attempt})") + # Use a fallback strategy for retrying, up to 10 times. + self.loop.call_later(retry_after, self.retry_statsd_connection, statsd_url, retry_after * 2, attempt + 1) async def cache_filter_list_data(self) -> None: """Cache all the data in the FilterList on the site.""" @@ -146,7 +164,7 @@ class Bot(commands.Bot): if self._resolver: await self._resolver.close() - if self.stats._transport: + if self.stats and self.stats._transport: self.stats._transport.close() if self.redis_session: @@ -168,7 +186,12 @@ class Bot(commands.Bot): async def login(self, *args, **kwargs) -> None: """Re-create the connector and set up sessions before logging into Discord.""" self._recreate() - await self.stats.create_socket() + + if self.stats: + await self.stats.create_socket() + else: + log.info("self.stats is not defined, skipping create_socket step in login") + await super().login(*args, **kwargs) async def on_guild_available(self, guild: discord.Guild) -> None: @@ -214,7 +237,10 @@ class Bot(commands.Bot): async def on_error(self, event: str, *args, **kwargs) -> None: """Log errors raised in event listeners rather than printing them to stderr.""" - self.stats.incr(f"errors.event.{event}") + if self.stats: + self.stats.incr(f"errors.event.{event}") + else: + log.info(f"self.stats is not defined, skipping errors.event.{event} increment in on_error") with push_scope() as scope: scope.set_tag("event", event) -- cgit v1.2.3 From ca761f04eb3353ae4e9a992d23d13d131d5a6ad0 Mon Sep 17 00:00:00 2001 From: Thomas Petersson Date: Mon, 5 Oct 2020 19:30:58 +0200 Subject: fix(bot): Not assign stats to None self.stats is referred to as bot.stats in the project, which was overlooked. This should "disable" stats until it's successfully reconnected. The retry attempts will continue until it stops throwing or fails 10x --- bot/bot.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/bot/bot.py b/bot/bot.py index 0b842d07a..545efefe6 100644 --- a/bot/bot.py +++ b/bot/bot.py @@ -15,6 +15,7 @@ from bot import DEBUG_MODE, api, constants from bot.async_stats import AsyncStatsClient log = logging.getLogger('bot') +LOCALHOST = "127.0.0.1" class Bot(commands.Bot): @@ -44,12 +45,12 @@ class Bot(commands.Bot): # Since statsd is UDP, there are no errors for sending to a down port. # For this reason, setting the statsd host to 127.0.0.1 for development # will effectively disable stats. - statsd_url = "127.0.0.1" + statsd_url = LOCALHOST try: self.stats = AsyncStatsClient(self.loop, statsd_url, 8125, prefix="bot") except socket.gaierror as socket_error: - self.stats = None + self.stats = AsyncStatsClient(self.loop, LOCALHOST) self.loop.call_later(30, self.retry_statsd_connection, statsd_url) log.warning(f"Statsd client failed to instantiate with error:\n{socket_error}") -- cgit v1.2.3 From 897e714ec0ce8468f10e3c20b50e30bfc96e5c77 Mon Sep 17 00:00:00 2001 From: Thomas Petersson Date: Mon, 5 Oct 2020 19:32:22 +0200 Subject: fix(bot): redundant false checks on self.stats --- bot/bot.py | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/bot/bot.py b/bot/bot.py index 545efefe6..fbf5eb761 100644 --- a/bot/bot.py +++ b/bot/bot.py @@ -165,7 +165,7 @@ class Bot(commands.Bot): if self._resolver: await self._resolver.close() - if self.stats and self.stats._transport: + if self.stats._transport: self.stats._transport.close() if self.redis_session: @@ -187,12 +187,7 @@ class Bot(commands.Bot): async def login(self, *args, **kwargs) -> None: """Re-create the connector and set up sessions before logging into Discord.""" self._recreate() - - if self.stats: - await self.stats.create_socket() - else: - log.info("self.stats is not defined, skipping create_socket step in login") - + await self.stats.create_socket() await super().login(*args, **kwargs) async def on_guild_available(self, guild: discord.Guild) -> None: @@ -238,10 +233,7 @@ class Bot(commands.Bot): async def on_error(self, event: str, *args, **kwargs) -> None: """Log errors raised in event listeners rather than printing them to stderr.""" - if self.stats: - self.stats.incr(f"errors.event.{event}") - else: - log.info(f"self.stats is not defined, skipping errors.event.{event} increment in on_error") + self.stats.incr(f"errors.event.{event}") with push_scope() as scope: scope.set_tag("event", event) -- cgit v1.2.3 From 7a817f8e088546b535c0a0d71c08f5abbeb4bb0c Mon Sep 17 00:00:00 2001 From: Thomas Petersson Date: Mon, 5 Oct 2020 23:38:17 +0200 Subject: fix(bot): refactor of connect_statsd --- bot/bot.py | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/bot/bot.py b/bot/bot.py index fbf5eb761..06827c7e6 100644 --- a/bot/bot.py +++ b/bot/bot.py @@ -47,14 +47,10 @@ class Bot(commands.Bot): # will effectively disable stats. statsd_url = LOCALHOST - try: - self.stats = AsyncStatsClient(self.loop, statsd_url, 8125, prefix="bot") - except socket.gaierror as socket_error: - self.stats = AsyncStatsClient(self.loop, LOCALHOST) - self.loop.call_later(30, self.retry_statsd_connection, statsd_url) - log.warning(f"Statsd client failed to instantiate with error:\n{socket_error}") + self.stats = AsyncStatsClient(self.loop, LOCALHOST) + self.connect_statsd(statsd_url) - def retry_statsd_connection(self, statsd_url: str, retry_after: int = 30, attempt: int = 1) -> None: + def connect_statsd(self, statsd_url: str, retry_after: int = 30, attempt: int = 1) -> None: """Callback used to retry a connection to statsd if it should fail.""" if attempt >= 10: log.error("Reached 10 attempts trying to reconnect AsyncStatsClient. Aborting") @@ -63,9 +59,9 @@ class Bot(commands.Bot): try: self.stats = AsyncStatsClient(self.loop, statsd_url, 8125, prefix="bot") except socket.gaierror: - log.warning(f"Statsd client failed to reconnect (Retry attempt: {attempt})") + log.warning(f"Statsd client failed to connect (Attempts: {attempt})") # Use a fallback strategy for retrying, up to 10 times. - self.loop.call_later(retry_after, self.retry_statsd_connection, statsd_url, retry_after * 2, attempt + 1) + self.loop.call_later(retry_after, self.retry_statsd_connection, statsd_url, retry_after ** 2, attempt + 1) async def cache_filter_list_data(self) -> None: """Cache all the data in the FilterList on the site.""" -- cgit v1.2.3 From 3e37bf88c86b0884b327d3eeb165b42860fa2fce Mon Sep 17 00:00:00 2001 From: Thomas Petersson Date: Mon, 5 Oct 2020 23:39:54 +0200 Subject: fix(bot): better fallback logic --- bot/bot.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bot/bot.py b/bot/bot.py index 06827c7e6..eee940637 100644 --- a/bot/bot.py +++ b/bot/bot.py @@ -50,9 +50,9 @@ class Bot(commands.Bot): self.stats = AsyncStatsClient(self.loop, LOCALHOST) self.connect_statsd(statsd_url) - def connect_statsd(self, statsd_url: str, retry_after: int = 30, attempt: int = 1) -> None: + def connect_statsd(self, statsd_url: str, retry_after: int = 2, attempt: int = 1) -> None: """Callback used to retry a connection to statsd if it should fail.""" - if attempt >= 10: + if attempt > 5: log.error("Reached 10 attempts trying to reconnect AsyncStatsClient. Aborting") return @@ -60,7 +60,7 @@ class Bot(commands.Bot): self.stats = AsyncStatsClient(self.loop, statsd_url, 8125, prefix="bot") except socket.gaierror: log.warning(f"Statsd client failed to connect (Attempts: {attempt})") - # Use a fallback strategy for retrying, up to 10 times. + # Use a fallback strategy for retrying, up to 5 times. self.loop.call_later(retry_after, self.retry_statsd_connection, statsd_url, retry_after ** 2, attempt + 1) async def cache_filter_list_data(self) -> None: -- cgit v1.2.3 From 382ad7708eb5dadff30a89da33f2fba9f53cd8c6 Mon Sep 17 00:00:00 2001 From: Xithrius Date: Wed, 4 Nov 2020 15:43:22 -0800 Subject: User command gets verification time and message count. --- bot/exts/info/information.py | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/bot/exts/info/information.py b/bot/exts/info/information.py index 5aaf85e5a..c83dfadc5 100644 --- a/bot/exts/info/information.py +++ b/bot/exts/info/information.py @@ -6,6 +6,7 @@ from collections import Counter, defaultdict from string import Template from typing import Any, Mapping, Optional, Tuple, Union +from dateutil import parser from discord import ChannelType, Colour, Embed, Guild, Message, Role, Status, utils from discord.abc import GuildChannel from discord.ext.commands import BucketType, Cog, Context, Paginator, command, group, has_any_role @@ -21,7 +22,6 @@ from bot.utils.time import time_since log = logging.getLogger(__name__) - STATUS_EMOTES = { Status.offline: constants.Emojis.status_offline, Status.dnd: constants.Emojis.status_dnd, @@ -254,6 +254,7 @@ class Information(Cog): if is_mod_channel(ctx.channel): fields.append(await self.expanded_user_infraction_counts(user)) fields.append(await self.user_nomination_counts(user)) + fields.append(await self.user_verification_and_messages(user)) else: fields.append(await self.basic_user_infraction_counts(user)) @@ -354,6 +355,25 @@ class Information(Cog): return "Nominations", "\n".join(output) + async def user_verification_and_messages(self, user: FetchedMember) -> Tuple[str, str]: + """Gets the time of verification and amount of messages for `member`.""" + user_activity = await self.bot.api_client.get(f'bot/users/{user.id}/metricity_data') + + activity_output = [] + + if user_activity['verified_at'] is not None: + verified_delta_formatted = time_since(parser.isoparse(user_activity['verified_at']), max_units=3) + activity_output.append(f'This user verified {verified_delta_formatted}') + else: + activity_output.append('This user is not verified.') + + if user_activity['total_messages']: + activity_output.append(f"This user has a total of {user_activity['total_messages']} messages.") + else: + activity_output.append(f"This user has not sent any messages on this server.") + + return "Activity", "\n".join(activity_output) + def format_fields(self, mapping: Mapping[str, Any], field_width: Optional[int] = None) -> str: """Format a mapping to be readable to a human.""" # sorting is technically superfluous but nice if you want to look for a specific field -- cgit v1.2.3 From e42cdaed973408c0753366401adb946e8402d082 Mon Sep 17 00:00:00 2001 From: Xithrius Date: Fri, 6 Nov 2020 18:12:23 -0800 Subject: Moved activity data further up in embed. --- bot/exts/info/information.py | 45 ++++++++++++++++++++++++++++++-------------- 1 file changed, 31 insertions(+), 14 deletions(-) diff --git a/bot/exts/info/information.py b/bot/exts/info/information.py index c83dfadc5..c4c73efdf 100644 --- a/bot/exts/info/information.py +++ b/bot/exts/info/information.py @@ -12,6 +12,7 @@ from discord.abc import GuildChannel from discord.ext.commands import BucketType, Cog, Context, Paginator, command, group, has_any_role from bot import constants +from bot.api import ResponseCodeError from bot.bot import Bot from bot.converters import FetchedMember from bot.decorators import in_whitelist @@ -235,14 +236,18 @@ class Information(Cog): roles = None membership = "The user is not a member of the server" + verified_at, activity = await self.user_verification_and_messages(user) + verified_at = f"Verified: {verified_at}" if is_mod_channel(ctx.channel) else "" + fields = [ ( "User information", textwrap.dedent(f""" Created: {created} + {verified_at} Profile: {user.mention} ID: {user.id} - """).strip() + """).strip().replace("\n\n", "\n") ), ( "Member information", @@ -252,9 +257,10 @@ class Information(Cog): # Show more verbose output in moderation channels for infractions and nominations if is_mod_channel(ctx.channel): + fields.append(activity) + fields.append(await self.expanded_user_infraction_counts(user)) fields.append(await self.user_nomination_counts(user)) - fields.append(await self.user_verification_and_messages(user)) else: fields.append(await self.basic_user_infraction_counts(user)) @@ -355,24 +361,35 @@ class Information(Cog): return "Nominations", "\n".join(output) - async def user_verification_and_messages(self, user: FetchedMember) -> Tuple[str, str]: + async def user_verification_and_messages(self, user: FetchedMember) -> Tuple[Union[bool, str], Tuple[str, str]]: """Gets the time of verification and amount of messages for `member`.""" - user_activity = await self.bot.api_client.get(f'bot/users/{user.id}/metricity_data') - activity_output = [] - if user_activity['verified_at'] is not None: - verified_delta_formatted = time_since(parser.isoparse(user_activity['verified_at']), max_units=3) - activity_output.append(f'This user verified {verified_delta_formatted}') + try: + user_activity = await self.bot.api_client.get(f'bot/users/{user.id}/metricity_data') + except ResponseCodeError as e: + verified_at = False + activity_output = f"{e.status}: No activity" else: - activity_output.append('This user is not verified.') + if user_activity['verified_at'] is not None: + verified_at = time_since(parser.isoparse(user_activity["verified_at"]), max_units=3) + else: + verified_at = "Not verified" - if user_activity['total_messages']: - activity_output.append(f"This user has a total of {user_activity['total_messages']} messages.") - else: - activity_output.append(f"This user has not sent any messages on this server.") + if user_activity["total_messages"]: + activity_output.append(user_activity['total_messages']) + else: + activity_output.append("No messages") + + if user_activity["activity_blocks"]: + activity_output.append(user_activity["activity_blocks"]) + else: + activity_output.append("No activity") + + activity_output = "\n".join( + f"{name}: {metric}" for name, metric in zip(["Messages", "Activity blocks"], activity_output)) - return "Activity", "\n".join(activity_output) + return verified_at, ("Activity", activity_output) def format_fields(self, mapping: Mapping[str, Any], field_width: Optional[int] = None) -> str: """Format a mapping to be readable to a human.""" -- cgit v1.2.3 From fca8b814df974b4c30e14a72d48681da77259899 Mon Sep 17 00:00:00 2001 From: Thomas Petersson Date: Mon, 9 Nov 2020 18:15:00 +0100 Subject: fix(bot): statds pr review suggestions --- bot/bot.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/bot/bot.py b/bot/bot.py index eee940637..b097513f1 100644 --- a/bot/bot.py +++ b/bot/bot.py @@ -37,6 +37,7 @@ class Bot(commands.Bot): self._connector = None self._resolver = None + self._statsd_timerhandle: asyncio.TimerHandle = None self._guild_available = asyncio.Event() statsd_url = constants.Stats.statsd_host @@ -48,20 +49,24 @@ class Bot(commands.Bot): statsd_url = LOCALHOST self.stats = AsyncStatsClient(self.loop, LOCALHOST) - self.connect_statsd(statsd_url) + self._connect_statsd(statsd_url) - def connect_statsd(self, statsd_url: str, retry_after: int = 2, attempt: int = 1) -> None: + def _connect_statsd(self, statsd_url: str, retry_after: int = 2, attempt: int = 1) -> None: """Callback used to retry a connection to statsd if it should fail.""" - if attempt > 5: - log.error("Reached 10 attempts trying to reconnect AsyncStatsClient. Aborting") + if self._statsd_timerhandle and not self._statsd_timerhandle.cancelled: + self._statsd_timerhandle.cancel() + + if attempt >= 5: + log.error("Reached 5 attempts trying to reconnect AsyncStatsClient. Aborting") return try: self.stats = AsyncStatsClient(self.loop, statsd_url, 8125, prefix="bot") except socket.gaierror: - log.warning(f"Statsd client failed to connect (Attempts: {attempt})") + log.warning(f"Statsd client failed to connect (Attempt(s): {attempt})") # Use a fallback strategy for retrying, up to 5 times. - self.loop.call_later(retry_after, self.retry_statsd_connection, statsd_url, retry_after ** 2, attempt + 1) + self._statsd_timerhandle = self.loop.call_later( + retry_after, self._connect_statsd, statsd_url, retry_after * 5, attempt + 1) async def cache_filter_list_data(self) -> None: """Cache all the data in the FilterList on the site.""" @@ -167,6 +172,9 @@ class Bot(commands.Bot): if self.redis_session: await self.redis_session.close() + if self._statsd_timerhandle and not self._statsd_timerhandle.cancelled: + self._statsd_timerhandle.cancel() + def insert_item_into_filter_list_cache(self, item: Dict[str, str]) -> None: """Add an item to the bots filter_list_cache.""" type_ = item["type"] -- cgit v1.2.3 From 097298e260f0d1d84a8442e5c267042424314f3e Mon Sep 17 00:00:00 2001 From: Xithrius Date: Wed, 11 Nov 2020 18:09:30 -0800 Subject: Changed logic of membership info creation. --- bot/exts/info/information.py | 34 ++++++++++++++++++++-------------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/bot/exts/info/information.py b/bot/exts/info/information.py index c4c73efdf..a8adb817b 100644 --- a/bot/exts/info/information.py +++ b/bot/exts/info/information.py @@ -225,29 +225,34 @@ class Information(Cog): if is_set and (emoji := getattr(constants.Emojis, f"badge_{badge}", None)): badges.append(emoji) + verified_at, activity = await self.user_verification_and_messages(user) + if on_server: joined = time_since(user.joined_at, max_units=3) roles = ", ".join(role.mention for role in user.roles[1:]) - membership = textwrap.dedent(f""" - Joined: {joined} - Roles: {roles or None} - """).strip() + if is_mod_channel(ctx.channel): + membership = textwrap.dedent(f""" + Joined: {joined} + Verified: {verified_at} + Roles: {roles or None} + """).strip() + else: + membership = textwrap.dedent(f""" + Joined: {joined} + Roles: {roles or None} + """).strip() else: roles = None membership = "The user is not a member of the server" - verified_at, activity = await self.user_verification_and_messages(user) - verified_at = f"Verified: {verified_at}" if is_mod_channel(ctx.channel) else "" - fields = [ ( "User information", textwrap.dedent(f""" Created: {created} - {verified_at} Profile: {user.mention} ID: {user.id} - """).strip().replace("\n\n", "\n") + """).strip() ), ( "Member information", @@ -364,17 +369,18 @@ class Information(Cog): async def user_verification_and_messages(self, user: FetchedMember) -> Tuple[Union[bool, str], Tuple[str, str]]: """Gets the time of verification and amount of messages for `member`.""" activity_output = [] + verified_at = False try: user_activity = await self.bot.api_client.get(f'bot/users/{user.id}/metricity_data') except ResponseCodeError as e: - verified_at = False - activity_output = f"{e.status}: No activity" + if e.status == 404: + activity_output = "No activity" + else: - if user_activity['verified_at'] is not None: + verified_at = user_activity['verified_at'] + if verified_at is not None: verified_at = time_since(parser.isoparse(user_activity["verified_at"]), max_units=3) - else: - verified_at = "Not verified" if user_activity["total_messages"]: activity_output.append(user_activity['total_messages']) -- cgit v1.2.3 From 51c4ecd2b5b0afedcdfcf2d3c85100a312720a09 Mon Sep 17 00:00:00 2001 From: Matteo Bertucci Date: Thu, 12 Nov 2020 09:09:58 +0100 Subject: Remove selenium from the element list This could lead to some confusion with the users believing that this channel is reserved to help related to the selenium tool. --- bot/resources/elements.json | 1 - 1 file changed, 1 deletion(-) diff --git a/bot/resources/elements.json b/bot/resources/elements.json index 2dc9b6fd6..a3ac5b99f 100644 --- a/bot/resources/elements.json +++ b/bot/resources/elements.json @@ -32,7 +32,6 @@ "gallium", "germanium", "arsenic", - "selenium", "bromine", "krypton", "rubidium", -- cgit v1.2.3 From 8ea13768378deadef6e666ed40ed88ff8a08e16d Mon Sep 17 00:00:00 2001 From: Steele Date: Fri, 20 Nov 2020 22:22:10 -0500 Subject: `!close` removes the cooldown role from the claimant even when invoked by someone else; flattened `close_command` --- bot/exts/help_channels.py | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/bot/exts/help_channels.py b/bot/exts/help_channels.py index f5a8b251b..e50fab7fc 100644 --- a/bot/exts/help_channels.py +++ b/bot/exts/help_channels.py @@ -213,18 +213,23 @@ class HelpChannels(commands.Cog): and reset the send permissions cooldown for the user who started the session. """ log.trace("close command invoked; checking if the channel is in-use.") - if ctx.channel.category == self.in_use_category: - if await self.dormant_check(ctx): - await self.remove_cooldown_role(ctx.author) + if ctx.channel.category != self.in_use_category: + log.debug(f"{ctx.author} invoked command 'dormant' outside an in-use help channel") + return - # Ignore missing task when cooldown has passed but the channel still isn't dormant. - if ctx.author.id in self.scheduler: - self.scheduler.cancel(ctx.author.id) + if not await self.dormant_check(ctx): + return - await self.move_to_dormant(ctx.channel, "command") - self.scheduler.cancel(ctx.channel.id) - else: - log.debug(f"{ctx.author} invoked command 'dormant' outside an in-use help channel") + guild = self.bot.get_guild(constants.Guild.id) + claimant = guild.get_member(await self.help_channel_claimants.get(ctx.channel.id)) + await self.move_to_dormant(ctx.channel, "command") + await self.remove_cooldown_role(claimant) + + # Ignore missing task when cooldown has passed but the channel still isn't dormant. + if ctx.author.id in self.scheduler: + self.scheduler.cancel(ctx.author.id) + + self.scheduler.cancel(ctx.channel.id) async def get_available_candidate(self) -> discord.TextChannel: """ -- cgit v1.2.3 From 089efa35345af32c8f5475bb49bd09b9cd3f06c3 Mon Sep 17 00:00:00 2001 From: Steele Date: Wed, 25 Nov 2020 16:09:40 -0500 Subject: `!close` removes role when they have no help channels left; needs to be fixed so role is removed when the channel times out --- bot/exts/help_channels.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/bot/exts/help_channels.py b/bot/exts/help_channels.py index e50fab7fc..e1d28ece3 100644 --- a/bot/exts/help_channels.py +++ b/bot/exts/help_channels.py @@ -223,7 +223,10 @@ class HelpChannels(commands.Cog): guild = self.bot.get_guild(constants.Guild.id) claimant = guild.get_member(await self.help_channel_claimants.get(ctx.channel.id)) await self.move_to_dormant(ctx.channel, "command") - await self.remove_cooldown_role(claimant) + + # Remove the cooldown role if they have no other channels left + if claimant.id not in {user_id for _, user_id in await self.help_channel_claimants.items()}: + await self.remove_cooldown_role(claimant) # Ignore missing task when cooldown has passed but the channel still isn't dormant. if ctx.author.id in self.scheduler: @@ -413,6 +416,8 @@ class HelpChannels(commands.Cog): for channel in self.get_category_channels(self.in_use_category): await self.move_idle_channel(channel, has_task=False) + log.trace(f'Initial state of help_channel_claimants: {await self.help_channel_claimants.items()}') + # Prevent the command from being used until ready. # The ready event wasn't used because channels could change categories between the time # the command is invoked and the cog is ready (e.g. if move_idle_channel wasn't called yet). -- cgit v1.2.3 From 731fea162705583e1ee6edeb5da270b628a018d5 Mon Sep 17 00:00:00 2001 From: Steele Date: Wed, 25 Nov 2020 16:44:37 -0500 Subject: Moved the removal of the cooldown role from `close_command` to `move_to_dormant` --- bot/exts/help_channels.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/bot/exts/help_channels.py b/bot/exts/help_channels.py index e1d28ece3..4fd4896df 100644 --- a/bot/exts/help_channels.py +++ b/bot/exts/help_channels.py @@ -220,14 +220,8 @@ class HelpChannels(commands.Cog): if not await self.dormant_check(ctx): return - guild = self.bot.get_guild(constants.Guild.id) - claimant = guild.get_member(await self.help_channel_claimants.get(ctx.channel.id)) await self.move_to_dormant(ctx.channel, "command") - # Remove the cooldown role if they have no other channels left - if claimant.id not in {user_id for _, user_id in await self.help_channel_claimants.items()}: - await self.remove_cooldown_role(claimant) - # Ignore missing task when cooldown has passed but the channel still isn't dormant. if ctx.author.id in self.scheduler: self.scheduler.cancel(ctx.author.id) @@ -552,18 +546,25 @@ class HelpChannels(commands.Cog): async def move_to_dormant(self, channel: discord.TextChannel, caller: str) -> None: """ - Make the `channel` dormant. + Make the `channel` dormant and remove the help cooldown role if it was the claimant's only channel. A caller argument is provided for metrics. """ log.info(f"Moving #{channel} ({channel.id}) to the Dormant category.") + guild = self.bot.get_guild(constants.Guild.id) + claimant = guild.get_member(await self.help_channel_claimants.get(channel.id)) + await self.help_channel_claimants.delete(channel.id) await self.move_to_bottom_position( channel=channel, category_id=constants.Categories.help_dormant, ) + # Remove the cooldown role if the claimant has no other channels left + if claimant.id not in {user_id for _, user_id in await self.help_channel_claimants.items()}: + await self.remove_cooldown_role(claimant) + self.bot.stats.incr(f"help.dormant_calls.{caller}") in_use_time = await self.get_in_use_time(channel.id) -- cgit v1.2.3 From 3f490ab413b64474bc7e40a2d66d3c3178d615ba Mon Sep 17 00:00:00 2001 From: Steele Date: Thu, 26 Nov 2020 13:34:03 -0500 Subject: Changes requested by @MarkKoz, new `unclaim_channel` method Deleted expensive logging operation; moved cooldown role removal functionality to new `unclaim_channel` method; handle possibility that claimant has left the guild; optimized redis cache iteration with `any` --- bot/exts/help_channels.py | 46 ++++++++++++++++++++++++++-------------------- 1 file changed, 26 insertions(+), 20 deletions(-) diff --git a/bot/exts/help_channels.py b/bot/exts/help_channels.py index 5676728e9..25ca67d47 100644 --- a/bot/exts/help_channels.py +++ b/bot/exts/help_channels.py @@ -221,16 +221,9 @@ class HelpChannels(commands.Cog): log.debug(f"{ctx.author} invoked command 'dormant' outside an in-use help channel") return - if not await self.dormant_check(ctx): - return - - await self.move_to_dormant(ctx.channel, "command") - - # Ignore missing task when cooldown has passed but the channel still isn't dormant. - if ctx.author.id in self.scheduler: - self.scheduler.cancel(ctx.author.id) - - self.scheduler.cancel(ctx.channel.id) + if await self.dormant_check(ctx): + await self.move_to_dormant(ctx.channel, "command") + self.scheduler.cancel(ctx.channel.id) async def get_available_candidate(self) -> discord.TextChannel: """ @@ -414,8 +407,6 @@ class HelpChannels(commands.Cog): for channel in self.get_category_channels(self.in_use_category): await self.move_idle_channel(channel, has_task=False) - log.trace(f'Initial state of help_channel_claimants: {await self.help_channel_claimants.items()}') - # Prevent the command from being used until ready. # The ready event wasn't used because channels could change categories between the time # the command is invoked and the cog is ready (e.g. if move_idle_channel wasn't called yet). @@ -550,24 +541,18 @@ class HelpChannels(commands.Cog): async def move_to_dormant(self, channel: discord.TextChannel, caller: str) -> None: """ - Make the `channel` dormant and remove the help cooldown role if it was the claimant's only channel. + Make the `channel` dormant. A caller argument is provided for metrics. """ log.info(f"Moving #{channel} ({channel.id}) to the Dormant category.") - guild = self.bot.get_guild(constants.Guild.id) - claimant = guild.get_member(await self.help_channel_claimants.get(channel.id)) - - await self.help_channel_claimants.delete(channel.id) await self.move_to_bottom_position( channel=channel, category_id=constants.Categories.help_dormant, ) - # Remove the cooldown role if the claimant has no other channels left - if claimant.id not in {user_id for _, user_id in await self.help_channel_claimants.items()}: - await self.remove_cooldown_role(claimant) + await self.unclaim_channel(channel) self.bot.stats.incr(f"help.dormant_calls.{caller}") @@ -592,6 +577,27 @@ class HelpChannels(commands.Cog): self.channel_queue.put_nowait(channel) self.report_stats() + async def unclaim_channel(self, channel: discord.TextChannel) -> None: + """ + Deletes `channel` from the mapping of channels to claimants and removes the help cooldown + role from the claimant if it was their only channel + """ + claimant_id = await self.help_channel_claimants.pop(channel.id) + + # Ignore missing task when cooldown has passed but the channel still isn't dormant. + if claimant_id in self.scheduler: + self.scheduler.cancel(claimant_id) + + claimant = self.bot.get_guild(constants.Guild.id).get_member(claimant_id) + + if claimant is None: + # `claimant` has left the guild, so the cooldown role need not be removed + return + + # Remove the cooldown role if the claimant has no other channels left + if not any(claimant.id == user_id for _, user_id in await self.help_channel_claimants.items()): + await self.remove_cooldown_role(claimant) + async def move_to_in_use(self, channel: discord.TextChannel) -> None: """Make a channel in-use and schedule it to be made dormant.""" log.info(f"Moving #{channel} ({channel.id}) to the In Use category.") -- cgit v1.2.3 From 1e1d57f6294eb7c3a8d9a0c76c77eb10c43b3ebe Mon Sep 17 00:00:00 2001 From: Thomas Petersson Date: Fri, 27 Nov 2020 16:00:19 +0100 Subject: fix(bot): PR reivew of bot.py --- bot/bot.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/bot/bot.py b/bot/bot.py index b097513f1..bcce4a118 100644 --- a/bot/bot.py +++ b/bot/bot.py @@ -53,20 +53,22 @@ class Bot(commands.Bot): def _connect_statsd(self, statsd_url: str, retry_after: int = 2, attempt: int = 1) -> None: """Callback used to retry a connection to statsd if it should fail.""" - if self._statsd_timerhandle and not self._statsd_timerhandle.cancelled: - self._statsd_timerhandle.cancel() - - if attempt >= 5: - log.error("Reached 5 attempts trying to reconnect AsyncStatsClient. Aborting") + if attempt >= 8: + log.error("Reached 8 attempts trying to reconnect AsyncStatsClient. Aborting") return try: self.stats = AsyncStatsClient(self.loop, statsd_url, 8125, prefix="bot") except socket.gaierror: log.warning(f"Statsd client failed to connect (Attempt(s): {attempt})") - # Use a fallback strategy for retrying, up to 5 times. + # Use a fallback strategy for retrying, up to 8 times. self._statsd_timerhandle = self.loop.call_later( - retry_after, self._connect_statsd, statsd_url, retry_after * 5, attempt + 1) + retry_after, + self._connect_statsd, + statsd_url, + retry_after * 2, + attempt + 1 + ) async def cache_filter_list_data(self) -> None: """Cache all the data in the FilterList on the site.""" @@ -172,7 +174,7 @@ class Bot(commands.Bot): if self.redis_session: await self.redis_session.close() - if self._statsd_timerhandle and not self._statsd_timerhandle.cancelled: + if self._statsd_timerhandle: self._statsd_timerhandle.cancel() def insert_item_into_filter_list_cache(self, item: Dict[str, str]) -> None: -- cgit v1.2.3 From 6ac6786c480ec9919009acca4906a52234f42285 Mon Sep 17 00:00:00 2001 From: Steele Date: Sun, 29 Nov 2020 13:04:38 -0500 Subject: `!close` removes role from claimant only, new method `unclaim_channel`. Previously `!close` would remove the cooldown role from the person who issued the command, whereas now `unclaim_channel` handles removing the role from the claimant if it was their only channel open. --- bot/exts/help_channels/_cog.py | 42 +++++++++++++++++++++++++++++------------- 1 file changed, 29 insertions(+), 13 deletions(-) diff --git a/bot/exts/help_channels/_cog.py b/bot/exts/help_channels/_cog.py index e22d4663e..86eb91b02 100644 --- a/bot/exts/help_channels/_cog.py +++ b/bot/exts/help_channels/_cog.py @@ -145,22 +145,17 @@ class HelpChannels(commands.Cog): Make the current in-use help channel dormant. Make the channel dormant if the user passes the `dormant_check`, - delete the message that invoked this, - and reset the send permissions cooldown for the user who started the session. + delete the message that invoked this. """ log.trace("close command invoked; checking if the channel is in-use.") - if ctx.channel.category == self.in_use_category: - if await self.dormant_check(ctx): - await _cooldown.remove_cooldown_role(ctx.author) - # Ignore missing task when cooldown has passed but the channel still isn't dormant. - if ctx.author.id in self.scheduler: - self.scheduler.cancel(ctx.author.id) - - await self.move_to_dormant(ctx.channel, "command") - self.scheduler.cancel(ctx.channel.id) - else: + if ctx.channel.category != self.in_use_category: log.debug(f"{ctx.author} invoked command 'dormant' outside an in-use help channel") + return + + if await self.dormant_check(ctx): + await self.move_to_dormant(ctx.channel, "command") + self.scheduler.cancel(ctx.channel.id) async def get_available_candidate(self) -> discord.TextChannel: """ @@ -368,12 +363,13 @@ class HelpChannels(commands.Cog): """ log.info(f"Moving #{channel} ({channel.id}) to the Dormant category.") - await _caches.claimants.delete(channel.id) await self.move_to_bottom_position( channel=channel, category_id=constants.Categories.help_dormant, ) + await self.unclaim_channel(channel) + self.bot.stats.incr(f"help.dormant_calls.{caller}") in_use_time = await _channel.get_in_use_time(channel.id) @@ -397,6 +393,26 @@ class HelpChannels(commands.Cog): self.channel_queue.put_nowait(channel) self.report_stats() + async def unclaim_channel(self, channel: discord.TextChannel) -> None: + """ + Remove the claimant from the claimant cache and remove the cooldown role + if it was their last open help channel. + """ + claimant_id = await _caches.claimants.pop(channel.id) + + # Ignore missing task when cooldown has passed but the channel still isn't dormant. + if claimant_id in self.scheduler: + self.scheduler.cancel(claimant_id) + + claimant = self.bot.get_guild(constants.Guild.id).get_member(claimant_id) + if claimant is None: + log.info(f"{claimant_id} left the guild during their help session; the cooldown role won't be removed") + return + + # Remove the cooldown role if the claimant has no other channels left + if not any(claimant.id == user_id for _, user_id in await _caches.claimants.items()): + await _cooldown.remove_cooldown_role(claimant) + async def move_to_in_use(self, channel: discord.TextChannel) -> None: """Make a channel in-use and schedule it to be made dormant.""" log.info(f"Moving #{channel} ({channel.id}) to the In Use category.") -- cgit v1.2.3 From 15593de0a1a503a39aa29031061bc17ac26e4230 Mon Sep 17 00:00:00 2001 From: Steele Date: Sun, 29 Nov 2020 14:15:19 -0500 Subject: Corrected `unclaim_channel` docstring to comply with style guide --- bot/exts/help_channels/_cog.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/bot/exts/help_channels/_cog.py b/bot/exts/help_channels/_cog.py index 86eb91b02..983c5d183 100644 --- a/bot/exts/help_channels/_cog.py +++ b/bot/exts/help_channels/_cog.py @@ -395,8 +395,10 @@ class HelpChannels(commands.Cog): async def unclaim_channel(self, channel: discord.TextChannel) -> None: """ - Remove the claimant from the claimant cache and remove the cooldown role - if it was their last open help channel. + Mark the channel as unclaimed and remove the cooldown role from the claimant if needed. + + The role is only removed if they have no claimed channels left once the current one is unclaimed. + This method also handles canceling the automatic removal of the cooldown role. """ claimant_id = await _caches.claimants.pop(channel.id) -- cgit v1.2.3 From 9e4b78b40b407c2bb6d4666767d700b7993a54e5 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Fri, 4 Dec 2020 14:46:50 +0200 Subject: Create command for showing Discord snowflake creation time --- bot/exts/utils/utils.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/bot/exts/utils/utils.py b/bot/exts/utils/utils.py index 6d8d98695..3f16bc10b 100644 --- a/bot/exts/utils/utils.py +++ b/bot/exts/utils/utils.py @@ -9,6 +9,7 @@ from typing import Dict, Optional, Tuple, Union from discord import Colour, Embed, utils from discord.ext.commands import BadArgument, Cog, Context, clean_content, command, has_any_role +from discord.utils import snowflake_time from bot.bot import Bot from bot.constants import Channels, MODERATION_ROLES, STAFF_ROLES @@ -16,6 +17,7 @@ from bot.decorators import in_whitelist from bot.pagination import LinePaginator from bot.utils import messages from bot.utils.cache import AsyncCache +from bot.utils.time import time_since log = logging.getLogger(__name__) @@ -166,6 +168,21 @@ class Utils(Cog): embed.description = best_match await ctx.send(embed=embed) + @command(aliases=("snf", "snfl")) + @in_whitelist(channels=(Channels.bot_commands,), roles=STAFF_ROLES) + async def snowflake(self, ctx: Context, snowflake: int) -> None: + """Get Discord snowflake creation time.""" + 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) + @command(aliases=("poll",)) @has_any_role(*MODERATION_ROLES) async def vote(self, ctx: Context, title: clean_content(fix_channel_mentions=True), *options: str) -> None: -- cgit v1.2.3 From 72f869a81d882acf2eb3f1714d4f52d01384b0ae Mon Sep 17 00:00:00 2001 From: Matteo Bertucci Date: Fri, 4 Dec 2020 14:46:58 +0100 Subject: Add the `s` alias to `infraction search` --- bot/exts/moderation/infraction/management.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/moderation/infraction/management.py b/bot/exts/moderation/infraction/management.py index c58410f8c..b3783cd60 100644 --- a/bot/exts/moderation/infraction/management.py +++ b/bot/exts/moderation/infraction/management.py @@ -197,7 +197,7 @@ class ModManagement(commands.Cog): # endregion # region: Search infractions - @infraction_group.group(name="search", invoke_without_command=True) + @infraction_group.group(name="search", aliases=('s',), invoke_without_command=True) async def infraction_search_group(self, ctx: Context, query: t.Union[UserMention, Snowflake, str]) -> None: """Searches for infractions in the database.""" if isinstance(query, int): -- cgit v1.2.3 From f537768034a2c9791ca08a91c66b9f97aef8edca Mon Sep 17 00:00:00 2001 From: Steele Date: Sat, 5 Dec 2020 12:08:44 -0500 Subject: Bot relays the infraction reason in the DM. Previously, the infraction DM from the bot gave a formulaic message about the nickname policy. It now gives a slightly different message along with the reason given by the mod. This means that the message the user gets and the infraction reason that gets recorded are now the same. --- bot/exts/moderation/infraction/superstarify.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/bot/exts/moderation/infraction/superstarify.py b/bot/exts/moderation/infraction/superstarify.py index 96dfb562f..a4327fb95 100644 --- a/bot/exts/moderation/infraction/superstarify.py +++ b/bot/exts/moderation/infraction/superstarify.py @@ -111,7 +111,7 @@ class Superstarify(InfractionScheduler, Cog): member: Member, duration: Expiry, *, - reason: str = None, + reason: str = '', ) -> None: """ Temporarily force a random superstar name (like Taylor Swift) to be the user's nickname. @@ -128,15 +128,16 @@ class Superstarify(InfractionScheduler, Cog): Alternatively, an ISO 8601 timestamp can be provided for the duration. - An optional reason can be provided. If no reason is given, the original name will be shown - in a generated reason. + An optional reason can be provided, which would be added to a message stating their old nickname + and linking to the nickname policy. """ if await _utils.get_active_infraction(ctx, member, "superstar"): return # Post the infraction to the API old_nick = member.display_name - reason = reason or f"old nick: {old_nick}" + reason = (f"Nickname '{old_nick}' does not comply with our [nickname policy]({NICKNAME_POLICY_URL}). " + f"{reason}") infraction = await _utils.post_infraction(ctx, member, "superstar", reason, duration, active=True) id_ = infraction["id"] @@ -152,7 +153,6 @@ class Superstarify(InfractionScheduler, Cog): old_nick = escape_markdown(old_nick) forced_nick = escape_markdown(forced_nick) - superstar_reason = f"Your nickname didn't comply with our [nickname policy]({NICKNAME_POLICY_URL})." nickname_info = textwrap.dedent(f""" Old nickname: `{old_nick}` New nickname: `{forced_nick}` @@ -160,7 +160,7 @@ class Superstarify(InfractionScheduler, Cog): successful = await self.apply_infraction( ctx, infraction, member, action(), - user_reason=superstar_reason, + user_reason=reason, additional_info=nickname_info ) -- cgit v1.2.3 From d7e94f2570c69ae04c32bc4bad338b1be0c1da26 Mon Sep 17 00:00:00 2001 From: Steele Date: Sat, 5 Dec 2020 12:09:54 -0500 Subject: Add `starify` and `unstarify` as command aliases. --- 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 a4327fb95..e7d1c4da8 100644 --- a/bot/exts/moderation/infraction/superstarify.py +++ b/bot/exts/moderation/infraction/superstarify.py @@ -104,7 +104,7 @@ class Superstarify(InfractionScheduler, Cog): await self.reapply_infraction(infraction, action) - @command(name="superstarify", aliases=("force_nick", "star")) + @command(name="superstarify", aliases=("force_nick", "star", "starify")) async def superstarify( self, ctx: Context, @@ -182,7 +182,7 @@ class Superstarify(InfractionScheduler, Cog): ) await ctx.send(embed=embed) - @command(name="unsuperstarify", aliases=("release_nick", "unstar")) + @command(name="unsuperstarify", aliases=("release_nick", "unstar", "unstarify")) async def unsuperstarify(self, ctx: Context, member: Member) -> None: """Remove the superstarify infraction and allow the user to change their nickname.""" await self.pardon_infraction(ctx, "superstar", member) -- cgit v1.2.3 From e08c39238dabe40abca7ae4eaed6873e26fd051f Mon Sep 17 00:00:00 2001 From: Joe Banks Date: Sun, 6 Dec 2020 14:15:34 +0000 Subject: Create review-policy.yml --- .github/review-policy.yml | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .github/review-policy.yml diff --git a/.github/review-policy.yml b/.github/review-policy.yml new file mode 100644 index 000000000..421b30f8a --- /dev/null +++ b/.github/review-policy.yml @@ -0,0 +1,3 @@ +remote: python-discord/.github +path: review-policies/core-developers.yml +ref: main -- cgit v1.2.3 From 0f66fe3040d70de51ece1aa0de38a88b20000221 Mon Sep 17 00:00:00 2001 From: Steele Date: Sun, 6 Dec 2020 11:16:56 -0500 Subject: User gets a more detailed message from the bot Whereas one of my previous commits makes the message the user gets and the infraction that gets recorded the same, the recorded infraction is now shorter, but the message the user gets is more similar to the embed posted in the public channel. We also softened the language of the user-facing message a bit. --- bot/exts/moderation/infraction/superstarify.py | 28 +++++++++++++++----------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/bot/exts/moderation/infraction/superstarify.py b/bot/exts/moderation/infraction/superstarify.py index e7d1c4da8..1d512a4c7 100644 --- a/bot/exts/moderation/infraction/superstarify.py +++ b/bot/exts/moderation/infraction/superstarify.py @@ -136,9 +136,8 @@ class Superstarify(InfractionScheduler, Cog): # Post the infraction to the API old_nick = member.display_name - reason = (f"Nickname '{old_nick}' does not comply with our [nickname policy]({NICKNAME_POLICY_URL}). " - f"{reason}") - infraction = await _utils.post_infraction(ctx, member, "superstar", reason, duration, active=True) + infraction_reason = f'Old nickname: {old_nick}. {reason}' + infraction = await _utils.post_infraction(ctx, member, "superstar", infraction_reason, duration, active=True) id_ = infraction["id"] forced_nick = self.get_nick(id_, member.id) @@ -158,9 +157,21 @@ class Superstarify(InfractionScheduler, Cog): New nickname: `{forced_nick}` """).strip() + formatted_reason = f'**Additional details:** {reason}\n\n' if reason else '' + + embed_reason = ( + f"Your previous nickname, **{old_nick}**, " + f"didn't comply with our nickname policy. " + f"Your new nickname will be **{forced_nick}**.\n\n" + f"{formatted_reason}" + f"You will be unable to change your nickname until **{expiry_str}**. " + "If you're confused by this, please read our " + f"[official nickname policy]({NICKNAME_POLICY_URL})." + ) + successful = await self.apply_infraction( ctx, infraction, member, action(), - user_reason=reason, + user_reason=embed_reason, additional_info=nickname_info ) @@ -171,14 +182,7 @@ class Superstarify(InfractionScheduler, Cog): embed = Embed( title="Congratulations!", colour=constants.Colours.soft_orange, - description=( - f"Your previous nickname, **{old_nick}**, " - f"was so bad that we have decided to change it. " - f"Your new nickname will be **{forced_nick}**.\n\n" - f"You will be unable to change your nickname until **{expiry_str}**.\n\n" - "If you're confused by this, please read our " - f"[official nickname policy]({NICKNAME_POLICY_URL})." - ) + description=embed_reason ) await ctx.send(embed=embed) -- cgit v1.2.3 From 345ee39e7cc5449e563817c4f30895638c66c206 Mon Sep 17 00:00:00 2001 From: Dennis Pham Date: Sun, 6 Dec 2020 13:29:35 -0500 Subject: Update CODEOWNERS for @Den4200 --- .github/CODEOWNERS | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 642676078..73e303325 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,6 +1,3 @@ -# Request Dennis for any PR -* @Den4200 - # Extensions **/bot/exts/backend/sync/** @MarkKoz **/bot/exts/filters/*token_remover.py @MarkKoz @@ -9,8 +6,8 @@ bot/exts/info/codeblock/** @MarkKoz bot/exts/utils/extensions.py @MarkKoz bot/exts/utils/snekbox.py @MarkKoz @Akarys42 bot/exts/help_channels/** @MarkKoz @Akarys42 -bot/exts/moderation/** @Akarys42 @mbaruh -bot/exts/info/** @Akarys42 @mbaruh +bot/exts/moderation/** @Akarys42 @mbaruh @Den4200 +bot/exts/info/** @Akarys42 @mbaruh @Den4200 bot/exts/filters/** @mbaruh # Utils @@ -26,9 +23,9 @@ tests/bot/exts/test_cogs.py @MarkKoz tests/** @Akarys42 # CI & Docker -.github/workflows/** @MarkKoz @Akarys42 @SebastiaanZ -Dockerfile @MarkKoz @Akarys42 -docker-compose.yml @MarkKoz @Akarys42 +.github/workflows/** @MarkKoz @Akarys42 @SebastiaanZ @Den4200 +Dockerfile @MarkKoz @Akarys42 @Den4200 +docker-compose.yml @MarkKoz @Akarys42 @Den4200 # Tools Pipfile* @Akarys42 -- cgit v1.2.3 From 032b64f625d9d16f532ba0e895a412bc24ee9659 Mon Sep 17 00:00:00 2001 From: Steele Date: Mon, 7 Dec 2020 11:04:38 -0500 Subject: Use the original wording of the public embed, but change the title to "Superstarified!" Per internal staff discussion, we'll keep the wording of the message. --- bot/exts/moderation/infraction/superstarify.py | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/bot/exts/moderation/infraction/superstarify.py b/bot/exts/moderation/infraction/superstarify.py index 1d512a4c7..ffc470c54 100644 --- a/bot/exts/moderation/infraction/superstarify.py +++ b/bot/exts/moderation/infraction/superstarify.py @@ -157,32 +157,29 @@ class Superstarify(InfractionScheduler, Cog): New nickname: `{forced_nick}` """).strip() - formatted_reason = f'**Additional details:** {reason}\n\n' if reason else '' - - embed_reason = ( + user_message = ( f"Your previous nickname, **{old_nick}**, " - f"didn't comply with our nickname policy. " + f"was so bad that we have decided to change it. " f"Your new nickname will be **{forced_nick}**.\n\n" - f"{formatted_reason}" + "{reason}" f"You will be unable to change your nickname until **{expiry_str}**. " "If you're confused by this, please read our " f"[official nickname policy]({NICKNAME_POLICY_URL})." - ) + ).format successful = await self.apply_infraction( ctx, infraction, member, action(), - user_reason=embed_reason, + user_reason=user_message(reason=f'**Additional details:** {reason}\n\n' if reason else ''), additional_info=nickname_info ) - # Send an embed with the infraction information to the invoking context if - # superstar was successful. + # Send an embed with to the invoking context if superstar was successful. if successful: log.trace(f"Sending superstar #{id_} embed.") embed = Embed( - title="Congratulations!", + title="Superstarified!", colour=constants.Colours.soft_orange, - description=embed_reason + description=user_message(reason='') ) await ctx.send(embed=embed) -- cgit v1.2.3 From 9d00ef35afd5b64c070003a7941cc38d98bdf9cc Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Wed, 9 Dec 2020 07:56:21 +0200 Subject: Add sf alias to snowflake command --- bot/exts/utils/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/utils/utils.py b/bot/exts/utils/utils.py index 3f16bc10b..87abbe4de 100644 --- a/bot/exts/utils/utils.py +++ b/bot/exts/utils/utils.py @@ -168,7 +168,7 @@ class Utils(Cog): embed.description = best_match await ctx.send(embed=embed) - @command(aliases=("snf", "snfl")) + @command(aliases=("snf", "snfl", "sf")) @in_whitelist(channels=(Channels.bot_commands,), roles=STAFF_ROLES) async def snowflake(self, ctx: Context, snowflake: int) -> None: """Get Discord snowflake creation time.""" -- cgit v1.2.3 From e0335bbb3fe1c35259647be2e23fb09fb2b09284 Mon Sep 17 00:00:00 2001 From: Steele Date: Wed, 9 Dec 2020 19:58:25 -0500 Subject: Create Verify cog for new `!verify` command. `!verify` command allows moderators to apply the Developer role to a user. `!verify` is therefore removed as an alias for `!accept`. --- bot/exts/moderation/verification.py | 2 +- bot/exts/moderation/verify.py | 45 +++++++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 bot/exts/moderation/verify.py diff --git a/bot/exts/moderation/verification.py b/bot/exts/moderation/verification.py index c599156d0..b1c94185a 100644 --- a/bot/exts/moderation/verification.py +++ b/bot/exts/moderation/verification.py @@ -756,7 +756,7 @@ class Verification(Cog): log.trace(f"Bumping verification stats in category: {category}") self.bot.stats.incr(f"verification.{category}") - @command(name='accept', aliases=('verify', 'verified', 'accepted'), hidden=True) + @command(name='accept', aliases=('verified', 'accepted'), hidden=True) @has_no_roles(constants.Roles.verified) @in_whitelist(channels=(constants.Channels.verification,)) async def accept_command(self, ctx: Context, *_) -> None: # We don't actually care about the args diff --git a/bot/exts/moderation/verify.py b/bot/exts/moderation/verify.py new file mode 100644 index 000000000..09f50efde --- /dev/null +++ b/bot/exts/moderation/verify.py @@ -0,0 +1,45 @@ +import logging + +from discord import Member, Role +from discord.ext.commands import Cog, Context, command, has_any_role + +from bot.bot import Bot +from bot.constants import Emojis, Guild, MODERATION_ROLES, Roles + +log = logging.getLogger(__name__) + + +class Verify(Cog): + """Command for applying verification roles.""" + + def __init__(self, bot: Bot) -> None: + self.bot = bot + self.developer_role: Role = None + + @Cog.listener() + async def on_ready(self) -> None: + """Sets `self.developer_role` to the Role object once the bot is online.""" + await self.bot.wait_until_guild_available() + self.developer_role = self.bot.get_guild(Guild.id).get_role(Roles.verified) + + @command(name='verify') + @has_any_role(*MODERATION_ROLES) + async def apply_developer_role(self, ctx: Context, user: Member) -> None: + """Command for moderators to apply the Developer role to any user.""" + log.trace(f'verify command called by {ctx.author} for {user.id}.') + if self.developer_role is None: + await self.on_ready() + + if self.developer_role in user.roles: + log.trace(f'{user.id} is already a developer, aborting.') + await ctx.send(f'{Emojis.cross_mark} {user} is already a developer.') + return + + await user.add_roles(self.developer_role) + log.trace(f'Developer role successfully applied to {user.id}') + await ctx.send(f'{Emojis.check_mark} Developer role role applied to {user}.') + + +def setup(bot: Bot) -> None: + """Load the Verify cog.""" + bot.add_cog(Verify(bot)) -- cgit v1.2.3 From 0833ad51cfbd93df2d5a655255e6161334b4efe6 Mon Sep 17 00:00:00 2001 From: Steele Date: Wed, 9 Dec 2020 22:59:02 -0500 Subject: Delete verify.py, integrate `!verify` command into verification.py. There wasn't any reason the command needed its own cog, so the exact same functionality is now in the Verification cog. --- bot/exts/moderation/verification.py | 16 +++++++++++++ bot/exts/moderation/verify.py | 45 ------------------------------------- 2 files changed, 16 insertions(+), 45 deletions(-) delete mode 100644 bot/exts/moderation/verify.py diff --git a/bot/exts/moderation/verification.py b/bot/exts/moderation/verification.py index b1c94185a..c42c6588f 100644 --- a/bot/exts/moderation/verification.py +++ b/bot/exts/moderation/verification.py @@ -848,6 +848,22 @@ class Verification(Cog): else: return True + @command(name='verify') + @has_any_role(*constants.MODERATION_ROLES) + async def apply_developer_role(self, ctx: Context, user: discord.Member) -> None: + """Command for moderators to apply the Developer role to any user.""" + log.trace(f'verify command called by {ctx.author} for {user.id}.') + developer_role = self.bot.get_guild(constants.Guild.id).get_role(constants.Roles.verified) + + if developer_role in user.roles: + log.trace(f'{user.id} is already a developer, aborting.') + await ctx.send(f'{constants.Emojis.cross_mark} {user} is already a developer.') + return + + await user.add_roles(developer_role) + log.trace(f'Developer role successfully applied to {user.id}') + await ctx.send(f'{constants.Emojis.check_mark} Developer role applied to {user}.') + # endregion diff --git a/bot/exts/moderation/verify.py b/bot/exts/moderation/verify.py deleted file mode 100644 index 09f50efde..000000000 --- a/bot/exts/moderation/verify.py +++ /dev/null @@ -1,45 +0,0 @@ -import logging - -from discord import Member, Role -from discord.ext.commands import Cog, Context, command, has_any_role - -from bot.bot import Bot -from bot.constants import Emojis, Guild, MODERATION_ROLES, Roles - -log = logging.getLogger(__name__) - - -class Verify(Cog): - """Command for applying verification roles.""" - - def __init__(self, bot: Bot) -> None: - self.bot = bot - self.developer_role: Role = None - - @Cog.listener() - async def on_ready(self) -> None: - """Sets `self.developer_role` to the Role object once the bot is online.""" - await self.bot.wait_until_guild_available() - self.developer_role = self.bot.get_guild(Guild.id).get_role(Roles.verified) - - @command(name='verify') - @has_any_role(*MODERATION_ROLES) - async def apply_developer_role(self, ctx: Context, user: Member) -> None: - """Command for moderators to apply the Developer role to any user.""" - log.trace(f'verify command called by {ctx.author} for {user.id}.') - if self.developer_role is None: - await self.on_ready() - - if self.developer_role in user.roles: - log.trace(f'{user.id} is already a developer, aborting.') - await ctx.send(f'{Emojis.cross_mark} {user} is already a developer.') - return - - await user.add_roles(self.developer_role) - log.trace(f'Developer role successfully applied to {user.id}') - await ctx.send(f'{Emojis.check_mark} Developer role role applied to {user}.') - - -def setup(bot: Bot) -> None: - """Load the Verify cog.""" - bot.add_cog(Verify(bot)) -- cgit v1.2.3 From 47a2607ac85c7cf808152c301fb6969723915389 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Thu, 10 Dec 2020 15:39:09 +0200 Subject: Use Snowflake converter for snowflake command --- bot/exts/utils/utils.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bot/exts/utils/utils.py b/bot/exts/utils/utils.py index 87abbe4de..8e7e6ba36 100644 --- a/bot/exts/utils/utils.py +++ b/bot/exts/utils/utils.py @@ -13,6 +13,7 @@ from discord.utils import snowflake_time from bot.bot import Bot from bot.constants import Channels, MODERATION_ROLES, STAFF_ROLES +from bot.converters import Snowflake from bot.decorators import in_whitelist from bot.pagination import LinePaginator from bot.utils import messages @@ -170,7 +171,7 @@ class Utils(Cog): @command(aliases=("snf", "snfl", "sf")) @in_whitelist(channels=(Channels.bot_commands,), roles=STAFF_ROLES) - async def snowflake(self, ctx: Context, snowflake: int) -> None: + async def snowflake(self, ctx: Context, snowflake: Snowflake) -> None: """Get Discord snowflake creation time.""" created_at = snowflake_time(snowflake) embed = Embed( -- cgit v1.2.3 From def97dd4c9d43bf2a5275a860a9eeb8e91bdb5a9 Mon Sep 17 00:00:00 2001 From: Sebastiaan Zeeff Date: Thu, 10 Dec 2020 21:30:58 +0100 Subject: Send a custom workflow status embed to Discord This commit introduces the same custom status embed as is already being used for Sir Lancebot. The default embeds GitHub sends are disabled, as they were causing slight issues with rate limits from time to time. It works like this: - The Lint & Test workflow stores an artifact with PR information, if we are linting/testing a PR. - Whenever we reach the end of a workflow run sequence, a status embed is send with the conclusion status. Signed-off-by: Sebastiaan Zeeff --- .github/workflows/lint-test.yml | 22 +++++++++++ .github/workflows/status_embed.yaml | 78 +++++++++++++++++++++++++++++++++++++ 2 files changed, 100 insertions(+) create mode 100644 .github/workflows/status_embed.yaml diff --git a/.github/workflows/lint-test.yml b/.github/workflows/lint-test.yml index 5444fc3de..a38f031fa 100644 --- a/.github/workflows/lint-test.yml +++ b/.github/workflows/lint-test.yml @@ -113,3 +113,25 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: coveralls + + # Prepare the Pull Request Payload artifact. If this fails, we + # we fail silently using the `continue-on-error` option. It's + # nice if this succeeds, but if it fails for any reason, it + # does not mean that our lint-test checks failed. + - name: Prepare Pull Request Payload artifact + id: prepare-artifact + if: always() && github.event_name == 'pull_request' + continue-on-error: true + run: cat $GITHUB_EVENT_PATH | jq '.pull_request' > pull_request_payload.json + + # This only makes sense if the previous step succeeded. To + # get the original outcome of the previous step before the + # `continue-on-error` conclusion is applied, we use the + # `.outcome` value. This step also fails silently. + - name: Upload a Build Artifact + if: steps.prepare-artifact.outcome == 'success' + continue-on-error: true + uses: actions/upload-artifact@v2 + with: + name: pull-request-payload + path: pull_request_payload.json diff --git a/.github/workflows/status_embed.yaml b/.github/workflows/status_embed.yaml new file mode 100644 index 000000000..b6a71b887 --- /dev/null +++ b/.github/workflows/status_embed.yaml @@ -0,0 +1,78 @@ +name: Status Embed + +on: + workflow_run: + workflows: + - Lint & Test + - Build + - Deploy + types: + - completed + +jobs: + status_embed: + # We need to send a status embed whenever the workflow + # sequence we're running terminates. There are a number + # of situations in which that happens: + # + # 1. We reach the end of the Deploy workflow, without + # it being skipped. + # + # 2. A `pull_request` triggered a Lint & Test workflow, + # as the sequence always terminates with one run. + # + # 3. If any workflow ends in failure or was cancelled. + if: >- + (github.event.workflow_run.name == 'Deploy' && github.event.workflow_run.conclusion != 'skipped') || + github.event.workflow_run.event == 'pull_request' || + github.event.workflow_run.conclusion == 'failure' || + github.event.workflow_run.conclusion == 'cancelled' + name: Send Status Embed to Discord + runs-on: ubuntu-latest + + steps: + # A workflow_run event does not contain all the information + # we need for a PR embed. That's why we upload an artifact + # with that information in the Lint workflow. + - name: Get Pull Request Information + id: pr_info + if: github.event.workflow_run.event == 'pull_request' + run: | + curl -s -H "Authorization: token $GITHUB_TOKEN" ${{ github.event.workflow_run.artifacts_url }} > artifacts.json + DOWNLOAD_URL=$(cat artifacts.json | jq -r '.artifacts[] | select(.name == "pull-request-payload") | .archive_download_url') + [ -z "$DOWNLOAD_URL" ] && exit 1 + wget --quiet --header="Authorization: token $GITHUB_TOKEN" -O pull_request_payload.zip $DOWNLOAD_URL || exit 2 + unzip -p pull_request_payload.zip > pull_request_payload.json + [ -s pull_request_payload.json ] || exit 3 + echo "::set-output name=pr_author_login::$(jq -r '.user.login // empty' pull_request_payload.json)" + echo "::set-output name=pr_number::$(jq -r '.number // empty' pull_request_payload.json)" + echo "::set-output name=pr_title::$(jq -r '.title // empty' pull_request_payload.json)" + echo "::set-output name=pr_source::$(jq -r '.head.label // empty' pull_request_payload.json)" + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + # Send an informational status embed to Discord instead of the + # standard embeds that Discord sends. This embed will contain + # more information and we can fine tune when we actually want + # to send an embed. + - name: GitHub Actions Status Embed for Discord + uses: SebastiaanZ/github-status-embed-for-discord@v0.2.1 + with: + # Our GitHub Actions webhook + webhook_id: '784184528997842985' + webhook_token: ${{ secrets.GHA_WEBHOOK_TOKEN }} + + # Workflow information + workflow_name: ${{ github.event.workflow_run.name }} + run_id: ${{ github.event.workflow_run.id }} + run_number: ${{ github.event.workflow_run.run_number }} + status: ${{ github.event.workflow_run.conclusion }} + actor: ${{ github.actor }} + repository: ${{ github.repository }} + ref: ${{ github.ref }} + sha: ${{ github.event.workflow_run.head_sha }} + + pr_author_login: ${{ steps.pr_info.outputs.pr_author_login }} + pr_number: ${{ steps.pr_info.outputs.pr_number }} + pr_title: ${{ steps.pr_info.outputs.pr_title }} + pr_source: ${{ steps.pr_info.outputs.pr_source }} -- cgit v1.2.3 From 9250608d1ec80fcc098e0174f5204f157fab9b8e Mon Sep 17 00:00:00 2001 From: Xithrius Date: Thu, 10 Dec 2020 16:34:47 -0800 Subject: Compressed if into or statements. --- bot/exts/info/information.py | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/bot/exts/info/information.py b/bot/exts/info/information.py index a8adb817b..5d94d73e9 100644 --- a/bot/exts/info/information.py +++ b/bot/exts/info/information.py @@ -382,15 +382,8 @@ class Information(Cog): if verified_at is not None: verified_at = time_since(parser.isoparse(user_activity["verified_at"]), max_units=3) - if user_activity["total_messages"]: - activity_output.append(user_activity['total_messages']) - else: - activity_output.append("No messages") - - if user_activity["activity_blocks"]: - activity_output.append(user_activity["activity_blocks"]) - else: - activity_output.append("No activity") + activity_output.append(user_activity['total_messages'] or "No messages") + activity_output.append(user_activity["activity_blocks"] or "No activity") activity_output = "\n".join( f"{name}: {metric}" for name, metric in zip(["Messages", "Activity blocks"], activity_output)) -- cgit v1.2.3 From 223455d979cc794f857fc77e6211837c9639cca9 Mon Sep 17 00:00:00 2001 From: Xithrius <15021300+Xithrius@users.noreply.github.com> Date: Thu, 10 Dec 2020 16:38:48 -0800 Subject: Compressed embed building Co-authored-by: Hassan Abouelela <47495861+HassanAbouelela@users.noreply.github.com> --- bot/exts/info/information.py | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/bot/exts/info/information.py b/bot/exts/info/information.py index a8adb817b..648f283bc 100644 --- a/bot/exts/info/information.py +++ b/bot/exts/info/information.py @@ -230,17 +230,11 @@ class Information(Cog): if on_server: joined = time_since(user.joined_at, max_units=3) roles = ", ".join(role.mention for role in user.roles[1:]) - if is_mod_channel(ctx.channel): - membership = textwrap.dedent(f""" - Joined: {joined} - Verified: {verified_at} - Roles: {roles or None} - """).strip() - else: - membership = textwrap.dedent(f""" - Joined: {joined} - Roles: {roles or None} - """).strip() + membership = {"Joined": joined, "Verified": verified_at, "Roles": roles or None} + if not is_mod_channel(ctx.channel): + membership.pop("Verified") + + membership = textwrap.dedent("\n".join([f"{key}: {value}" for key, value in membership.items()])) else: roles = None membership = "The user is not a member of the server" -- cgit v1.2.3 From dd2f29feae436a550b73b20d43166a9548840f47 Mon Sep 17 00:00:00 2001 From: Xithrius <15021300+Xithrius@users.noreply.github.com> Date: Thu, 10 Dec 2020 16:39:29 -0800 Subject: Slightly reformatted activity block building. Co-authored-by: Hassan Abouelela <47495861+HassanAbouelela@users.noreply.github.com> --- bot/exts/info/information.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bot/exts/info/information.py b/bot/exts/info/information.py index 648f283bc..22a32cdb5 100644 --- a/bot/exts/info/information.py +++ b/bot/exts/info/information.py @@ -387,7 +387,8 @@ class Information(Cog): activity_output.append("No activity") activity_output = "\n".join( - f"{name}: {metric}" for name, metric in zip(["Messages", "Activity blocks"], activity_output)) + f"{name}: {metric}" for name, metric in zip(["Messages", "Activity blocks"], activity_output) + ) return verified_at, ("Activity", activity_output) -- cgit v1.2.3 From 77a8e420a69fdafb9fe96739d9d728c7a5d3638f Mon Sep 17 00:00:00 2001 From: Xithrius Date: Thu, 10 Dec 2020 16:57:35 -0800 Subject: Added docstring for the user activity function. --- bot/exts/info/information.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/bot/exts/info/information.py b/bot/exts/info/information.py index 26cf5fee3..8eec22c58 100644 --- a/bot/exts/info/information.py +++ b/bot/exts/info/information.py @@ -361,7 +361,12 @@ class Information(Cog): return "Nominations", "\n".join(output) async def user_verification_and_messages(self, user: FetchedMember) -> Tuple[Union[bool, str], Tuple[str, str]]: - """Gets the time of verification and amount of messages for `member`.""" + """ + Gets the time of verification and amount of messages for `member`. + + Fetches information from the metricity database that's hosted by the site. + If the database returns a code besides a 404, then many parts of the bot are broken including this one. + """ activity_output = [] verified_at = False -- cgit v1.2.3 From 35f7ecda15e017afd184a94404d21c3f97cd0583 Mon Sep 17 00:00:00 2001 From: Sebastiaan Zeeff <33516116+SebastiaanZ@users.noreply.github.com> Date: Fri, 11 Dec 2020 06:45:21 +0100 Subject: Make sure PR build artifact is always uploaded GitHub Actions has an implicit status condition, `success()`, that is added whenever an `if` condition lacks a status function check of its own. In this case, while the upload step did check for the outcome of the previous "always" step, it did not have an actual status check and, thus, only ran on success. Since we always want to upload the artifact, even if other steps failed, I've added the "always" status function now. --- .github/workflows/lint-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lint-test.yml b/.github/workflows/lint-test.yml index a38f031fa..6fa8e8333 100644 --- a/.github/workflows/lint-test.yml +++ b/.github/workflows/lint-test.yml @@ -129,7 +129,7 @@ jobs: # `continue-on-error` conclusion is applied, we use the # `.outcome` value. This step also fails silently. - name: Upload a Build Artifact - if: steps.prepare-artifact.outcome == 'success' + if: always() && steps.prepare-artifact.outcome == 'success' continue-on-error: true uses: actions/upload-artifact@v2 with: -- cgit v1.2.3 From 2fa5b78e357bf45e23e188dc501180ed241237d1 Mon Sep 17 00:00:00 2001 From: Xithrius Date: Fri, 11 Dec 2020 05:06:03 -0800 Subject: Added catching for unparsable short ISO dates. --- bot/exts/info/information.py | 11 +++++++---- tests/bot/exts/info/test_information.py | 1 + 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/bot/exts/info/information.py b/bot/exts/info/information.py index 8eec22c58..0c04d7cd0 100644 --- a/bot/exts/info/information.py +++ b/bot/exts/info/information.py @@ -230,7 +230,7 @@ class Information(Cog): if on_server: joined = time_since(user.joined_at, max_units=3) roles = ", ".join(role.mention for role in user.roles[1:]) - membership = {"Joined": joined, "Verified": verified_at, "Roles": roles or None} + membership = {"Joined": joined, "Verified": verified_at or "False", "Roles": roles or None} if not is_mod_channel(ctx.channel): membership.pop("Verified") @@ -377,9 +377,12 @@ class Information(Cog): activity_output = "No activity" else: - verified_at = user_activity['verified_at'] - if verified_at is not None: - verified_at = time_since(parser.isoparse(user_activity["verified_at"]), max_units=3) + try: + if (verified_at := user_activity['verified_at']) is not None: + verified_at = time_since(parser.isoparse(verified_at), max_units=3) + except ValueError: + log.warning('Could not parse ISO string correctly for user verification date.') + verified_at = None activity_output.append(user_activity['total_messages'] or "No messages") activity_output.append(user_activity["activity_blocks"] or "No activity") diff --git a/tests/bot/exts/info/test_information.py b/tests/bot/exts/info/test_information.py index daede54c5..254b0a867 100644 --- a/tests/bot/exts/info/test_information.py +++ b/tests/bot/exts/info/test_information.py @@ -355,6 +355,7 @@ class UserEmbedTests(unittest.IsolatedAsyncioTestCase): self.assertEqual( textwrap.dedent(f""" Joined: {"1 year ago"} + Verified: {"False"} Roles: &Moderators """).strip(), embed.fields[1].value -- cgit v1.2.3 From 9f1bbe528311afaf5a56ebafdac7a629c9ce238e Mon Sep 17 00:00:00 2001 From: Xithrius Date: Fri, 11 Dec 2020 05:11:48 -0800 Subject: Single to double quotes & warning includes user ID. --- bot/exts/info/information.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/bot/exts/info/information.py b/bot/exts/info/information.py index 0c04d7cd0..187950689 100644 --- a/bot/exts/info/information.py +++ b/bot/exts/info/information.py @@ -371,20 +371,20 @@ class Information(Cog): verified_at = False try: - user_activity = await self.bot.api_client.get(f'bot/users/{user.id}/metricity_data') + user_activity = await self.bot.api_client.get(f"bot/users/{user.id}/metricity_data") except ResponseCodeError as e: if e.status == 404: activity_output = "No activity" else: try: - if (verified_at := user_activity['verified_at']) is not None: + if (verified_at := user_activity["verified_at"]) is not None: verified_at = time_since(parser.isoparse(verified_at), max_units=3) except ValueError: - log.warning('Could not parse ISO string correctly for user verification date.') + log.warning(f"Could not parse ISO string correctly for user {user.id} verification date.") verified_at = None - activity_output.append(user_activity['total_messages'] or "No messages") + activity_output.append(user_activity["total_messages"] or "No messages") activity_output.append(user_activity["activity_blocks"] or "No activity") activity_output = "\n".join( -- cgit v1.2.3 From 628bd4ffd1717eaed9372287c59fae1b23d4cbdf Mon Sep 17 00:00:00 2001 From: Joe Banks Date: Sat, 12 Dec 2020 02:08:35 +0000 Subject: Comma separators in metricity data in user command --- bot/exts/info/information.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bot/exts/info/information.py b/bot/exts/info/information.py index 187950689..178d48a67 100644 --- a/bot/exts/info/information.py +++ b/bot/exts/info/information.py @@ -384,8 +384,8 @@ class Information(Cog): log.warning(f"Could not parse ISO string correctly for user {user.id} verification date.") verified_at = None - activity_output.append(user_activity["total_messages"] or "No messages") - activity_output.append(user_activity["activity_blocks"] or "No activity") + activity_output.append(f"{user_activity['total_messages']:,}" or "No messages") + activity_output.append(f"{user_activity['activity_blocks']:,}" or "No activity") activity_output = "\n".join( f"{name}: {metric}" for name, metric in zip(["Messages", "Activity blocks"], activity_output) -- cgit v1.2.3 From b98c7f35916b9e5a41945030d87227394bafa1d5 Mon Sep 17 00:00:00 2001 From: Joe Banks Date: Sat, 12 Dec 2020 02:29:52 +0000 Subject: Update comma code to fix tests --- bot/exts/info/information.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/bot/exts/info/information.py b/bot/exts/info/information.py index 178d48a67..2543d1e28 100644 --- a/bot/exts/info/information.py +++ b/bot/exts/info/information.py @@ -384,8 +384,15 @@ class Information(Cog): log.warning(f"Could not parse ISO string correctly for user {user.id} verification date.") verified_at = None - activity_output.append(f"{user_activity['total_messages']:,}" or "No messages") - activity_output.append(f"{user_activity['activity_blocks']:,}" or "No activity") + if messages := user_activity["total_messages"]: + activity_output.append(f"{messages:,}") + else: + activity_output.append("No messages") + + if activity_blocks := user_activity["activity_blocks"]: + activity_output.append(f"{activity_blocks:,}") + else: + activity_output.append("No activity") activity_output = "\n".join( f"{name}: {metric}" for name, metric in zip(["Messages", "Activity blocks"], activity_output) -- cgit v1.2.3 From c3597108c8d191fd527de0f532e0bda238c3c50e Mon Sep 17 00:00:00 2001 From: Joe Banks Date: Sat, 12 Dec 2020 02:34:43 +0000 Subject: Revert "Update comma code to fix tests" This reverts commit b98c7f35916b9e5a41945030d87227394bafa1d5. --- bot/exts/info/information.py | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/bot/exts/info/information.py b/bot/exts/info/information.py index 2543d1e28..178d48a67 100644 --- a/bot/exts/info/information.py +++ b/bot/exts/info/information.py @@ -384,15 +384,8 @@ class Information(Cog): log.warning(f"Could not parse ISO string correctly for user {user.id} verification date.") verified_at = None - if messages := user_activity["total_messages"]: - activity_output.append(f"{messages:,}") - else: - activity_output.append("No messages") - - if activity_blocks := user_activity["activity_blocks"]: - activity_output.append(f"{activity_blocks:,}") - else: - activity_output.append("No activity") + activity_output.append(f"{user_activity['total_messages']:,}" or "No messages") + activity_output.append(f"{user_activity['activity_blocks']:,}" or "No activity") activity_output = "\n".join( f"{name}: {metric}" for name, metric in zip(["Messages", "Activity blocks"], activity_output) -- cgit v1.2.3 From bcab67375c778fb30c86d8edd19bed854b0f8b45 Mon Sep 17 00:00:00 2001 From: Joe Banks Date: Sat, 12 Dec 2020 02:34:51 +0000 Subject: Revert "Comma separators in metricity data in user command" This reverts commit 628bd4ffd1717eaed9372287c59fae1b23d4cbdf. --- bot/exts/info/information.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bot/exts/info/information.py b/bot/exts/info/information.py index 178d48a67..187950689 100644 --- a/bot/exts/info/information.py +++ b/bot/exts/info/information.py @@ -384,8 +384,8 @@ class Information(Cog): log.warning(f"Could not parse ISO string correctly for user {user.id} verification date.") verified_at = None - activity_output.append(f"{user_activity['total_messages']:,}" or "No messages") - activity_output.append(f"{user_activity['activity_blocks']:,}" or "No activity") + activity_output.append(user_activity["total_messages"] or "No messages") + activity_output.append(user_activity["activity_blocks"] or "No activity") activity_output = "\n".join( f"{name}: {metric}" for name, metric in zip(["Messages", "Activity blocks"], activity_output) -- cgit v1.2.3 From 93fb7413e7f98ced1a56f5dc00aea363e7a16625 Mon Sep 17 00:00:00 2001 From: Numerlor <25886452+Numerlor@users.noreply.github.com> Date: Mon, 14 Dec 2020 22:01:16 +0100 Subject: Fix codeblock escape On some devices the previous escaping didn't work properly, escaping all backticks will make sure none of them get registered as Markdown --- bot/resources/tags/codeblock.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bot/resources/tags/codeblock.md b/bot/resources/tags/codeblock.md index 8d48bdf06..ac64656e5 100644 --- a/bot/resources/tags/codeblock.md +++ b/bot/resources/tags/codeblock.md @@ -1,7 +1,7 @@ Here's how to format Python code on Discord: -\```py +\`\`\`py print('Hello world!') -\``` +\`\`\` **These are backticks, not quotes.** Check [this](https://superuser.com/questions/254076/how-do-i-type-the-tick-and-backtick-characters-on-windows/254077#254077) out if you can't find the backtick key. -- cgit v1.2.3 From ab0785b06157e9628c02bdb5aaecef0d5d8c5cb4 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Wed, 16 Dec 2020 17:40:14 +0200 Subject: Add codeowner entries for ks129 --- .github/CODEOWNERS | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 73e303325..ad813d893 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -6,9 +6,11 @@ bot/exts/info/codeblock/** @MarkKoz bot/exts/utils/extensions.py @MarkKoz bot/exts/utils/snekbox.py @MarkKoz @Akarys42 bot/exts/help_channels/** @MarkKoz @Akarys42 -bot/exts/moderation/** @Akarys42 @mbaruh @Den4200 +bot/exts/moderation/** @Akarys42 @mbaruh @Den4200 @ks129 bot/exts/info/** @Akarys42 @mbaruh @Den4200 bot/exts/filters/** @mbaruh +bot/exts/fun/** @ks129 +bot/exts/utils/** @ks129 # Utils bot/utils/extensions.py @MarkKoz -- cgit v1.2.3 From 14e71609e8e40be0832b66df7a0c309ba262659a Mon Sep 17 00:00:00 2001 From: Joe Banks Date: Wed, 16 Dec 2020 23:50:23 +0000 Subject: Update verification.py --- bot/exts/moderation/verification.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bot/exts/moderation/verification.py b/bot/exts/moderation/verification.py index c42c6588f..7aa559617 100644 --- a/bot/exts/moderation/verification.py +++ b/bot/exts/moderation/verification.py @@ -565,11 +565,11 @@ class Verification(Cog): raw_member = await self.bot.http.get_member(member.guild.id, member.id) - # If the user has the is_pending flag set, they will be using the alternate + # If the user has the pending flag set, they will be using the alternate # gate and will not need a welcome DM with verification instructions. # We will send them an alternate DM once they verify with the welcome # video. - if raw_member.get("is_pending"): + if raw_member.get("pending"): await self.member_gating_cache.set(member.id, True) # TODO: Temporary, remove soon after asking joe. -- cgit v1.2.3