Skip to content

Commit 2334827

Browse files
committed
Merge remote-tracking branch 'origin/main' into filters/autoban
2 parents 798150e + 3b846ae commit 2334827

File tree

32 files changed

+360
-303
lines changed

32 files changed

+360
-303
lines changed

bot/converters.py

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import re
44
import typing as t
5-
from datetime import datetime
5+
from datetime import datetime, timezone
66
from ssl import CertificateError
77

88
import dateutil.parser
@@ -11,7 +11,7 @@
1111
from aiohttp import ClientConnectorError
1212
from dateutil.relativedelta import relativedelta
1313
from discord.ext.commands import BadArgument, Bot, Context, Converter, IDConverter, MemberConverter, UserConverter
14-
from discord.utils import DISCORD_EPOCH, escape_markdown, snowflake_time
14+
from discord.utils import escape_markdown, snowflake_time
1515

1616
from bot import exts
1717
from bot.api import ResponseCodeError
@@ -28,7 +28,7 @@
2828

2929
log = get_logger(__name__)
3030

31-
DISCORD_EPOCH_DT = datetime.utcfromtimestamp(DISCORD_EPOCH / 1000)
31+
DISCORD_EPOCH_DT = snowflake_time(0)
3232
RE_USER_MENTION = re.compile(r"<@!?([0-9]+)>$")
3333

3434

@@ -71,10 +71,10 @@ class ValidDiscordServerInvite(Converter):
7171

7272
async def convert(self, ctx: Context, server_invite: str) -> dict:
7373
"""Check whether the string is a valid Discord server invite."""
74-
invite_code = INVITE_RE.search(server_invite)
74+
invite_code = INVITE_RE.match(server_invite)
7575
if invite_code:
7676
response = await ctx.bot.http_session.get(
77-
f"{URLs.discord_invite_api}/{invite_code[1]}"
77+
f"{URLs.discord_invite_api}/{invite_code.group('invite')}"
7878
)
7979
if response.status != 404:
8080
invite_data = await response.json()
@@ -273,14 +273,14 @@ async def convert(self, ctx: Context, arg: str) -> int:
273273
snowflake = int(arg)
274274

275275
try:
276-
time = snowflake_time(snowflake).replace(tzinfo=None)
276+
time = snowflake_time(snowflake)
277277
except (OverflowError, OSError) as e:
278278
# Not sure if this can ever even happen, but let's be safe.
279279
raise BadArgument(f"{error}: {e}")
280280

281281
if time < DISCORD_EPOCH_DT:
282282
raise BadArgument(f"{error}: timestamp is before the Discord epoch.")
283-
elif (datetime.utcnow() - time).days < -1:
283+
elif (datetime.now(timezone.utc) - time).days < -1:
284284
raise BadArgument(f"{error}: timestamp is too far into the future.")
285285

286286
return snowflake
@@ -387,7 +387,7 @@ async def convert(self, ctx: Context, duration: str) -> datetime:
387387
The converter supports the same symbols for each unit of time as its parent class.
388388
"""
389389
delta = await super().convert(ctx, duration)
390-
now = datetime.utcnow()
390+
now = datetime.now(timezone.utc)
391391

392392
try:
393393
return now + delta
@@ -443,8 +443,8 @@ async def convert(self, ctx: Context, datetime_string: str) -> datetime:
443443
The converter is flexible in the formats it accepts, as it uses the `isoparse` method of
444444
`dateutil.parser`. In general, it accepts datetime strings that start with a date,
445445
optionally followed by a time. Specifying a timezone offset in the datetime string is
446-
supported, but the `datetime` object will be converted to UTC and will be returned without
447-
`tzinfo` as a timezone-unaware `datetime` object.
446+
supported, but the `datetime` object will be converted to UTC. If no timezone is specified, the datetime will
447+
be assumed to be in UTC already. In all cases, the returned object will have the UTC timezone.
448448
449449
See: https://dateutil.readthedocs.io/en/stable/parser.html#dateutil.parser.isoparse
450450
@@ -470,7 +470,8 @@ async def convert(self, ctx: Context, datetime_string: str) -> datetime:
470470

471471
if dt.tzinfo:
472472
dt = dt.astimezone(dateutil.tz.UTC)
473-
dt = dt.replace(tzinfo=None)
473+
else: # Without a timezone, assume it represents UTC.
474+
dt = dt.replace(tzinfo=dateutil.tz.UTC)
474475

475476
return dt
476477

bot/exts/backend/error_handler.py

Lines changed: 19 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -59,17 +59,23 @@ async def on_command_error(self, ctx: Context, e: errors.CommandError) -> None:
5959
log.trace(f"Command {command} had its error already handled locally; ignoring.")
6060
return
6161

62+
debug_message = (
63+
f"Command {command} invoked by {ctx.message.author} with error "
64+
f"{e.__class__.__name__}: {e}"
65+
)
66+
6267
if isinstance(e, errors.CommandNotFound) and not getattr(ctx, "invoked_from_error_handler", False):
6368
if await self.try_silence(ctx):
6469
return
65-
# Try to look for a tag with the command's name
66-
await self.try_get_tag(ctx)
67-
return # Exit early to avoid logging.
70+
await self.try_get_tag(ctx) # Try to look for a tag with the command's name
6871
elif isinstance(e, errors.UserInputError):
72+
log.debug(debug_message)
6973
await self.handle_user_input_error(ctx, e)
7074
elif isinstance(e, errors.CheckFailure):
75+
log.debug(debug_message)
7176
await self.handle_check_failure(ctx, e)
7277
elif isinstance(e, errors.CommandOnCooldown):
78+
log.debug(debug_message)
7379
await ctx.send(e)
7480
elif isinstance(e, errors.CommandInvokeError):
7581
if isinstance(e.original, ResponseCodeError):
@@ -80,22 +86,16 @@ async def on_command_error(self, ctx: Context, e: errors.CommandError) -> None:
8086
await ctx.send(f"Cannot infract that user. {e.original.reason}")
8187
else:
8288
await self.handle_unexpected_error(ctx, e.original)
83-
return # Exit early to avoid logging.
8489
elif isinstance(e, errors.ConversionError):
8590
if isinstance(e.original, ResponseCodeError):
8691
await self.handle_api_error(ctx, e.original)
8792
else:
8893
await self.handle_unexpected_error(ctx, e.original)
89-
return # Exit early to avoid logging.
90-
elif not isinstance(e, errors.DisabledCommand):
94+
elif isinstance(e, errors.DisabledCommand):
95+
log.debug(debug_message)
96+
else:
9197
# MaxConcurrencyReached, ExtensionError
9298
await self.handle_unexpected_error(ctx, e)
93-
return # Exit early to avoid logging.
94-
95-
log.debug(
96-
f"Command {command} invoked by {ctx.message.author} with error "
97-
f"{e.__class__.__name__}: {e}"
98-
)
9999

100100
@staticmethod
101101
def get_help_command(ctx: Context) -> t.Coroutine:
@@ -188,9 +188,6 @@ async def try_get_tag(self, ctx: Context) -> None:
188188
if not any(role.id in MODERATION_ROLES for role in ctx.author.roles):
189189
await self.send_command_suggestion(ctx, ctx.invoked_with)
190190

191-
# Return to not raise the exception
192-
return
193-
194191
async def send_command_suggestion(self, ctx: Context, command_name: str) -> None:
195192
"""Sends user similar commands if any can be found."""
196193
# No similar tag found, or tag on cooldown -
@@ -235,38 +232,32 @@ async def handle_user_input_error(self, ctx: Context, e: errors.UserInputError)
235232
"""
236233
if isinstance(e, errors.MissingRequiredArgument):
237234
embed = self._get_error_embed("Missing required argument", e.param.name)
238-
await ctx.send(embed=embed)
239-
await self.get_help_command(ctx)
240235
self.bot.stats.incr("errors.missing_required_argument")
241236
elif isinstance(e, errors.TooManyArguments):
242237
embed = self._get_error_embed("Too many arguments", str(e))
243-
await ctx.send(embed=embed)
244-
await self.get_help_command(ctx)
245238
self.bot.stats.incr("errors.too_many_arguments")
246239
elif isinstance(e, errors.BadArgument):
247240
embed = self._get_error_embed("Bad argument", str(e))
248-
await ctx.send(embed=embed)
249-
await self.get_help_command(ctx)
250241
self.bot.stats.incr("errors.bad_argument")
251242
elif isinstance(e, errors.BadUnionArgument):
252243
embed = self._get_error_embed("Bad argument", f"{e}\n{e.errors[-1]}")
253-
await ctx.send(embed=embed)
254-
await self.get_help_command(ctx)
255244
self.bot.stats.incr("errors.bad_union_argument")
256245
elif isinstance(e, errors.ArgumentParsingError):
257246
embed = self._get_error_embed("Argument parsing error", str(e))
258247
await ctx.send(embed=embed)
259248
self.get_help_command(ctx).close()
260249
self.bot.stats.incr("errors.argument_parsing_error")
250+
return
261251
else:
262252
embed = self._get_error_embed(
263253
"Input error",
264254
"Something about your input seems off. Check the arguments and try again."
265255
)
266-
await ctx.send(embed=embed)
267-
await self.get_help_command(ctx)
268256
self.bot.stats.incr("errors.other_user_input_error")
269257

258+
await ctx.send(embed=embed)
259+
await self.get_help_command(ctx)
260+
270261
@staticmethod
271262
async def handle_check_failure(ctx: Context, e: errors.CheckFailure) -> None:
272263
"""
@@ -299,21 +290,21 @@ async def handle_check_failure(ctx: Context, e: errors.CheckFailure) -> None:
299290
async def handle_api_error(ctx: Context, e: ResponseCodeError) -> None:
300291
"""Send an error message in `ctx` for ResponseCodeError and log it."""
301292
if e.status == 404:
302-
await ctx.send("There does not seem to be anything matching your query.")
303293
log.debug(f"API responded with 404 for command {ctx.command}")
294+
await ctx.send("There does not seem to be anything matching your query.")
304295
ctx.bot.stats.incr("errors.api_error_404")
305296
elif e.status == 400:
306297
content = await e.response.json()
307298
log.debug(f"API responded with 400 for command {ctx.command}: %r.", content)
308299
await ctx.send("According to the API, your request is malformed.")
309300
ctx.bot.stats.incr("errors.api_error_400")
310301
elif 500 <= e.status < 600:
311-
await ctx.send("Sorry, there seems to be an internal issue with the API.")
312302
log.warning(f"API responded with {e.status} for command {ctx.command}")
303+
await ctx.send("Sorry, there seems to be an internal issue with the API.")
313304
ctx.bot.stats.incr("errors.api_internal_server_error")
314305
else:
315-
await ctx.send(f"Got an unexpected status code from the API (`{e.status}`).")
316306
log.warning(f"Unexpected API response for command {ctx.command}: {e.status}")
307+
await ctx.send(f"Got an unexpected status code from the API (`{e.status}`).")
317308
ctx.bot.stats.incr(f"errors.api_error_{e.status}")
318309

319310
@staticmethod

bot/exts/filters/antispam.py

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,12 @@
22
from collections import defaultdict
33
from collections.abc import Mapping
44
from dataclasses import dataclass, field
5-
from datetime import datetime, timedelta
5+
from datetime import timedelta
66
from itertools import takewhile
77
from operator import attrgetter, itemgetter
88
from typing import Dict, Iterable, List, Set
99

10+
import arrow
1011
from discord import Colour, Member, Message, NotFound, Object, TextChannel
1112
from discord.ext.commands import Cog
1213

@@ -177,21 +178,17 @@ async def on_message(self, message: Message) -> None:
177178

178179
self.cache.append(message)
179180

180-
earliest_relevant_at = datetime.utcnow() - timedelta(seconds=self.max_interval)
181-
relevant_messages = list(
182-
takewhile(lambda msg: msg.created_at.replace(tzinfo=None) > earliest_relevant_at, self.cache)
183-
)
181+
earliest_relevant_at = arrow.utcnow() - timedelta(seconds=self.max_interval)
182+
relevant_messages = list(takewhile(lambda msg: msg.created_at > earliest_relevant_at, self.cache))
184183

185184
for rule_name in AntiSpamConfig.rules:
186185
rule_config = AntiSpamConfig.rules[rule_name]
187186
rule_function = RULE_FUNCTION_MAPPING[rule_name]
188187

189188
# Create a list of messages that were sent in the interval that the rule cares about.
190-
latest_interesting_stamp = datetime.utcnow() - timedelta(seconds=rule_config['interval'])
189+
latest_interesting_stamp = arrow.utcnow() - timedelta(seconds=rule_config['interval'])
191190
messages_for_rule = list(
192-
takewhile(
193-
lambda msg: msg.created_at.replace(tzinfo=None) > latest_interesting_stamp, relevant_messages
194-
)
191+
takewhile(lambda msg: msg.created_at > latest_interesting_stamp, relevant_messages)
195192
)
196193

197194
result = await rule_function(message, messages_for_rule, rule_config)

bot/exts/filters/filtering.py

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
from datetime import datetime, timedelta
44
from typing import Any, Dict, List, Mapping, NamedTuple, Optional, Tuple, Union
55

6-
import dateutil
6+
import arrow
7+
import dateutil.parser
78
import discord.errors
89
import regex
910
from async_rediscache import RedisCache
@@ -209,8 +210,8 @@ def get_name_matches(self, name: str) -> List[re.Match]:
209210
async def check_send_alert(self, member: Member) -> bool:
210211
"""When there is less than 3 days after last alert, return `False`, otherwise `True`."""
211212
if last_alert := await self.name_alerts.get(member.id):
212-
last_alert = datetime.utcfromtimestamp(last_alert)
213-
if datetime.utcnow() - timedelta(days=DAYS_BETWEEN_ALERTS) < last_alert:
213+
last_alert = arrow.get(last_alert)
214+
if arrow.utcnow() - timedelta(days=DAYS_BETWEEN_ALERTS) < last_alert:
214215
log.trace(f"Last alert was too recent for {member}'s nickname.")
215216
return False
216217

@@ -244,7 +245,7 @@ async def check_bad_words_in_name(self, member: Member) -> None:
244245
)
245246

246247
# Update time when alert sent
247-
await self.name_alerts.set(member.id, datetime.utcnow().timestamp())
248+
await self.name_alerts.set(member.id, arrow.utcnow().timestamp())
248249

249250
async def filter_eval(self, result: str, msg: Message) -> bool:
250251
"""
@@ -543,7 +544,7 @@ async def _has_invites(self, text: str) -> Union[dict, bool]:
543544
# discord\.gg/gdudes-pony-farm
544545
text = text.replace("\\", "")
545546

546-
invites = INVITE_RE.findall(text)
547+
invites = [m.group("invite") for m in INVITE_RE.finditer(text)]
547548
invite_data = dict()
548549
for invite in invites:
549550
if invite in invite_data:
@@ -639,25 +640,25 @@ async def notify_member(self, filtered_member: Member, reason: str, channel: Tex
639640

640641
def schedule_msg_delete(self, msg: dict) -> None:
641642
"""Delete an offensive message once its deletion date is reached."""
642-
delete_at = dateutil.parser.isoparse(msg['delete_date']).replace(tzinfo=None)
643+
delete_at = dateutil.parser.isoparse(msg['delete_date'])
643644
self.scheduler.schedule_at(delete_at, msg['id'], self.delete_offensive_msg(msg))
644645

645646
async def reschedule_offensive_msg_deletion(self) -> None:
646647
"""Get all the pending message deletion from the API and reschedule them."""
647648
await self.bot.wait_until_ready()
648649
response = await self.bot.api_client.get('bot/offensive-messages',)
649650

650-
now = datetime.utcnow()
651+
now = arrow.utcnow()
651652

652653
for msg in response:
653-
delete_at = dateutil.parser.isoparse(msg['delete_date']).replace(tzinfo=None)
654+
delete_at = dateutil.parser.isoparse(msg['delete_date'])
654655

655656
if delete_at < now:
656657
await self.delete_offensive_msg(msg)
657658
else:
658659
self.schedule_msg_delete(msg)
659660

660-
async def delete_offensive_msg(self, msg: Mapping[str, str]) -> None:
661+
async def delete_offensive_msg(self, msg: Mapping[str, int]) -> None:
661662
"""Delete an offensive message, and then delete it from the db."""
662663
try:
663664
channel = self.bot.get_channel(msg['channel_id'])

bot/exts/fun/off_topic_names.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import difflib
2-
from datetime import datetime, timedelta
2+
from datetime import timedelta
33

4+
import arrow
45
from discord import Colour, Embed
56
from discord.ext.commands import Cog, Context, group, has_any_role
67
from discord.utils import sleep_until
@@ -22,9 +23,9 @@ async def update_names(bot: Bot) -> None:
2223
while True:
2324
# Since we truncate the compute timedelta to seconds, we add one second to ensure
2425
# we go past midnight in the `seconds_to_sleep` set below.
25-
today_at_midnight = datetime.utcnow().replace(microsecond=0, second=0, minute=0, hour=0)
26+
today_at_midnight = arrow.utcnow().replace(microsecond=0, second=0, minute=0, hour=0)
2627
next_midnight = today_at_midnight + timedelta(days=1)
27-
await sleep_until(next_midnight)
28+
await sleep_until(next_midnight.datetime)
2829

2930
try:
3031
channel_0_name, channel_1_name, channel_2_name = await bot.api_client.get(

bot/exts/help_channels/_cog.py

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -125,14 +125,21 @@ async def claim_channel(self, message: discord.Message) -> None:
125125
"""
126126
log.info(f"Channel #{message.channel} was claimed by `{message.author.id}`.")
127127
await self.move_to_in_use(message.channel)
128-
await self._handle_role_change(message.author, message.author.add_roles)
129128

130-
await _message.pin(message)
129+
# Handle odd edge case of `message.author` not being a `discord.Member` (see bot#1839)
130+
if not isinstance(message.author, discord.Member):
131+
log.warning(
132+
f"{message.author} ({message.author.id}) isn't a member. Not giving cooldown role or sending DM."
133+
)
134+
else:
135+
await self._handle_role_change(message.author, message.author.add_roles)
131136

132-
try:
133-
await _message.dm_on_open(message)
134-
except Exception as e:
135-
log.warning("Error occurred while sending DM:", exc_info=e)
137+
try:
138+
await _message.dm_on_open(message)
139+
except Exception as e:
140+
log.warning("Error occurred while sending DM:", exc_info=e)
141+
142+
await _message.pin(message)
136143

137144
# Add user with channel for dormant check.
138145
await _caches.claimants.set(message.channel.id, message.author.id)

0 commit comments

Comments
 (0)