aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--bot/converters.py23
-rw-r--r--bot/exts/backend/error_handler.py47
-rw-r--r--bot/exts/filters/antispam.py15
-rw-r--r--bot/exts/filters/filtering.py19
-rw-r--r--bot/exts/fun/off_topic_names.py7
-rw-r--r--bot/exts/help_channels/_cog.py19
-rw-r--r--bot/exts/info/doc/_batch_parser.py22
-rw-r--r--bot/exts/info/doc/_cog.py1
-rw-r--r--bot/exts/info/doc/_redis_cache.py43
-rw-r--r--bot/exts/info/help.py6
-rw-r--r--bot/exts/info/information.py2
-rw-r--r--bot/exts/moderation/defcon.py8
-rw-r--r--bot/exts/moderation/incidents.py2
-rw-r--r--bot/exts/moderation/infraction/_scheduler.py12
-rw-r--r--bot/exts/moderation/infraction/_utils.py20
-rw-r--r--bot/exts/moderation/infraction/management.py9
-rw-r--r--bot/exts/moderation/modlog.py35
-rw-r--r--bot/exts/moderation/modpings.py5
-rw-r--r--bot/exts/moderation/voice_gate.py6
-rw-r--r--bot/exts/recruitment/talentpool/_review.py7
-rw-r--r--bot/exts/utils/internal.py13
-rw-r--r--bot/exts/utils/ping.py5
-rw-r--r--bot/exts/utils/reminders.py10
-rw-r--r--bot/monkey_patches.py7
-rw-r--r--bot/utils/checks.py3
-rw-r--r--bot/utils/regex.py18
-rw-r--r--bot/utils/time.py19
-rw-r--r--poetry.lock181
-rw-r--r--pyproject.toml2
-rw-r--r--tests/bot/exts/moderation/infraction/test_utils.py20
-rw-r--r--tests/bot/test_converters.py50
-rw-r--r--tests/bot/utils/test_time.py27
32 files changed, 360 insertions, 303 deletions
diff --git a/bot/converters.py b/bot/converters.py
index 4a4d3b544..f50acb9c6 100644
--- a/bot/converters.py
+++ b/bot/converters.py
@@ -2,7 +2,7 @@ from __future__ import annotations
import re
import typing as t
-from datetime import datetime
+from datetime import datetime, timezone
from ssl import CertificateError
import dateutil.parser
@@ -11,7 +11,7 @@ import discord
from aiohttp import ClientConnectorError
from dateutil.relativedelta import relativedelta
from discord.ext.commands import BadArgument, Bot, Context, Converter, IDConverter, MemberConverter, UserConverter
-from discord.utils import DISCORD_EPOCH, escape_markdown, snowflake_time
+from discord.utils import escape_markdown, snowflake_time
from bot import exts
from bot.api import ResponseCodeError
@@ -28,7 +28,7 @@ if t.TYPE_CHECKING:
log = get_logger(__name__)
-DISCORD_EPOCH_DT = datetime.utcfromtimestamp(DISCORD_EPOCH / 1000)
+DISCORD_EPOCH_DT = snowflake_time(0)
RE_USER_MENTION = re.compile(r"<@!?([0-9]+)>$")
@@ -71,10 +71,10 @@ class ValidDiscordServerInvite(Converter):
async def convert(self, ctx: Context, server_invite: str) -> dict:
"""Check whether the string is a valid Discord server invite."""
- invite_code = INVITE_RE.search(server_invite)
+ invite_code = INVITE_RE.match(server_invite)
if invite_code:
response = await ctx.bot.http_session.get(
- f"{URLs.discord_invite_api}/{invite_code[1]}"
+ f"{URLs.discord_invite_api}/{invite_code.group('invite')}"
)
if response.status != 404:
invite_data = await response.json()
@@ -273,14 +273,14 @@ class Snowflake(IDConverter):
snowflake = int(arg)
try:
- time = snowflake_time(snowflake).replace(tzinfo=None)
+ time = snowflake_time(snowflake)
except (OverflowError, OSError) as e:
# Not sure if this can ever even happen, but let's be safe.
raise BadArgument(f"{error}: {e}")
if time < DISCORD_EPOCH_DT:
raise BadArgument(f"{error}: timestamp is before the Discord epoch.")
- elif (datetime.utcnow() - time).days < -1:
+ elif (datetime.now(timezone.utc) - time).days < -1:
raise BadArgument(f"{error}: timestamp is too far into the future.")
return snowflake
@@ -387,7 +387,7 @@ class Duration(DurationDelta):
The converter supports the same symbols for each unit of time as its parent class.
"""
delta = await super().convert(ctx, duration)
- now = datetime.utcnow()
+ now = datetime.now(timezone.utc)
try:
return now + delta
@@ -443,8 +443,8 @@ class ISODateTime(Converter):
The converter is flexible in the formats it accepts, as it uses the `isoparse` method of
`dateutil.parser`. In general, it accepts datetime strings that start with a date,
optionally followed by a time. Specifying a timezone offset in the datetime string is
- supported, but the `datetime` object will be converted to UTC and will be returned without
- `tzinfo` as a timezone-unaware `datetime` object.
+ supported, but the `datetime` object will be converted to UTC. If no timezone is specified, the datetime will
+ be assumed to be in UTC already. In all cases, the returned object will have the UTC timezone.
See: https://dateutil.readthedocs.io/en/stable/parser.html#dateutil.parser.isoparse
@@ -470,7 +470,8 @@ class ISODateTime(Converter):
if dt.tzinfo:
dt = dt.astimezone(dateutil.tz.UTC)
- dt = dt.replace(tzinfo=None)
+ else: # Without a timezone, assume it represents UTC.
+ dt = dt.replace(tzinfo=dateutil.tz.UTC)
return dt
diff --git a/bot/exts/backend/error_handler.py b/bot/exts/backend/error_handler.py
index 7644b93ae..6ab6634a6 100644
--- a/bot/exts/backend/error_handler.py
+++ b/bot/exts/backend/error_handler.py
@@ -59,17 +59,23 @@ class ErrorHandler(Cog):
log.trace(f"Command {command} had its error already handled locally; ignoring.")
return
+ debug_message = (
+ f"Command {command} invoked by {ctx.message.author} with error "
+ f"{e.__class__.__name__}: {e}"
+ )
+
if isinstance(e, errors.CommandNotFound) and not getattr(ctx, "invoked_from_error_handler", False):
if await self.try_silence(ctx):
return
- # Try to look for a tag with the command's name
- await self.try_get_tag(ctx)
- return # Exit early to avoid logging.
+ await self.try_get_tag(ctx) # Try to look for a tag with the command's name
elif isinstance(e, errors.UserInputError):
+ log.debug(debug_message)
await self.handle_user_input_error(ctx, e)
elif isinstance(e, errors.CheckFailure):
+ log.debug(debug_message)
await self.handle_check_failure(ctx, e)
elif isinstance(e, errors.CommandOnCooldown):
+ log.debug(debug_message)
await ctx.send(e)
elif isinstance(e, errors.CommandInvokeError):
if isinstance(e.original, ResponseCodeError):
@@ -80,22 +86,16 @@ class ErrorHandler(Cog):
await ctx.send(f"Cannot infract that user. {e.original.reason}")
else:
await self.handle_unexpected_error(ctx, e.original)
- return # Exit early to avoid logging.
elif isinstance(e, errors.ConversionError):
if isinstance(e.original, ResponseCodeError):
await self.handle_api_error(ctx, e.original)
else:
await self.handle_unexpected_error(ctx, e.original)
- return # Exit early to avoid logging.
- elif not isinstance(e, errors.DisabledCommand):
+ elif isinstance(e, errors.DisabledCommand):
+ log.debug(debug_message)
+ else:
# MaxConcurrencyReached, ExtensionError
await self.handle_unexpected_error(ctx, e)
- return # Exit early to avoid logging.
-
- log.debug(
- f"Command {command} invoked by {ctx.message.author} with error "
- f"{e.__class__.__name__}: {e}"
- )
@staticmethod
def get_help_command(ctx: Context) -> t.Coroutine:
@@ -188,9 +188,6 @@ class ErrorHandler(Cog):
if not any(role.id in MODERATION_ROLES for role in ctx.author.roles):
await self.send_command_suggestion(ctx, ctx.invoked_with)
- # Return to not raise the exception
- return
-
async def send_command_suggestion(self, ctx: Context, command_name: str) -> None:
"""Sends user similar commands if any can be found."""
# No similar tag found, or tag on cooldown -
@@ -235,38 +232,32 @@ class ErrorHandler(Cog):
"""
if isinstance(e, errors.MissingRequiredArgument):
embed = self._get_error_embed("Missing required argument", e.param.name)
- await ctx.send(embed=embed)
- await self.get_help_command(ctx)
self.bot.stats.incr("errors.missing_required_argument")
elif isinstance(e, errors.TooManyArguments):
embed = self._get_error_embed("Too many arguments", str(e))
- await ctx.send(embed=embed)
- await self.get_help_command(ctx)
self.bot.stats.incr("errors.too_many_arguments")
elif isinstance(e, errors.BadArgument):
embed = self._get_error_embed("Bad argument", str(e))
- await ctx.send(embed=embed)
- await self.get_help_command(ctx)
self.bot.stats.incr("errors.bad_argument")
elif isinstance(e, errors.BadUnionArgument):
embed = self._get_error_embed("Bad argument", f"{e}\n{e.errors[-1]}")
- await ctx.send(embed=embed)
- await self.get_help_command(ctx)
self.bot.stats.incr("errors.bad_union_argument")
elif isinstance(e, errors.ArgumentParsingError):
embed = self._get_error_embed("Argument parsing error", str(e))
await ctx.send(embed=embed)
self.get_help_command(ctx).close()
self.bot.stats.incr("errors.argument_parsing_error")
+ return
else:
embed = self._get_error_embed(
"Input error",
"Something about your input seems off. Check the arguments and try again."
)
- await ctx.send(embed=embed)
- await self.get_help_command(ctx)
self.bot.stats.incr("errors.other_user_input_error")
+ await ctx.send(embed=embed)
+ await self.get_help_command(ctx)
+
@staticmethod
async def handle_check_failure(ctx: Context, e: errors.CheckFailure) -> None:
"""
@@ -299,8 +290,8 @@ class ErrorHandler(Cog):
async def handle_api_error(ctx: Context, e: ResponseCodeError) -> None:
"""Send an error message in `ctx` for ResponseCodeError and log it."""
if e.status == 404:
- await ctx.send("There does not seem to be anything matching your query.")
log.debug(f"API responded with 404 for command {ctx.command}")
+ await ctx.send("There does not seem to be anything matching your query.")
ctx.bot.stats.incr("errors.api_error_404")
elif e.status == 400:
content = await e.response.json()
@@ -308,12 +299,12 @@ class ErrorHandler(Cog):
await ctx.send("According to the API, your request is malformed.")
ctx.bot.stats.incr("errors.api_error_400")
elif 500 <= e.status < 600:
- await ctx.send("Sorry, there seems to be an internal issue with the API.")
log.warning(f"API responded with {e.status} for command {ctx.command}")
+ await ctx.send("Sorry, there seems to be an internal issue with the API.")
ctx.bot.stats.incr("errors.api_internal_server_error")
else:
- await ctx.send(f"Got an unexpected status code from the API (`{e.status}`).")
log.warning(f"Unexpected API response for command {ctx.command}: {e.status}")
+ await ctx.send(f"Got an unexpected status code from the API (`{e.status}`).")
ctx.bot.stats.incr(f"errors.api_error_{e.status}")
@staticmethod
diff --git a/bot/exts/filters/antispam.py b/bot/exts/filters/antispam.py
index 37ac70508..ddfd11231 100644
--- a/bot/exts/filters/antispam.py
+++ b/bot/exts/filters/antispam.py
@@ -2,11 +2,12 @@ import asyncio
from collections import defaultdict
from collections.abc import Mapping
from dataclasses import dataclass, field
-from datetime import datetime, timedelta
+from datetime import timedelta
from itertools import takewhile
from operator import attrgetter, itemgetter
from typing import Dict, Iterable, List, Set
+import arrow
from discord import Colour, Member, Message, NotFound, Object, TextChannel
from discord.ext.commands import Cog
@@ -177,21 +178,17 @@ class AntiSpam(Cog):
self.cache.append(message)
- earliest_relevant_at = datetime.utcnow() - timedelta(seconds=self.max_interval)
- relevant_messages = list(
- takewhile(lambda msg: msg.created_at.replace(tzinfo=None) > earliest_relevant_at, self.cache)
- )
+ earliest_relevant_at = arrow.utcnow() - timedelta(seconds=self.max_interval)
+ relevant_messages = list(takewhile(lambda msg: msg.created_at > earliest_relevant_at, self.cache))
for rule_name in AntiSpamConfig.rules:
rule_config = AntiSpamConfig.rules[rule_name]
rule_function = RULE_FUNCTION_MAPPING[rule_name]
# Create a list of messages that were sent in the interval that the rule cares about.
- latest_interesting_stamp = datetime.utcnow() - timedelta(seconds=rule_config['interval'])
+ latest_interesting_stamp = arrow.utcnow() - timedelta(seconds=rule_config['interval'])
messages_for_rule = list(
- takewhile(
- lambda msg: msg.created_at.replace(tzinfo=None) > latest_interesting_stamp, relevant_messages
- )
+ takewhile(lambda msg: msg.created_at > latest_interesting_stamp, relevant_messages)
)
result = await rule_function(message, messages_for_rule, rule_config)
diff --git a/bot/exts/filters/filtering.py b/bot/exts/filters/filtering.py
index b20a9c2c9..bda4e7ac2 100644
--- a/bot/exts/filters/filtering.py
+++ b/bot/exts/filters/filtering.py
@@ -3,7 +3,8 @@ import re
from datetime import datetime, timedelta
from typing import Any, Dict, List, Mapping, NamedTuple, Optional, Tuple, Union
-import dateutil
+import arrow
+import dateutil.parser
import discord.errors
import regex
from async_rediscache import RedisCache
@@ -209,8 +210,8 @@ class Filtering(Cog):
async def check_send_alert(self, member: Member) -> bool:
"""When there is less than 3 days after last alert, return `False`, otherwise `True`."""
if last_alert := await self.name_alerts.get(member.id):
- last_alert = datetime.utcfromtimestamp(last_alert)
- if datetime.utcnow() - timedelta(days=DAYS_BETWEEN_ALERTS) < last_alert:
+ last_alert = arrow.get(last_alert)
+ if arrow.utcnow() - timedelta(days=DAYS_BETWEEN_ALERTS) < last_alert:
log.trace(f"Last alert was too recent for {member}'s nickname.")
return False
@@ -244,7 +245,7 @@ class Filtering(Cog):
)
# Update time when alert sent
- await self.name_alerts.set(member.id, datetime.utcnow().timestamp())
+ await self.name_alerts.set(member.id, arrow.utcnow().timestamp())
async def filter_eval(self, result: str, msg: Message) -> bool:
"""
@@ -543,7 +544,7 @@ class Filtering(Cog):
# discord\.gg/gdudes-pony-farm
text = text.replace("\\", "")
- invites = INVITE_RE.findall(text)
+ invites = [m.group("invite") for m in INVITE_RE.finditer(text)]
invite_data = dict()
for invite in invites:
if invite in invite_data:
@@ -639,7 +640,7 @@ class Filtering(Cog):
def schedule_msg_delete(self, msg: dict) -> None:
"""Delete an offensive message once its deletion date is reached."""
- delete_at = dateutil.parser.isoparse(msg['delete_date']).replace(tzinfo=None)
+ delete_at = dateutil.parser.isoparse(msg['delete_date'])
self.scheduler.schedule_at(delete_at, msg['id'], self.delete_offensive_msg(msg))
async def reschedule_offensive_msg_deletion(self) -> None:
@@ -647,17 +648,17 @@ class Filtering(Cog):
await self.bot.wait_until_ready()
response = await self.bot.api_client.get('bot/offensive-messages',)
- now = datetime.utcnow()
+ now = arrow.utcnow()
for msg in response:
- delete_at = dateutil.parser.isoparse(msg['delete_date']).replace(tzinfo=None)
+ delete_at = dateutil.parser.isoparse(msg['delete_date'])
if delete_at < now:
await self.delete_offensive_msg(msg)
else:
self.schedule_msg_delete(msg)
- async def delete_offensive_msg(self, msg: Mapping[str, str]) -> None:
+ async def delete_offensive_msg(self, msg: Mapping[str, int]) -> None:
"""Delete an offensive message, and then delete it from the db."""
try:
channel = self.bot.get_channel(msg['channel_id'])
diff --git a/bot/exts/fun/off_topic_names.py b/bot/exts/fun/off_topic_names.py
index 427667c66..7df1d172d 100644
--- a/bot/exts/fun/off_topic_names.py
+++ b/bot/exts/fun/off_topic_names.py
@@ -1,6 +1,7 @@
import difflib
-from datetime import datetime, timedelta
+from datetime import timedelta
+import arrow
from discord import Colour, Embed
from discord.ext.commands import Cog, Context, group, has_any_role
from discord.utils import sleep_until
@@ -22,9 +23,9 @@ async def update_names(bot: Bot) -> None:
while True:
# Since we truncate the compute timedelta to seconds, we add one second to ensure
# we go past midnight in the `seconds_to_sleep` set below.
- today_at_midnight = datetime.utcnow().replace(microsecond=0, second=0, minute=0, hour=0)
+ today_at_midnight = arrow.utcnow().replace(microsecond=0, second=0, minute=0, hour=0)
next_midnight = today_at_midnight + timedelta(days=1)
- await sleep_until(next_midnight)
+ await sleep_until(next_midnight.datetime)
try:
channel_0_name, channel_1_name, channel_2_name = await bot.api_client.get(
diff --git a/bot/exts/help_channels/_cog.py b/bot/exts/help_channels/_cog.py
index 498305b47..3c6cf7f26 100644
--- a/bot/exts/help_channels/_cog.py
+++ b/bot/exts/help_channels/_cog.py
@@ -125,14 +125,21 @@ class HelpChannels(commands.Cog):
"""
log.info(f"Channel #{message.channel} was claimed by `{message.author.id}`.")
await self.move_to_in_use(message.channel)
- await self._handle_role_change(message.author, message.author.add_roles)
- await _message.pin(message)
+ # Handle odd edge case of `message.author` not being a `discord.Member` (see bot#1839)
+ if not isinstance(message.author, discord.Member):
+ log.warning(
+ f"{message.author} ({message.author.id}) isn't a member. Not giving cooldown role or sending DM."
+ )
+ else:
+ await self._handle_role_change(message.author, message.author.add_roles)
- try:
- await _message.dm_on_open(message)
- except Exception as e:
- log.warning("Error occurred while sending DM:", exc_info=e)
+ try:
+ await _message.dm_on_open(message)
+ except Exception as e:
+ log.warning("Error occurred while sending DM:", exc_info=e)
+
+ await _message.pin(message)
# Add user with channel for dormant check.
await _caches.claimants.set(message.channel.id, message.author.id)
diff --git a/bot/exts/info/doc/_batch_parser.py b/bot/exts/info/doc/_batch_parser.py
index 92f814c9d..c27f28eac 100644
--- a/bot/exts/info/doc/_batch_parser.py
+++ b/bot/exts/info/doc/_batch_parser.py
@@ -17,6 +17,7 @@ from bot.utils import scheduling
from . import _cog, doc_cache
from ._parsing import get_symbol_markdown
+from ._redis_cache import StaleItemCounter
log = get_logger(__name__)
@@ -24,6 +25,8 @@ log = get_logger(__name__)
class StaleInventoryNotifier:
"""Handle sending notifications about stale inventories through `DocItem`s to dev log."""
+ symbol_counter = StaleItemCounter()
+
def __init__(self):
self._init_task = scheduling.create_task(
self._init_channel(),
@@ -40,13 +43,16 @@ class StaleInventoryNotifier:
async def send_warning(self, doc_item: _cog.DocItem) -> None:
"""Send a warning to dev log if one wasn't already sent for `item`'s url."""
if doc_item.url not in self._warned_urls:
- self._warned_urls.add(doc_item.url)
- await self._init_task
- embed = discord.Embed(
- description=f"Doc item `{doc_item.symbol_id=}` present in loaded documentation inventories "
- f"not found on [site]({doc_item.url}), inventories may need to be refreshed."
- )
- await self._dev_log.send(embed=embed)
+ # Only warn if the item got less than 3 warnings
+ # or if it has been more than 3 weeks since the last warning
+ if await self.symbol_counter.increment_for(doc_item) < 3:
+ self._warned_urls.add(doc_item.url)
+ await self._init_task
+ embed = discord.Embed(
+ description=f"Doc item `{doc_item.symbol_id=}` present in loaded documentation inventories "
+ f"not found on [site]({doc_item.url}), inventories may need to be refreshed."
+ )
+ await self._dev_log.send(embed=embed)
class QueueItem(NamedTuple):
@@ -103,7 +109,7 @@ class BatchParser:
if doc_item not in self._item_futures and doc_item not in self._queue:
self._item_futures[doc_item].user_requested = True
- async with bot.instance.http_session.get(doc_item.url) as response:
+ async with bot.instance.http_session.get(doc_item.url, raise_for_status=True) as response:
soup = await bot.instance.loop.run_in_executor(
None,
BeautifulSoup,
diff --git a/bot/exts/info/doc/_cog.py b/bot/exts/info/doc/_cog.py
index fbbcd4a10..ebf5f5932 100644
--- a/bot/exts/info/doc/_cog.py
+++ b/bot/exts/info/doc/_cog.py
@@ -464,6 +464,7 @@ class DocCog(commands.Cog):
) -> None:
"""Clear the persistent redis cache for `package`."""
if await doc_cache.delete(package_name):
+ await self.item_fetcher.stale_inventory_notifier.symbol_counter.delete()
await ctx.send(f"Successfully cleared the cache for `{package_name}`.")
else:
await ctx.send("No keys matching the package found.")
diff --git a/bot/exts/info/doc/_redis_cache.py b/bot/exts/info/doc/_redis_cache.py
index 79648893a..107f2344f 100644
--- a/bot/exts/info/doc/_redis_cache.py
+++ b/bot/exts/info/doc/_redis_cache.py
@@ -25,8 +25,7 @@ class DocRedisCache(RedisObject):
All keys from a single page are stored together, expiring a week after the first set.
"""
- url_key = remove_suffix(item.relative_url_path, ".html")
- redis_key = f"{self.namespace}:{item.package}:{url_key}"
+ redis_key = f"{self.namespace}:{item_key(item)}"
needs_expire = False
with await self._get_pool_connection() as connection:
@@ -44,10 +43,36 @@ class DocRedisCache(RedisObject):
@namespace_lock
async def get(self, item: DocItem) -> Optional[str]:
"""Return the Markdown content of the symbol `item` if it exists."""
- url_key = remove_suffix(item.relative_url_path, ".html")
+ with await self._get_pool_connection() as connection:
+ return await connection.hget(f"{self.namespace}:{item_key(item)}", item.symbol_id, encoding="utf8")
+ @namespace_lock
+ async def delete(self, package: str) -> bool:
+ """Remove all values for `package`; return True if at least one key was deleted, False otherwise."""
+ with await self._get_pool_connection() as connection:
+ package_keys = [
+ package_key async for package_key in connection.iscan(match=f"{self.namespace}:{package}:*")
+ ]
+ if package_keys:
+ await connection.delete(*package_keys)
+ return True
+ return False
+
+
+class StaleItemCounter(RedisObject):
+ """Manage increment counters for stale `DocItem`s."""
+
+ @namespace_lock
+ async def increment_for(self, item: DocItem) -> int:
+ """
+ Increment the counter for `item` by 1, set it to expire in 3 weeks and return the new value.
+
+ If the counter didn't exist, initialize it with 1.
+ """
+ key = f"{self.namespace}:{item_key(item)}:{item.symbol_id}"
with await self._get_pool_connection() as connection:
- return await connection.hget(f"{self.namespace}:{item.package}:{url_key}", item.symbol_id, encoding="utf8")
+ await connection.expire(key, WEEK_SECONDS * 3)
+ return int(await connection.incr(key))
@namespace_lock
async def delete(self, package: str) -> bool:
@@ -62,10 +87,6 @@ class DocRedisCache(RedisObject):
return False
-def remove_suffix(string: str, suffix: str) -> str:
- """Remove `suffix` from end of `string`."""
- # TODO replace usages with str.removesuffix on 3.9
- if string.endswith(suffix):
- return string[:-len(suffix)]
- else:
- return string
+def item_key(item: DocItem) -> str:
+ """Get the redis redis key string from `item`."""
+ return f"{item.package}:{item.relative_url_path.removesuffix('.html')}"
diff --git a/bot/exts/info/help.py b/bot/exts/info/help.py
index f413caded..743dfdd3f 100644
--- a/bot/exts/info/help.py
+++ b/bot/exts/info/help.py
@@ -1,4 +1,5 @@
import itertools
+import re
from collections import namedtuple
from contextlib import suppress
from typing import List, Union
@@ -179,7 +180,10 @@ class CustomHelpCommand(HelpCommand):
except CommandError:
command_details += NOT_ALLOWED_TO_RUN_MESSAGE
- command_details += f"*{command.help or 'No details provided.'}*\n"
+ # Remove line breaks from docstrings, if not used to separate paragraphs.
+ # Allow overriding this behaviour via putting \u2003 at the start of a line.
+ formatted_doc = re.sub("(?<!\n)\n(?![\n\u2003])", " ", command.help)
+ command_details += f"*{formatted_doc or 'No details provided.'}*\n"
embed.description = command_details
return embed
diff --git a/bot/exts/info/information.py b/bot/exts/info/information.py
index 1b3e28e79..0dcb8de11 100644
--- a/bot/exts/info/information.py
+++ b/bot/exts/info/information.py
@@ -200,7 +200,7 @@ class Information(Cog):
f"\nRoles: {num_roles}"
f"\nMember status: {member_status}"
)
- embed.set_thumbnail(url=ctx.guild.icon_url)
+ embed.set_thumbnail(url=ctx.guild.icon.url)
# Members
total_members = f"{ctx.guild.member_count:,}"
diff --git a/bot/exts/moderation/defcon.py b/bot/exts/moderation/defcon.py
index 80ba10112..822a87b61 100644
--- a/bot/exts/moderation/defcon.py
+++ b/bot/exts/moderation/defcon.py
@@ -4,6 +4,7 @@ from datetime import datetime
from enum import Enum
from typing import Optional, Union
+import arrow
from aioredis import RedisError
from async_rediscache import RedisCache
from dateutil.relativedelta import relativedelta
@@ -109,9 +110,9 @@ class Defcon(Cog):
async def on_member_join(self, member: Member) -> None:
"""Check newly joining users to see if they meet the account age threshold."""
if self.threshold:
- now = datetime.utcnow()
+ now = arrow.utcnow()
- if now - member.created_at.replace(tzinfo=None) < relativedelta_to_timedelta(self.threshold):
+ if now - member.created_at < relativedelta_to_timedelta(self.threshold):
log.info(f"Rejecting user {member}: Account is too new")
message_sent = False
@@ -254,7 +255,8 @@ class Defcon(Cog):
expiry_message = ""
if expiry:
- expiry_message = f" for the next {humanize_delta(relativedelta(expiry, datetime.utcnow()), max_units=2)}"
+ activity_duration = relativedelta(expiry, arrow.utcnow().datetime)
+ expiry_message = f" for the next {humanize_delta(activity_duration, max_units=2)}"
if self.threshold:
channel_message = (
diff --git a/bot/exts/moderation/incidents.py b/bot/exts/moderation/incidents.py
index 097fa36f1..e265e29d3 100644
--- a/bot/exts/moderation/incidents.py
+++ b/bot/exts/moderation/incidents.py
@@ -105,7 +105,7 @@ async def make_embed(incident: discord.Message, outcome: Signal, actioned_by: di
else:
embed.set_author(name="[Failed to relay attachment]", url=attachment.proxy_url) # Embed links the file
else:
- file = None
+ file = discord.utils.MISSING
return embed, file
diff --git a/bot/exts/moderation/infraction/_scheduler.py b/bot/exts/moderation/infraction/_scheduler.py
index c07b043be..762eb6afa 100644
--- a/bot/exts/moderation/infraction/_scheduler.py
+++ b/bot/exts/moderation/infraction/_scheduler.py
@@ -1,9 +1,9 @@
import textwrap
import typing as t
from abc import abstractmethod
-from datetime import datetime
from gettext import ngettext
+import arrow
import dateutil.parser
import discord
from discord.ext.commands import Context
@@ -67,7 +67,7 @@ class InfractionScheduler:
# We make sure to fire this
if to_schedule:
next_reschedule_point = max(
- dateutil.parser.isoparse(infr["expires_at"]).replace(tzinfo=None) for infr in to_schedule
+ dateutil.parser.isoparse(infr["expires_at"]) for infr in to_schedule
)
log.trace("Will reschedule remaining infractions at %s", next_reschedule_point)
@@ -83,8 +83,8 @@ class InfractionScheduler:
"""Reapply an infraction if it's still active or deactivate it if less than 60 sec left."""
if infraction["expires_at"] is not None:
# Calculate the time remaining, in seconds, for the mute.
- expiry = dateutil.parser.isoparse(infraction["expires_at"]).replace(tzinfo=None)
- delta = (expiry - datetime.utcnow()).total_seconds()
+ expiry = dateutil.parser.isoparse(infraction["expires_at"])
+ delta = (expiry - arrow.utcnow()).total_seconds()
else:
# If the infraction is permanent, it is not possible to get the time remaining.
delta = None
@@ -382,7 +382,7 @@ class InfractionScheduler:
log.info(f"Marking infraction #{id_} as inactive (expired).")
- expiry = dateutil.parser.isoparse(expiry).replace(tzinfo=None) if expiry else None
+ expiry = dateutil.parser.isoparse(expiry) if expiry else None
created = time.format_infraction_with_duration(inserted_at, expiry)
log_content = None
@@ -503,5 +503,5 @@ class InfractionScheduler:
At the time of expiration, the infraction is marked as inactive on the website and the
expiration task is cancelled.
"""
- expiry = dateutil.parser.isoparse(infraction["expires_at"]).replace(tzinfo=None)
+ expiry = dateutil.parser.isoparse(infraction["expires_at"])
self.scheduler.schedule_at(expiry, infraction["id"], self.deactivate_infraction(infraction))
diff --git a/bot/exts/moderation/infraction/_utils.py b/bot/exts/moderation/infraction/_utils.py
index 89718c857..c0ef80e3d 100644
--- a/bot/exts/moderation/infraction/_utils.py
+++ b/bot/exts/moderation/infraction/_utils.py
@@ -27,16 +27,18 @@ RULES_URL = "https://pythondiscord.com/pages/rules"
# Type aliases
Infraction = t.Dict[str, t.Union[str, int, bool]]
-APPEAL_EMAIL = "[email protected]"
+APPEAL_SERVER_INVITE = "https://discord.gg/WXrCJxWBnm"
INFRACTION_TITLE = "Please review our rules"
-INFRACTION_APPEAL_EMAIL_FOOTER = f"To appeal this infraction, send an e-mail to {APPEAL_EMAIL}"
+INFRACTION_APPEAL_SERVER_FOOTER = f"\n\nTo appeal this infraction, join our [appeals server]({APPEAL_SERVER_INVITE})."
INFRACTION_APPEAL_MODMAIL_FOOTER = (
- 'If you would like to discuss or appeal this infraction, '
- 'send a message to the ModMail bot'
+ '\n\nIf you would like to discuss or appeal this infraction, '
+ 'send a message to the ModMail bot.'
)
INFRACTION_AUTHOR_NAME = "Infraction information"
+LONGEST_EXTRAS = max(len(INFRACTION_APPEAL_SERVER_FOOTER), len(INFRACTION_APPEAL_MODMAIL_FOOTER))
+
INFRACTION_DESCRIPTION_TEMPLATE = (
"**Type:** {type}\n"
"**Expires:** {expires}\n"
@@ -170,8 +172,10 @@ async def notify_infraction(
)
# For case when other fields than reason is too long and this reach limit, then force-shorten string
- if len(text) > 4096:
- text = f"{text[:4093]}..."
+ if len(text) > 4096 - LONGEST_EXTRAS:
+ text = f"{text[:4093-LONGEST_EXTRAS]}..."
+
+ text += INFRACTION_APPEAL_SERVER_FOOTER if infr_type.lower() == 'ban' else INFRACTION_APPEAL_MODMAIL_FOOTER
embed = discord.Embed(
description=text,
@@ -182,10 +186,6 @@ async def notify_infraction(
embed.title = INFRACTION_TITLE
embed.url = RULES_URL
- embed.set_footer(
- text=INFRACTION_APPEAL_EMAIL_FOOTER if infr_type == 'Ban' else INFRACTION_APPEAL_MODMAIL_FOOTER
- )
-
return await send_private_embed(user, embed)
diff --git a/bot/exts/moderation/infraction/management.py b/bot/exts/moderation/infraction/management.py
index b1c8b64dc..1cd259a4b 100644
--- a/bot/exts/moderation/infraction/management.py
+++ b/bot/exts/moderation/infraction/management.py
@@ -1,6 +1,6 @@
import textwrap
import typing as t
-from datetime import datetime
+from datetime import datetime, timezone
import dateutil.parser
import discord
@@ -314,8 +314,11 @@ class ModManagement(commands.Cog):
if expires_at is None:
duration = "*Permanent*"
else:
- date_from = datetime.fromtimestamp(float(time.DISCORD_TIMESTAMP_REGEX.match(created).group(1)))
- date_to = dateutil.parser.isoparse(expires_at).replace(tzinfo=None)
+ date_from = datetime.fromtimestamp(
+ float(time.DISCORD_TIMESTAMP_REGEX.match(created).group(1)),
+ timezone.utc
+ )
+ date_to = dateutil.parser.isoparse(expires_at)
duration = humanize_delta(relativedelta(date_to, date_from))
lines = textwrap.dedent(f"""
diff --git a/bot/exts/moderation/modlog.py b/bot/exts/moderation/modlog.py
index b09eb2d14..6fcf43d8a 100644
--- a/bot/exts/moderation/modlog.py
+++ b/bot/exts/moderation/modlog.py
@@ -2,7 +2,7 @@ import asyncio
import difflib
import itertools
import typing as t
-from datetime import datetime
+from datetime import datetime, timezone
from itertools import zip_longest
import discord
@@ -58,7 +58,7 @@ class ModLog(Cog, name="ModLog"):
'bot/deleted-messages',
json={
'actor': actor_id,
- 'creation': datetime.utcnow().isoformat(),
+ 'creation': datetime.now(timezone.utc).isoformat(),
'deletedmessage_set': [
{
'id': message.id,
@@ -378,7 +378,7 @@ class ModLog(Cog, name="ModLog"):
await self.send_log_message(
Icons.guild_update, Colour.blurple(),
"Guild updated", message,
- thumbnail=after.icon_url_as(format="png")
+ thumbnail=after.icon.with_static_format("png")
)
@Cog.listener()
@@ -404,8 +404,8 @@ class ModLog(Cog, name="ModLog"):
if member.guild.id != GuildConstant.id:
return
- now = datetime.utcnow()
- difference = abs(relativedelta(now, member.created_at.replace(tzinfo=None)))
+ now = datetime.now(timezone.utc)
+ difference = abs(relativedelta(now, member.created_at))
message = format_user(member) + "\n\n**Account age:** " + humanize_delta(difference)
@@ -539,7 +539,7 @@ class ModLog(Cog, name="ModLog"):
channel = self.bot.get_channel(channel_id)
# Ignore not found channels, DMs, and messages outside of the main guild.
- if not channel or channel.guild and channel.guild.id != GuildConstant.id:
+ if not channel or not hasattr(channel, "guild") or channel.guild.id != GuildConstant.id:
return True
# Look at the parent channel of a thread.
@@ -786,11 +786,11 @@ class ModLog(Cog, name="ModLog"):
return
if not before.archived and after.archived:
- colour = Colour.red()
+ colour = Colours.soft_red
action = "archived"
icon = Icons.hash_red
elif before.archived and not after.archived:
- colour = Colour.green()
+ colour = Colours.soft_green
action = "un-archived"
icon = Icons.hash_green
else:
@@ -800,7 +800,10 @@ class ModLog(Cog, name="ModLog"):
icon,
colour,
f"Thread {action}",
- f"Thread {after.mention} (`{after.id}`) from {after.parent.mention} (`{after.parent.id}`) was {action}"
+ (
+ f"Thread {after.mention} ({after.name}, `{after.id}`) from {after.parent.mention} "
+ f"(`{after.parent.id}`) was {action}"
+ )
)
@Cog.listener()
@@ -808,9 +811,12 @@ class ModLog(Cog, name="ModLog"):
"""Log thread deletion."""
await self.send_log_message(
Icons.hash_red,
- Colour.red(),
+ Colours.soft_red,
"Thread deleted",
- f"Thread {thread.mention} (`{thread.id}`) from {thread.parent.mention} (`{thread.parent.id}`) deleted"
+ (
+ f"Thread {thread.mention} ({thread.name}, `{thread.id}`) from {thread.parent.mention} "
+ f"(`{thread.parent.id}`) deleted"
+ )
)
@Cog.listener()
@@ -823,9 +829,12 @@ class ModLog(Cog, name="ModLog"):
await self.send_log_message(
Icons.hash_green,
- Colour.green(),
+ Colours.soft_green,
"Thread created",
- f"Thread {thread.mention} (`{thread.id}`) from {thread.parent.mention} (`{thread.parent.id}`) created"
+ (
+ f"Thread {thread.mention} ({thread.name}, `{thread.id}`) from {thread.parent.mention} "
+ f"(`{thread.parent.id}`) created"
+ )
)
@Cog.listener()
diff --git a/bot/exts/moderation/modpings.py b/bot/exts/moderation/modpings.py
index a7ccb8162..f67d8f662 100644
--- a/bot/exts/moderation/modpings.py
+++ b/bot/exts/moderation/modpings.py
@@ -1,5 +1,6 @@
import datetime
+import arrow
from async_rediscache import RedisCache
from dateutil.parser import isoparse
from discord import Embed, Member
@@ -57,7 +58,7 @@ class ModPings(Cog):
if mod.id not in pings_off:
await self.reapply_role(mod)
else:
- expiry = isoparse(pings_off[mod.id]).replace(tzinfo=None)
+ expiry = isoparse(pings_off[mod.id])
self._role_scheduler.schedule_at(expiry, mod.id, self.reapply_role(mod))
async def reapply_role(self, mod: Member) -> None:
@@ -92,7 +93,7 @@ class ModPings(Cog):
The duration cannot be longer than 30 days.
"""
- delta = duration - datetime.datetime.utcnow()
+ delta = duration - arrow.utcnow()
if delta > datetime.timedelta(days=30):
await ctx.send(":x: Cannot remove the role for longer than 30 days.")
return
diff --git a/bot/exts/moderation/voice_gate.py b/bot/exts/moderation/voice_gate.py
index 8fdc7c76b..31799ec73 100644
--- a/bot/exts/moderation/voice_gate.py
+++ b/bot/exts/moderation/voice_gate.py
@@ -1,7 +1,8 @@
import asyncio
from contextlib import suppress
-from datetime import datetime, timedelta
+from datetime import timedelta
+import arrow
import discord
from async_rediscache import RedisCache
from discord import Colour, Member, VoiceState
@@ -166,8 +167,7 @@ class VoiceGate(Cog):
checks = {
"joined_at": (
- ctx.author.joined_at.replace(tzinfo=None) > datetime.utcnow()
- - timedelta(days=GateConf.minimum_days_member)
+ ctx.author.joined_at > arrow.utcnow() - timedelta(days=GateConf.minimum_days_member)
),
"total_messages": data["total_messages"] < GateConf.minimum_messages,
"voice_banned": data["voice_banned"],
diff --git a/bot/exts/recruitment/talentpool/_review.py b/bot/exts/recruitment/talentpool/_review.py
index dcf73c2cb..d880c524c 100644
--- a/bot/exts/recruitment/talentpool/_review.py
+++ b/bot/exts/recruitment/talentpool/_review.py
@@ -8,6 +8,7 @@ from collections import Counter
from datetime import datetime, timedelta
from typing import List, Optional, Union
+import arrow
from dateutil.parser import isoparse
from discord import Embed, Emoji, Member, Message, NoMoreItems, PartialMessage, TextChannel
from discord.ext.commands import Context
@@ -68,11 +69,11 @@ class Reviewer:
log.trace(f"Scheduling review of user with ID {user_id}")
user_data = self._pool.cache.get(user_id)
- inserted_at = isoparse(user_data['inserted_at']).replace(tzinfo=None)
+ inserted_at = isoparse(user_data['inserted_at'])
review_at = inserted_at + timedelta(days=MAX_DAYS_IN_POOL)
# If it's over a day overdue, it's probably an old nomination and shouldn't be automatically reviewed.
- if datetime.utcnow() - review_at < timedelta(days=1):
+ if arrow.utcnow() - review_at < timedelta(days=1):
self._review_scheduler.schedule_at(review_at, user_id, self.post_review(user_id, update_database=True))
async def post_review(self, user_id: int, update_database: bool) -> None:
@@ -347,7 +348,7 @@ class Reviewer:
nomination_times = f"{num_entries} times" if num_entries > 1 else "once"
rejection_times = f"{len(history)} times" if len(history) > 1 else "once"
- end_time = time_since(isoparse(history[0]['ended_at']).replace(tzinfo=None))
+ end_time = time_since(isoparse(history[0]['ended_at']))
review = (
f"They were nominated **{nomination_times}** before"
diff --git a/bot/exts/utils/internal.py b/bot/exts/utils/internal.py
index 879735945..165b5917d 100644
--- a/bot/exts/utils/internal.py
+++ b/bot/exts/utils/internal.py
@@ -5,10 +5,10 @@ import re
import textwrap
import traceback
from collections import Counter
-from datetime import datetime
from io import StringIO
from typing import Any, Optional, Tuple
+import arrow
import discord
from discord.ext.commands import Cog, Context, group, has_any_role, is_owner
@@ -29,7 +29,7 @@ class Internal(Cog):
self.ln = 0
self.stdout = StringIO()
- self.socket_since = datetime.utcnow()
+ self.socket_since = arrow.utcnow()
self.socket_event_total = 0
self.socket_events = Counter()
@@ -37,11 +37,10 @@ class Internal(Cog):
self.eval.add_check(is_owner().predicate)
@Cog.listener()
- async def on_socket_response(self, msg: dict) -> None:
+ async def on_socket_event_type(self, event_type: str) -> None:
"""When a websocket event is received, increase our counters."""
- if event_type := msg.get("t"):
- self.socket_event_total += 1
- self.socket_events[event_type] += 1
+ self.socket_event_total += 1
+ self.socket_events[event_type] += 1
def _format(self, inp: str, out: Any) -> Tuple[str, Optional[discord.Embed]]:
"""Format the eval output into a string & attempt to format it into an Embed."""
@@ -237,7 +236,7 @@ async def func(): # (None,) -> Any
@has_any_role(Roles.admins, Roles.owners, Roles.core_developers)
async def socketstats(self, ctx: Context) -> None:
"""Fetch information on the socket events received from Discord."""
- running_s = (datetime.utcnow() - self.socket_since).total_seconds()
+ running_s = (arrow.utcnow() - self.socket_since).total_seconds()
per_s = self.socket_event_total / running_s
diff --git a/bot/exts/utils/ping.py b/bot/exts/utils/ping.py
index 43d371d87..9fb5b7b8f 100644
--- a/bot/exts/utils/ping.py
+++ b/bot/exts/utils/ping.py
@@ -1,5 +1,4 @@
-from datetime import datetime
-
+import arrow
from aiohttp import client_exceptions
from discord import Embed
from discord.ext import commands
@@ -32,7 +31,7 @@ class Latency(commands.Cog):
"""
# datetime.datetime objects do not have the "milliseconds" attribute.
# It must be converted to seconds before converting to milliseconds.
- bot_ping = (datetime.utcnow() - ctx.message.created_at.replace(tzinfo=None)).total_seconds() * 1000
+ bot_ping = (arrow.utcnow() - ctx.message.created_at).total_seconds() * 1000
if bot_ping <= 0:
bot_ping = "Your clock is out of sync, could not calculate ping."
else:
diff --git a/bot/exts/utils/reminders.py b/bot/exts/utils/reminders.py
index 3cb9307a9..3dbcc4513 100644
--- a/bot/exts/utils/reminders.py
+++ b/bot/exts/utils/reminders.py
@@ -1,7 +1,7 @@
import random
import textwrap
import typing as t
-from datetime import datetime
+from datetime import datetime, timezone
from operator import itemgetter
import discord
@@ -52,14 +52,14 @@ class Reminders(Cog):
params={'active': 'true'}
)
- now = datetime.utcnow()
+ now = datetime.now(timezone.utc)
for reminder in response:
is_valid, *_ = self.ensure_valid_reminder(reminder)
if not is_valid:
continue
- remind_at = isoparse(reminder['expiration']).replace(tzinfo=None)
+ remind_at = isoparse(reminder['expiration'])
# If the reminder is already overdue ...
if remind_at < now:
@@ -144,7 +144,7 @@ class Reminders(Cog):
def schedule_reminder(self, reminder: dict) -> None:
"""A coroutine which sends the reminder once the time is reached, and cancels the running task."""
- reminder_datetime = isoparse(reminder['expiration']).replace(tzinfo=None)
+ reminder_datetime = isoparse(reminder['expiration'])
self.scheduler.schedule_at(reminder_datetime, reminder["id"], self.send_reminder(reminder))
async def _edit_reminder(self, reminder_id: int, payload: dict) -> dict:
@@ -333,7 +333,7 @@ class Reminders(Cog):
for content, remind_at, id_, mentions in reminders:
# Parse and humanize the time, make it pretty :D
- remind_datetime = isoparse(remind_at).replace(tzinfo=None)
+ remind_datetime = isoparse(remind_at)
time = discord_timestamp(remind_datetime, TimestampFormats.RELATIVE)
mentions = ", ".join([
diff --git a/bot/monkey_patches.py b/bot/monkey_patches.py
index e56a19da2..23482f7c3 100644
--- a/bot/monkey_patches.py
+++ b/bot/monkey_patches.py
@@ -1,5 +1,6 @@
-from datetime import datetime, timedelta
+from datetime import timedelta
+import arrow
from discord import Forbidden, http
from discord.ext import commands
@@ -38,13 +39,13 @@ def patch_typing() -> None:
async def honeybadger_type(self, channel_id: int) -> None: # noqa: ANN001
nonlocal last_403
- if last_403 and (datetime.utcnow() - last_403) < timedelta(minutes=5):
+ if last_403 and (arrow.utcnow() - last_403) < timedelta(minutes=5):
log.warning("Not sending typing event, we got a 403 less than 5 minutes ago.")
return
try:
await original(self, channel_id)
except Forbidden:
- last_403 = datetime.utcnow()
+ last_403 = arrow.utcnow()
log.warning("Got a 403 from typing event!")
pass
diff --git a/bot/utils/checks.py b/bot/utils/checks.py
index e7f2cfbda..188285684 100644
--- a/bot/utils/checks.py
+++ b/bot/utils/checks.py
@@ -1,4 +1,3 @@
-import datetime
from typing import Callable, Container, Iterable, Optional, Union
from discord.ext.commands import (
@@ -137,7 +136,7 @@ def cooldown_with_role_bypass(rate: int, per: float, type: BucketType = BucketTy
return
# cooldown logic, taken from discord.py internals
- current = ctx.message.created_at.replace(tzinfo=datetime.timezone.utc).timestamp()
+ current = ctx.message.created_at.timestamp()
bucket = buckets.get_bucket(ctx.message)
retry_after = bucket.update_rate_limit(current)
if retry_after:
diff --git a/bot/utils/regex.py b/bot/utils/regex.py
index 7bad1e627..d77f5950b 100644
--- a/bot/utils/regex.py
+++ b/bot/utils/regex.py
@@ -1,14 +1,14 @@
import re
INVITE_RE = re.compile(
- r"(?:discord(?:[\.,]|dot)gg|" # Could be discord.gg/
- r"discord(?:[\.,]|dot)com(?:\/|slash)invite|" # or discord.com/invite/
- r"discordapp(?:[\.,]|dot)com(?:\/|slash)invite|" # or discordapp.com/invite/
- r"discord(?:[\.,]|dot)me|" # or discord.me
- r"discord(?:[\.,]|dot)li|" # or discord.li
- r"discord(?:[\.,]|dot)io|" # or discord.io.
- r"(?:[\.,]|dot)gg" # or .gg/
- r")(?:[\/]|slash)" # / or 'slash'
- r"([a-zA-Z0-9\-]+)", # the invite code itself
+ r"(discord([\.,]|dot)gg|" # Could be discord.gg/
+ r"discord([\.,]|dot)com(\/|slash)invite|" # or discord.com/invite/
+ r"discordapp([\.,]|dot)com(\/|slash)invite|" # or discordapp.com/invite/
+ r"discord([\.,]|dot)me|" # or discord.me
+ r"discord([\.,]|dot)li|" # or discord.li
+ r"discord([\.,]|dot)io|" # or discord.io.
+ r"((?<!\w)([\.,]|dot))gg" # or .gg/
+ r")([\/]|slash)" # / or 'slash'
+ r"(?P<invite>[a-zA-Z0-9\-]+)", # the invite code itself
flags=re.IGNORECASE
)
diff --git a/bot/utils/time.py b/bot/utils/time.py
index 8cf7d623b..eaa9b72e9 100644
--- a/bot/utils/time.py
+++ b/bot/utils/time.py
@@ -3,6 +3,7 @@ import re
from enum import Enum
from typing import Optional, Union
+import arrow
import dateutil.parser
from dateutil.relativedelta import relativedelta
@@ -67,9 +68,9 @@ def discord_timestamp(timestamp: ValidTimestamp, format: TimestampFormats = Time
# Convert each possible timestamp class to an integer.
if isinstance(timestamp, datetime.datetime):
- timestamp = (timestamp.replace(tzinfo=None) - datetime.datetime.utcfromtimestamp(0)).total_seconds()
+ timestamp = (timestamp - arrow.get(0)).total_seconds()
elif isinstance(timestamp, datetime.date):
- timestamp = (timestamp - datetime.date.fromtimestamp(0)).total_seconds()
+ timestamp = (timestamp - arrow.get(0)).total_seconds()
elif isinstance(timestamp, datetime.timedelta):
timestamp = timestamp.total_seconds()
elif isinstance(timestamp, relativedelta):
@@ -124,7 +125,7 @@ def humanize_delta(delta: relativedelta, precision: str = "seconds", max_units:
def get_time_delta(time_string: str) -> str:
"""Returns the time in human-readable time delta format."""
- date_time = dateutil.parser.isoparse(time_string).replace(tzinfo=None)
+ date_time = dateutil.parser.isoparse(time_string)
time_delta = time_since(date_time)
return time_delta
@@ -157,7 +158,7 @@ def parse_duration_string(duration: str) -> Optional[relativedelta]:
def relativedelta_to_timedelta(delta: relativedelta) -> datetime.timedelta:
"""Converts a relativedelta object to a timedelta object."""
- utcnow = datetime.datetime.utcnow()
+ utcnow = arrow.utcnow()
return utcnow + delta - utcnow
@@ -196,8 +197,8 @@ def format_infraction_with_duration(
date_to_formatted = format_infraction(date_to)
- date_from = date_from or datetime.datetime.utcnow()
- date_to = dateutil.parser.isoparse(date_to).replace(tzinfo=None, microsecond=0)
+ date_from = date_from or datetime.datetime.now(datetime.timezone.utc)
+ date_to = dateutil.parser.isoparse(date_to).replace(microsecond=0)
delta = relativedelta(date_to, date_from)
if absolute:
@@ -215,15 +216,15 @@ def until_expiration(
"""
Get the remaining time until infraction's expiration, in a discord timestamp.
- Returns a human-readable version of the remaining duration between datetime.utcnow() and an expiry.
+ Returns a human-readable version of the remaining duration between arrow.utcnow() and an expiry.
Similar to time_since, except that this function doesn't error on a null input
and return null if the expiry is in the paste
"""
if not expiry:
return None
- now = datetime.datetime.utcnow()
- since = dateutil.parser.isoparse(expiry).replace(tzinfo=None, microsecond=0)
+ now = arrow.utcnow()
+ since = dateutil.parser.isoparse(expiry).replace(microsecond=0)
if since < now:
return None
diff --git a/poetry.lock b/poetry.lock
index 16c599bd1..d91941d45 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -722,7 +722,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[[package]]
name = "pycares"
-version = "4.0.0"
+version = "4.1.2"
description = "Python interface for c-ares"
category = "main"
optional = false
@@ -902,7 +902,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*"
[[package]]
name = "rapidfuzz"
-version = "1.7.1"
+version = "1.8.0"
description = "rapid fuzzy string matching"
category = "main"
optional = false
@@ -1114,7 +1114,7 @@ multidict = ">=4.0"
[metadata]
lock-version = "1.1"
python-versions = "3.9.*"
-content-hash = "e37923739c35ef349d57e324579acfe304cc7e6fc20ddc54205fc89f171ae94f"
+content-hash = "da321f13297501e62dd1eb362eccb586ea1a9c21ddb395e11a91b93a2f92e9d4"
[metadata.files]
aio-pika = [
@@ -1471,6 +1471,8 @@ lxml = [
{file = "lxml-4.6.3-cp27-cp27m-win_amd64.whl", hash = "sha256:8157dadbb09a34a6bd95a50690595e1fa0af1a99445e2744110e3dca7831c4ee"},
{file = "lxml-4.6.3-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:7728e05c35412ba36d3e9795ae8995e3c86958179c9770e65558ec3fdfd3724f"},
{file = "lxml-4.6.3-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:4bff24dfeea62f2e56f5bab929b4428ae6caba2d1eea0c2d6eb618e30a71e6d4"},
+ {file = "lxml-4.6.3-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:64812391546a18896adaa86c77c59a4998f33c24788cadc35789e55b727a37f4"},
+ {file = "lxml-4.6.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:c1a40c06fd5ba37ad39caa0b3144eb3772e813b5fb5b084198a985431c2f1e8d"},
{file = "lxml-4.6.3-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:74f7d8d439b18fa4c385f3f5dfd11144bb87c1da034a466c5b5577d23a1d9b51"},
{file = "lxml-4.6.3-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:f90ba11136bfdd25cae3951af8da2e95121c9b9b93727b1b896e3fa105b2f586"},
{file = "lxml-4.6.3-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:4c61b3a0db43a1607d6264166b230438f85bfed02e8cff20c22e564d0faff354"},
@@ -1674,39 +1676,37 @@ py = [
{file = "py-1.10.0.tar.gz", hash = "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3"},
]
pycares = [
- {file = "pycares-4.0.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:db5a533111a3cfd481e7e4fb2bf8bef69f4fa100339803e0504dd5aecafb96a5"},
- {file = "pycares-4.0.0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:fdff88393c25016f417770d82678423fc7a56995abb2df3d2a1e55725db6977d"},
- {file = "pycares-4.0.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:0aa97f900a7ffb259be77d640006585e2a907b0cd4edeee0e85cf16605995d5a"},
- {file = "pycares-4.0.0-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:a34b0e3e693dceb60b8a1169668d606c75cb100ceba0a2df53c234a0eb067fbc"},
- {file = "pycares-4.0.0-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:7661d6bbd51a337e7373cb356efa8be9b4655fda484e068f9455e939aec8d54e"},
- {file = "pycares-4.0.0-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:57315b8eb8fdbc56b3ad4932bc4b17132bb7c7fd2bd590f7fb84b6b522098aa9"},
- {file = "pycares-4.0.0-cp36-cp36m-win32.whl", hash = "sha256:dca9dc58845a9d083f302732a3130c68ded845ad5d463865d464e53c75a3dd45"},
- {file = "pycares-4.0.0-cp36-cp36m-win_amd64.whl", hash = "sha256:c95c964d5dd307e104b44b193095c67bb6b10c9eda1ffe7d44ab7a9e84c476d9"},
- {file = "pycares-4.0.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:26e67e4f81c80a5955dcf6193f3d9bee3c491fc0056299b383b84d792252fba4"},
- {file = "pycares-4.0.0-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:cd3011ffd5e1ad55880f7256791dbab9c43ebeda260474a968f19cd0319e1aef"},
- {file = "pycares-4.0.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:1b959dd5921d207d759d421eece1b60416df33a7f862465739d5f2c363c2f523"},
- {file = "pycares-4.0.0-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:6f258c1b74c048a9501a25f732f11b401564005e5e3c18f1ca6cad0c3dc0fb19"},
- {file = "pycares-4.0.0-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:b17ef48729786e62b574c6431f675f4cb02b27691b49e7428a605a50cd59c072"},
- {file = "pycares-4.0.0-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:82b3259cb590ddd107a6d2dc52da2a2e9a986bf242e893d58c786af2f8191047"},
- {file = "pycares-4.0.0-cp37-cp37m-win32.whl", hash = "sha256:4876fc790ae32832ae270c4a010a1a77e12ddf8d8e6ad70ad0b0a9d506c985f7"},
- {file = "pycares-4.0.0-cp37-cp37m-win_amd64.whl", hash = "sha256:f60c04c5561b1ddf85ca4e626943cc09d7fb684e1adb22abb632095415a40fd7"},
- {file = "pycares-4.0.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:615406013cdcd1b445e5d1a551d276c6200b3abe77e534f8a7f7e1551208d14f"},
- {file = "pycares-4.0.0-cp38-cp38-manylinux1_i686.whl", hash = "sha256:6580aef5d1b29a88c3d72fe73c691eacfd454f86e74d3fdd18f4bad8e8def98b"},
- {file = "pycares-4.0.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:8ebb3ba0485f66cae8eed7ce3e9ed6f2c0bfd5e7319d5d0fbbb511064f17e1d4"},
- {file = "pycares-4.0.0-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:c5362b7690ca481440f6b98395ac6df06aa50518ccb183c560464d1e5e2ab5d4"},
- {file = "pycares-4.0.0-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:eb60be66accc9a9ea1018b591a1f5800cba83491d07e9acc8c56bc6e6607ab54"},
- {file = "pycares-4.0.0-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:44896d6e191a6b5a914dbe3aa7c748481bf6ad19a9df33c1e76f8f2dc33fc8f0"},
- {file = "pycares-4.0.0-cp38-cp38-win32.whl", hash = "sha256:09b28fc7bc2cc05f7f69bf1636ddf46086e0a1837b62961e2092fcb40477320d"},
- {file = "pycares-4.0.0-cp38-cp38-win_amd64.whl", hash = "sha256:d4a5081e232c1d181883dcac4675807f3a6cf33911c4173fbea00c0523687ed4"},
- {file = "pycares-4.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:103353577a6266a53e71bfee4cf83825f1401fefa60f0fb8bdec35f13be6a5f2"},
- {file = "pycares-4.0.0-cp39-cp39-manylinux1_i686.whl", hash = "sha256:ad6caf580ee69806fc6534be93ddbb6e99bf94296d79ab351c37b2992b17abfd"},
- {file = "pycares-4.0.0-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:3d5e50c95849f6905d2a9dbf02ed03f82580173e3c5604a39e2ad054185631f1"},
- {file = "pycares-4.0.0-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:53bc4f181b19576499b02cea4b45391e8dcbe30abd4cd01492f66bfc15615a13"},
- {file = "pycares-4.0.0-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:d52f9c725d2a826d5ffa37681eb07ffb996bfe21788590ef257664a3898fc0b5"},
- {file = "pycares-4.0.0-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:3c7fb8d34ee11971c39acfaf98d0fac66725385ccef3bfe1b174c92b210e1aa4"},
- {file = "pycares-4.0.0-cp39-cp39-win32.whl", hash = "sha256:e9773e07684a55f54657df05237267611a77b294ec3bacb5f851c4ffca38a465"},
- {file = "pycares-4.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:38e54037f36c149146ff15f17a4a963fbdd0f9871d4a21cd94ff9f368140f57e"},
- {file = "pycares-4.0.0.tar.gz", hash = "sha256:d0154fc5753b088758fbec9bc137e1b24bb84fc0c6a09725c8bac25a342311cd"},
+ {file = "pycares-4.1.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:71b99b9e041ae3356b859822c511f286f84c8889ec9ed1fbf6ac30fb4da13e4c"},
+ {file = "pycares-4.1.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c000942f5fc64e6e046aa61aa53b629b576ba11607d108909727c3c8f211a157"},
+ {file = "pycares-4.1.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:b0e50ddc78252f2e2b6b5f2c73e5b2449dfb6bea7a5a0e21dfd1e2bcc9e17382"},
+ {file = "pycares-4.1.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6831e963a910b0a8cbdd2750ffcdf5f2bb0edb3f53ca69ff18484de2cc3807c4"},
+ {file = "pycares-4.1.2-cp310-cp310-win32.whl", hash = "sha256:ad7b28e1b6bc68edd3d678373fa3af84e39d287090434f25055d21b4716b2fc6"},
+ {file = "pycares-4.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:27a6f09dbfb69bb79609724c0f90dfaa7c215876a7cd9f12d585574d1f922112"},
+ {file = "pycares-4.1.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:e5a060f5fa90ae245aa99a4a8ad13ec39c2340400de037c7e8d27b081e1a3c64"},
+ {file = "pycares-4.1.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:056330275dea42b7199494047a745e1d9785d39fb8c4cd469dca043532240b80"},
+ {file = "pycares-4.1.2-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0aa897543a786daba74ec5e19638bd38b2b432d179a0e248eac1e62de5756207"},
+ {file = "pycares-4.1.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:cbceaa9b2c416aa931627466d3240aecfc905c292c842252e3d77b8630072505"},
+ {file = "pycares-4.1.2-cp36-cp36m-win32.whl", hash = "sha256:112e1385c451069112d6b5ea1f9c378544f3c6b89882ff964e9a64be3336d7e4"},
+ {file = "pycares-4.1.2-cp36-cp36m-win_amd64.whl", hash = "sha256:c6680f7fdc0f1163e8f6c2a11d11b9a0b524a61000d2a71f9ccd410f154fb171"},
+ {file = "pycares-4.1.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:58a41a2baabcd95266db776c510d349d417919407f03510fc87ac7488730d913"},
+ {file = "pycares-4.1.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a810d01c9a426ee8b0f36969c2aef5fb966712be9d7e466920beb328cd9cefa3"},
+ {file = "pycares-4.1.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:b266cec81dcea2c3efbbd3dda00af8d7eb0693ae9e47e8706518334b21f27d4a"},
+ {file = "pycares-4.1.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8319afe4838e09df267c421ca93da408f770b945ec6217dda72f1f6a493e37e4"},
+ {file = "pycares-4.1.2-cp37-cp37m-win32.whl", hash = "sha256:4d5da840aa0d9b15fa51107f09270c563a348cb77b14ae9653d0bbdbe326fcc2"},
+ {file = "pycares-4.1.2-cp37-cp37m-win_amd64.whl", hash = "sha256:5632f21d92cc0225ba5ff906e4e5dec415ef0b3df322c461d138190681cd5d89"},
+ {file = "pycares-4.1.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8fd1ff17a26bb004f0f6bb902ba7dddd810059096ae0cc3b45e4f5be46315d19"},
+ {file = "pycares-4.1.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:439799be4b7576e907139a7f9b3c8a01b90d3e38af4af9cd1fc6c1ee9a42b9e6"},
+ {file = "pycares-4.1.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:40079ed58efa91747c50aac4edf8ecc7e570132ab57dc0a4030eb0d016a6cab8"},
+ {file = "pycares-4.1.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4e190471a015f8225fa38069617192e06122771cce2b169ac7a60bfdbd3d4ab2"},
+ {file = "pycares-4.1.2-cp38-cp38-win32.whl", hash = "sha256:2b837315ed08c7df009b67725fe1f50489e99de9089f58ec1b243dc612f172aa"},
+ {file = "pycares-4.1.2-cp38-cp38-win_amd64.whl", hash = "sha256:c7eba3c8354b730a54d23237d0b6445a2f68570fa68d0848887da23a3f3b71f3"},
+ {file = "pycares-4.1.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2f5f84fe9f83eab9cd68544b165b74ba6e3412d029cc9ab20098d9c332869fc5"},
+ {file = "pycares-4.1.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569eef8597b5e02b1bc4644b9f272160304d8c9985357d7ecfcd054da97c0771"},
+ {file = "pycares-4.1.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:e1489aa25d14dbf7176110ead937c01176ed5a0ebefd3b092bbd6b202241814c"},
+ {file = "pycares-4.1.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:dc942692fca0e27081b7bb414bb971d34609c80df5e953f6d0c62ecc8019acd9"},
+ {file = "pycares-4.1.2-cp39-cp39-win32.whl", hash = "sha256:ed71dc4290d9c3353945965604ef1f6a4de631733e9819a7ebc747220b27e641"},
+ {file = "pycares-4.1.2-cp39-cp39-win_amd64.whl", hash = "sha256:ec00f3594ee775665167b1a1630edceefb1b1283af9ac57480dba2fb6fd6c360"},
+ {file = "pycares-4.1.2.tar.gz", hash = "sha256:03490be0e7b51a0c8073f877bec347eff31003f64f57d9518d419d9369452837"},
]
pycodestyle = [
{file = "pycodestyle-2.7.0-py2.py3-none-any.whl", hash = "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068"},
@@ -1792,57 +1792,64 @@ pyyaml = [
{file = "PyYAML-5.4.1.tar.gz", hash = "sha256:607774cbba28732bfa802b54baa7484215f530991055bb562efbed5b2f20a45e"},
]
rapidfuzz = [
- {file = "rapidfuzz-1.7.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:1ca9888e867aed2bb8d51571270e5f8393d718bb189fe1a7c0b047b8fd72bad3"},
- {file = "rapidfuzz-1.7.1-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:f336cd32a2a72eb9d7694618c9065ef3a2af330ab7e54bc0ec69d3b2eb08080e"},
- {file = "rapidfuzz-1.7.1-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:76124767ac3d3213a1aad989f80b156b225defef8addc825a5b631d3164c3213"},
- {file = "rapidfuzz-1.7.1-cp27-cp27m-win32.whl", hash = "sha256:c1090deb95e5369fff47c223c0ed3472644efc56817e288ebeaaa34822a1235c"},
- {file = "rapidfuzz-1.7.1-cp27-cp27m-win_amd64.whl", hash = "sha256:83f94c89e8f16679e0def3c7afa6c9ba477d837fd01250d6a1e3fea12267ce24"},
- {file = "rapidfuzz-1.7.1-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:cdd5962bd009b1457e280b5619d312cd6305b5b8afeff6c27869f98fee839c36"},
- {file = "rapidfuzz-1.7.1-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:2940960e212b66f00fc58f9b4a13e6f80221141dcbaee9c51f97e0a1f30ff1ab"},
- {file = "rapidfuzz-1.7.1-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:5ed4304a91043d27b92fe9af5eb87d1586548da6d03cbda5bbc98b00fee227cb"},
- {file = "rapidfuzz-1.7.1-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:be18495bd84bf2bd3e888270a3cd4dea868ff4b9b8ec6e540f0e195cda554140"},
- {file = "rapidfuzz-1.7.1-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:d5779e6f548b6f3edfbdfbeeda4158286684dcb2bae3515ce68c510ea48e1b4d"},
- {file = "rapidfuzz-1.7.1-cp35-cp35m-win32.whl", hash = "sha256:80d780c4f6da08eb6801489df54fdbdc5ef2b882bd73f9585ef6e0cf09f1690d"},
- {file = "rapidfuzz-1.7.1-cp35-cp35m-win_amd64.whl", hash = "sha256:3b205c63b8606c2b8595ba8403a8c3ebd39de9f7f44631a2f651f3efe106ae9a"},
- {file = "rapidfuzz-1.7.1-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:8f96588a8a7d021debb4c60d82b15a80995daa99159bbeddd8a37f68f75ee06c"},
- {file = "rapidfuzz-1.7.1-cp36-cp36m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:b8139116a937691dde17f27aafe774647808339305f4683b3a6d9bae6518aa2a"},
- {file = "rapidfuzz-1.7.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba574801c8410cc1f2d690ef65f898f6a660bba22ec8213e0f34dd0f0590bc71"},
- {file = "rapidfuzz-1.7.1-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2d5194e3cb638af0cc7c02daa61cef07e332fd3f790ec113006302131be9afa6"},
- {file = "rapidfuzz-1.7.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2dd9d8eaae888b966422cbcba954390a63b4933d8c513ea0056fd6e42d421d08"},
- {file = "rapidfuzz-1.7.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3725c61b9cf57b6b7a765b92046e7d9e5ccce845835b523954b410a70dc32692"},
- {file = "rapidfuzz-1.7.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:e417961e5ca450d6c7448accc5a7e4e9ab0dd3c63729f76215d5e672785920fc"},
- {file = "rapidfuzz-1.7.1-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:26d756284c8c6274b5d558e759415bfb4016fcdf168159b34702c346875d8cc0"},
- {file = "rapidfuzz-1.7.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4887766f0dcc5df43fe4315df4b3c642829e06dc60d5bcb5e682fb76657e8ed1"},
- {file = "rapidfuzz-1.7.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ec0a29671d59998b97998b757ab1c636dd3b7721eda41746ae897abe709681a9"},
- {file = "rapidfuzz-1.7.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dff55750fecd8c0f07bc199e48427c86873be2d0e6a3a80df98972847287f5d3"},
- {file = "rapidfuzz-1.7.1-cp37-cp37m-win32.whl", hash = "sha256:e113f741bb18b0ddd14d714d80ce9c6d5322724f3023b920708e82491e7aef28"},
- {file = "rapidfuzz-1.7.1-cp37-cp37m-win_amd64.whl", hash = "sha256:ef20654be0aed240ee44c98ce02639c37422adc3e144d28c4b6d3da043d9fd20"},
- {file = "rapidfuzz-1.7.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:9e27eb57745a4d2a390b056f6f490b712c2f54250c5d2c794dd76062065a8aef"},
- {file = "rapidfuzz-1.7.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:de2b0ebb67ee0b78973141dba91f574a325a3425664dbdbad37fd7aca7b28cab"},
- {file = "rapidfuzz-1.7.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:88c65d91dcd3c0595112d16555536c60ac5bcab1a43e517e155a242a39525057"},
- {file = "rapidfuzz-1.7.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:afd525a9b593cc1099f0210e116bcb4d9fc5585728d7bd929e6a4133dacd2d59"},
- {file = "rapidfuzz-1.7.1-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e6d77f104a8d67c01ae4248ced6f0d4ef05e63931afdf49c20decf962318877f"},
- {file = "rapidfuzz-1.7.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7db9d6ad0ab80e9e0f66f157b8e31b1d04ce5fa767b936ca1c212b98092572b1"},
- {file = "rapidfuzz-1.7.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0195c57f4beea0e7691594f59faf62a4be3c818c1955a8b9b712f37adc479d2d"},
- {file = "rapidfuzz-1.7.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9ffca8c8b74d12cd36c051e9befa7c4eb2d34624ce71f22dbfc659af15bf4a1e"},
- {file = "rapidfuzz-1.7.1-cp38-cp38-win32.whl", hash = "sha256:234cb75aa1e21cabad6a8c0718f84e2bfafdd4756b5232d5739545f97e343e59"},
- {file = "rapidfuzz-1.7.1-cp38-cp38-win_amd64.whl", hash = "sha256:058977e93ab736071fcd8828fc6289ec026e9ca4a19f2a0967f9260e63910da8"},
- {file = "rapidfuzz-1.7.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:9d02bb0724326826b1884cc9b9d9fd97ac352c18213f45e465a39ef069a33115"},
- {file = "rapidfuzz-1.7.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:212d6fa5b824aaa49a921c81d7cdc1d079b3545a30563ae14dc88e17918e76bf"},
- {file = "rapidfuzz-1.7.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a0cd8117deba10e2a1d6dccb6ff44a4c737adda3048dc45860c5f53cf64db14f"},
- {file = "rapidfuzz-1.7.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:61faa47b6b5d5a0cbe9fa6369df44d3f9435c4cccdb4d38d9de437f18b69dc4d"},
- {file = "rapidfuzz-1.7.1-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1daa756be52a7ee60d553ba667cda3a188ee811c92a9c21df43a4cdadb1eb8ca"},
- {file = "rapidfuzz-1.7.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c98ac10782dadf507e922963c8b8456a79151b4f10dbb08cfc86c1572db366dc"},
- {file = "rapidfuzz-1.7.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:358d80061ca107df6c3e1f67fa7af0f94a62827cb9c44ac09a16e78b38f7c3d5"},
- {file = "rapidfuzz-1.7.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a5f90fc31d54fcd74a97d175892555786a8214a3cff43077463915b8a45a191d"},
- {file = "rapidfuzz-1.7.1-cp39-cp39-win32.whl", hash = "sha256:55dffdcdccea6f077a4f09164039411f01f621633be5883c58ceaf94f007a688"},
- {file = "rapidfuzz-1.7.1-cp39-cp39-win_amd64.whl", hash = "sha256:d712a7f680d2074b587650f81865ca838c04fcc6b77c9d2d742de0853aaa24ce"},
- {file = "rapidfuzz-1.7.1-pp27-pypy_73-macosx_10_9_x86_64.whl", hash = "sha256:729d73a8db5a2b444a19d4aa2be009b2e628d207d7c754f6d280e3c6a59b94cb"},
- {file = "rapidfuzz-1.7.1-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:a1cabbc645395b6175cad79164d9ec621866a004b476e44cac534020b9f6bddb"},
- {file = "rapidfuzz-1.7.1-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:ae697294f456f7f76e5bd30db5a65e8b855e7e09f9a65e144efa1e2c5009553c"},
- {file = "rapidfuzz-1.7.1-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e8ae51c1cf1f034f15216fec2e1eef658c8b3a9cbdcc1a053cc7133ede9d616d"},
- {file = "rapidfuzz-1.7.1-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:dccc072f2a0eeb98d46a79427ef793836ebc5184b1fe544b34607be10705ddc3"},
- {file = "rapidfuzz-1.7.1.tar.gz", hash = "sha256:99495c679174b2a02641f7dc2364a208135cacca77fc4825a86efbfe1e23b0ff"},
+ {file = "rapidfuzz-1.8.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:91f094562c683802e6c972bce27a692dad70d6cd1114e626b29d990c3704c653"},
+ {file = "rapidfuzz-1.8.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:4a20682121e245cf5ad2dbdd771360763ea11b77520632a1034c4bb9ad1e854c"},
+ {file = "rapidfuzz-1.8.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:8810e75d8f9c4453bbd6209c372bf97514359b0b5efff555caf85b15f8a9d862"},
+ {file = "rapidfuzz-1.8.0-cp27-cp27m-win32.whl", hash = "sha256:00cf713d843735b5958d87294f08b05c653a593ced7c4120be34f5d26d7a320a"},
+ {file = "rapidfuzz-1.8.0-cp27-cp27m-win_amd64.whl", hash = "sha256:2baca64e23a623e077f57e5470de21af2765af15aa1088676eb2d475e664eed0"},
+ {file = "rapidfuzz-1.8.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:9bf7a6c61bacedd84023be356e057e1d209dd6997cfaa3c1cee77aa21d642f88"},
+ {file = "rapidfuzz-1.8.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:61b6434e3341ca5158ecb371b1ceb4c1f6110563a72d28bdce4eb2a084493e47"},
+ {file = "rapidfuzz-1.8.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:e425e690383f6cf308e8c2e8d630fa9596f67d233344efd8fae11e70a9f5635f"},
+ {file = "rapidfuzz-1.8.0-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:93db5e693b76d616b09df27ca5c79e0dda169af7f1b8f5ab3262826d981e37e2"},
+ {file = "rapidfuzz-1.8.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a8c4f76ed1c8a65892d98dc2913027c9acdb219d18f3a441cfa427a32861af9"},
+ {file = "rapidfuzz-1.8.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:71e217fd30901214cc96c0c15057278bafb7072aa9b2be4c97459c1fedf3e731"},
+ {file = "rapidfuzz-1.8.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d579dd447b8e851462e79054b68f94b66b09df8b3abb2aa5ca07fe00912ef5e8"},
+ {file = "rapidfuzz-1.8.0-cp310-cp310-win32.whl", hash = "sha256:5808064555273496dcd594d659bd28ee8d399149dd31575321034424455dc955"},
+ {file = "rapidfuzz-1.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:798fef1671ca66c78b47802228e9583f7ab32b99bdfe3984ebb1f96e93e38b5f"},
+ {file = "rapidfuzz-1.8.0-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:c9e0ed210831f5c73533bf11099ea7897db491e76c3443bef281d9c1c67d7f3a"},
+ {file = "rapidfuzz-1.8.0-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:c819bb19eb615a31ddc9cb8248a285bf04f58158b53ce096451178631f99b652"},
+ {file = "rapidfuzz-1.8.0-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:942ee45564f28ef70320d1229f02dc998bd93e3519c1f3a80f33ce144b51039c"},
+ {file = "rapidfuzz-1.8.0-cp35-cp35m-win32.whl", hash = "sha256:7e6ae2e5a3bc9acc51e118f25d32b8efcd431c5d8deb408336dd2ed0f21d087c"},
+ {file = "rapidfuzz-1.8.0-cp35-cp35m-win_amd64.whl", hash = "sha256:98901fba67c89ad2506f3946642cf6eb8f489592fb7eb307ebdf8bdb0c4e97f9"},
+ {file = "rapidfuzz-1.8.0-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:705e1686f406a0c77ef323cdb7369b7cf9e68f2abfcb83ff5f1e0a5b21f5a534"},
+ {file = "rapidfuzz-1.8.0-cp36-cp36m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:da0c5fe5fdbbd74206c1778af6b8c5ff8dfbe2dd04ae12bbe96642b358acefce"},
+ {file = "rapidfuzz-1.8.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:535253bc9224215131ae450aad6c9f7ef1b24f15c685045eab2b52511268bd06"},
+ {file = "rapidfuzz-1.8.0-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:acdad83f07d886705fce164b0d1f4e3b56788a205602ed3a7fc8b10ceaf05fbf"},
+ {file = "rapidfuzz-1.8.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:35097f649831f8375d6c65a237deccac3aceb573aa7fae1e5d3fa942e89de1c8"},
+ {file = "rapidfuzz-1.8.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:6f4db142e5b4b44314166a90e11603220db659bd2f9c23dd5db402c13eac8eb7"},
+ {file = "rapidfuzz-1.8.0-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:19a3f55f27411d68360540484874beda0b428b062596d5f0f141663ef0738bfd"},
+ {file = "rapidfuzz-1.8.0-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:22b4c1a7f6fe29bd8dae49f7d5ab085dc42c3964f1a78b6dca22fdf83b5c9bfa"},
+ {file = "rapidfuzz-1.8.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a8bfb2fbc147904b78d5c510ee75dc8704b606e956df23f33a9e89abc03f45c3"},
+ {file = "rapidfuzz-1.8.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e6dc5111ebfed2c4f2e4d120a9b280ea13ea4fbb60b6915dd239817b4fc092ed"},
+ {file = "rapidfuzz-1.8.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:db5ee2457d97cb967ffe08446a8c595c03fe747fdc2e145266713f9c516d1c4a"},
+ {file = "rapidfuzz-1.8.0-cp37-cp37m-win32.whl", hash = "sha256:12c1b78cc15fc26f555a4bf66088d5afb6354b5a5aa149a123f01a15af6c411b"},
+ {file = "rapidfuzz-1.8.0-cp37-cp37m-win_amd64.whl", hash = "sha256:693e9579048d8db4ff020715dd6f25aa315fd6445bc94e7400d7a94a227dad27"},
+ {file = "rapidfuzz-1.8.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:b4fe19df3edcf7de359448b872aec08e6592b4ca2d3df4d8ee57b5812d68bebf"},
+ {file = "rapidfuzz-1.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f3670b9df0e1f479637cad1577afca7766a02775dc08c14837cf495c82861d7c"},
+ {file = "rapidfuzz-1.8.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:61d118f36eb942649b0db344f7b7a19ad7e9b5749d831788187eb03b57ce1bfa"},
+ {file = "rapidfuzz-1.8.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:fce3a2c8a1d10da12aff4a0d367624e8ae9e15c1b84a5144843681d39be0c355"},
+ {file = "rapidfuzz-1.8.0-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1577ef26e3647ccc4cc9754c34ffaa731639779f4d7779e91a761c72adac093e"},
+ {file = "rapidfuzz-1.8.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fec9b7e60fde51990c3b48fc1aa9dba9ac3acaf78f623dbb645a6fe21a9654e"},
+ {file = "rapidfuzz-1.8.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b954469d93858bc8b48129bc63fd644382a4df5f3fb1b4b290f48eac1d00a2da"},
+ {file = "rapidfuzz-1.8.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:190ba709069a7e5a6b39b7c8bc413a08cfa7f1f4defec5d974c4128b510e0234"},
+ {file = "rapidfuzz-1.8.0-cp38-cp38-win32.whl", hash = "sha256:97b2d13d6323649b43d1b113681e4013ba230bd6e9827cc832dcebee447d7250"},
+ {file = "rapidfuzz-1.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:81c3091209b75f6611efe2af18834180946d4ce28f41ca8d44fce816187840d2"},
+ {file = "rapidfuzz-1.8.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:d610afa33e92aa0481a514ffda3ec51ca5df3c684c1c1c795307589c62025931"},
+ {file = "rapidfuzz-1.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d976f33ca6b5fabbb095c0a662f5b86baf706184fc24c7f125d4ddb54b8bf036"},
+ {file = "rapidfuzz-1.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0f5ca7bca2af598d4ddcf5b93b64b50654a9ff684e6f18d865f6e13fee442b3e"},
+ {file = "rapidfuzz-1.8.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:6dc2aac5ea6b0306dcd28a6d1a89d35ed2c6ac426f2673ee1b92cf3f1d0fd5cd"},
+ {file = "rapidfuzz-1.8.0-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f145c9831c0454a696a3136a6380ea4e01434e9cc2f2bc10d032864c16d1d0e5"},
+ {file = "rapidfuzz-1.8.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4ce53291575b56c9d45add73ea013f43bafcea55eee9d5139aa759918d7685f"},
+ {file = "rapidfuzz-1.8.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de5773a39c00a0f23cfc5da9e0e5fd0fb512b0ebe23dc7289a38e1f9a4b5cefc"},
+ {file = "rapidfuzz-1.8.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:87a802e55792bfbe192e2d557f38867dbe3671b49b3d5ecd873859c7460746ba"},
+ {file = "rapidfuzz-1.8.0-cp39-cp39-win32.whl", hash = "sha256:9391abf1121df831316222f28cea37397a0f72bd7978f3be6e7da29a7821e4e5"},
+ {file = "rapidfuzz-1.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:9eeca1b436042b5523dcf314f5822b1131597898c1d967f140d1917541a8a3d1"},
+ {file = "rapidfuzz-1.8.0-pp27-pypy_73-macosx_10_9_x86_64.whl", hash = "sha256:a01f2495aca479b49d3b3a8863d6ba9bea2043447a1ced74ae5ec5270059cbc1"},
+ {file = "rapidfuzz-1.8.0-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:b7d4b1a5d16817f8cdb34365c7b58ae22d5cf1b3207720bb2fa0b55968bdb034"},
+ {file = "rapidfuzz-1.8.0-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c738d0d7f1744646d48d19b4c775926082bcefebd2460f45ca383a0e882f5672"},
+ {file = "rapidfuzz-1.8.0-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:0fb9c6078c17c12b52e66b7d0a2a1674f6bbbdc6a76e454c8479b95147018123"},
+ {file = "rapidfuzz-1.8.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:1482b385d83670eb069577c9667f72b41eec4f005aee32f1a4ff4e71e88afde2"},
+ {file = "rapidfuzz-1.8.0.tar.gz", hash = "sha256:83fff37acf0367314879231264169dcbc5e7de969a94f4b82055d06a7fddab9a"},
]
redis = [
{file = "redis-3.5.3-py2.py3-none-any.whl", hash = "sha256:432b788c4530cfe16d8d943a09d40ca6c16149727e4afe8c2c9d5580c59d9f24"},
diff --git a/pyproject.toml b/pyproject.toml
index e227ffaa6..563bf4a27 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -45,7 +45,7 @@ flake8-isort = "~=4.0"
pep8-naming = "~=0.9"
pre-commit = "~=2.1"
taskipy = "~=1.7.0"
-pip-licenses = "~=3.5.2"
+pip-licenses = "~=3.5.3"
python-dotenv = "~=0.17.1"
pytest = "~=6.2.4"
pytest-cov = "~=2.12.1"
diff --git a/tests/bot/exts/moderation/infraction/test_utils.py b/tests/bot/exts/moderation/infraction/test_utils.py
index eb256f1fd..72eebb254 100644
--- a/tests/bot/exts/moderation/infraction/test_utils.py
+++ b/tests/bot/exts/moderation/infraction/test_utils.py
@@ -139,14 +139,14 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase):
type="Ban",
expires="2020-02-26 09:20 (23 hours and 59 minutes)",
reason="No reason provided."
- ),
+ ) + utils.INFRACTION_APPEAL_SERVER_FOOTER,
colour=Colours.soft_red,
url=utils.RULES_URL
).set_author(
name=utils.INFRACTION_AUTHOR_NAME,
url=utils.RULES_URL,
icon_url=Icons.token_removed
- ).set_footer(text=utils.INFRACTION_APPEAL_MODMAIL_FOOTER),
+ ),
"send_result": True
},
{
@@ -157,14 +157,14 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase):
type="Warning",
expires="N/A",
reason="Test reason."
- ),
+ ) + utils.INFRACTION_APPEAL_MODMAIL_FOOTER,
colour=Colours.soft_red,
url=utils.RULES_URL
).set_author(
name=utils.INFRACTION_AUTHOR_NAME,
url=utils.RULES_URL,
icon_url=Icons.token_removed
- ).set_footer(text=utils.INFRACTION_APPEAL_MODMAIL_FOOTER),
+ ),
"send_result": False
},
# Note that this test case asserts that the DM that *would* get sent to the user is formatted
@@ -177,14 +177,14 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase):
type="Note",
expires="N/A",
reason="No reason provided."
- ),
+ ) + utils.INFRACTION_APPEAL_MODMAIL_FOOTER,
colour=Colours.soft_red,
url=utils.RULES_URL
).set_author(
name=utils.INFRACTION_AUTHOR_NAME,
url=utils.RULES_URL,
icon_url=Icons.defcon_denied
- ).set_footer(text=utils.INFRACTION_APPEAL_MODMAIL_FOOTER),
+ ),
"send_result": False
},
{
@@ -195,14 +195,14 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase):
type="Mute",
expires="2020-02-26 09:20 (23 hours and 59 minutes)",
reason="Test"
- ),
+ ) + utils.INFRACTION_APPEAL_MODMAIL_FOOTER,
colour=Colours.soft_red,
url=utils.RULES_URL
).set_author(
name=utils.INFRACTION_AUTHOR_NAME,
url=utils.RULES_URL,
icon_url=Icons.defcon_denied
- ).set_footer(text=utils.INFRACTION_APPEAL_MODMAIL_FOOTER),
+ ),
"send_result": False
},
{
@@ -213,14 +213,14 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase):
type="Mute",
expires="N/A",
reason="foo bar" * 4000
- )[:4093] + "...",
+ )[:4093-utils.LONGEST_EXTRAS] + "..." + utils.INFRACTION_APPEAL_MODMAIL_FOOTER,
colour=Colours.soft_red,
url=utils.RULES_URL
).set_author(
name=utils.INFRACTION_AUTHOR_NAME,
url=utils.RULES_URL,
icon_url=Icons.defcon_denied
- ).set_footer(text=utils.INFRACTION_APPEAL_MODMAIL_FOOTER),
+ ),
"send_result": True
}
]
diff --git a/tests/bot/test_converters.py b/tests/bot/test_converters.py
index ef6c8e19e..988b3857b 100644
--- a/tests/bot/test_converters.py
+++ b/tests/bot/test_converters.py
@@ -1,6 +1,6 @@
-import datetime
import re
import unittest
+from datetime import MAXYEAR, datetime, timezone
from unittest.mock import MagicMock, patch
from dateutil.relativedelta import relativedelta
@@ -17,7 +17,7 @@ class ConverterTests(unittest.IsolatedAsyncioTestCase):
cls.context = MagicMock
cls.context.author = 'bob'
- cls.fixed_utc_now = datetime.datetime.fromisoformat('2019-01-01T00:00:00')
+ cls.fixed_utc_now = datetime.fromisoformat('2019-01-01T00:00:00+00:00')
async def test_tag_name_converter_for_invalid(self):
"""TagNameConverter should raise the correct exception for invalid tag names."""
@@ -111,7 +111,7 @@ class ConverterTests(unittest.IsolatedAsyncioTestCase):
expected_datetime = self.fixed_utc_now + relativedelta(**duration_dict)
with patch('bot.converters.datetime') as mock_datetime:
- mock_datetime.utcnow.return_value = self.fixed_utc_now
+ mock_datetime.now.return_value = self.fixed_utc_now
with self.subTest(duration=duration, duration_dict=duration_dict):
converted_datetime = await converter.convert(self.context, duration)
@@ -157,52 +157,53 @@ class ConverterTests(unittest.IsolatedAsyncioTestCase):
async def test_duration_converter_out_of_range(self, mock_datetime):
"""Duration converter should raise BadArgument if datetime raises a ValueError."""
mock_datetime.__add__.side_effect = ValueError
- mock_datetime.utcnow.return_value = mock_datetime
+ mock_datetime.now.return_value = mock_datetime
- duration = f"{datetime.MAXYEAR}y"
+ duration = f"{MAXYEAR}y"
exception_message = f"`{duration}` results in a datetime outside the supported range."
with self.assertRaisesRegex(BadArgument, re.escape(exception_message)):
await Duration().convert(self.context, duration)
async def test_isodatetime_converter_for_valid(self):
"""ISODateTime converter returns correct datetime for valid datetime string."""
+ utc = timezone.utc
test_values = (
# `YYYY-mm-ddTHH:MM:SSZ` | `YYYY-mm-dd HH:MM:SSZ`
- ('2019-09-02T02:03:05Z', datetime.datetime(2019, 9, 2, 2, 3, 5)),
- ('2019-09-02 02:03:05Z', datetime.datetime(2019, 9, 2, 2, 3, 5)),
+ ('2019-09-02T02:03:05Z', datetime(2019, 9, 2, 2, 3, 5, tzinfo=utc)),
+ ('2019-09-02 02:03:05Z', datetime(2019, 9, 2, 2, 3, 5, tzinfo=utc)),
# `YYYY-mm-ddTHH:MM:SS±HH:MM` | `YYYY-mm-dd HH:MM:SS±HH:MM`
- ('2019-09-02T03:18:05+01:15', datetime.datetime(2019, 9, 2, 2, 3, 5)),
- ('2019-09-02 03:18:05+01:15', datetime.datetime(2019, 9, 2, 2, 3, 5)),
- ('2019-09-02T00:48:05-01:15', datetime.datetime(2019, 9, 2, 2, 3, 5)),
- ('2019-09-02 00:48:05-01:15', datetime.datetime(2019, 9, 2, 2, 3, 5)),
+ ('2019-09-02T03:18:05+01:15', datetime(2019, 9, 2, 2, 3, 5, tzinfo=utc)),
+ ('2019-09-02 03:18:05+01:15', datetime(2019, 9, 2, 2, 3, 5, tzinfo=utc)),
+ ('2019-09-02T00:48:05-01:15', datetime(2019, 9, 2, 2, 3, 5, tzinfo=utc)),
+ ('2019-09-02 00:48:05-01:15', datetime(2019, 9, 2, 2, 3, 5, tzinfo=utc)),
# `YYYY-mm-ddTHH:MM:SS±HHMM` | `YYYY-mm-dd HH:MM:SS±HHMM`
- ('2019-09-02T03:18:05+0115', datetime.datetime(2019, 9, 2, 2, 3, 5)),
- ('2019-09-02 03:18:05+0115', datetime.datetime(2019, 9, 2, 2, 3, 5)),
- ('2019-09-02T00:48:05-0115', datetime.datetime(2019, 9, 2, 2, 3, 5)),
- ('2019-09-02 00:48:05-0115', datetime.datetime(2019, 9, 2, 2, 3, 5)),
+ ('2019-09-02T03:18:05+0115', datetime(2019, 9, 2, 2, 3, 5, tzinfo=utc)),
+ ('2019-09-02 03:18:05+0115', datetime(2019, 9, 2, 2, 3, 5, tzinfo=utc)),
+ ('2019-09-02T00:48:05-0115', datetime(2019, 9, 2, 2, 3, 5, tzinfo=utc)),
+ ('2019-09-02 00:48:05-0115', datetime(2019, 9, 2, 2, 3, 5, tzinfo=utc)),
# `YYYY-mm-ddTHH:MM:SS±HH` | `YYYY-mm-dd HH:MM:SS±HH`
- ('2019-09-02 03:03:05+01', datetime.datetime(2019, 9, 2, 2, 3, 5)),
- ('2019-09-02T01:03:05-01', datetime.datetime(2019, 9, 2, 2, 3, 5)),
+ ('2019-09-02 03:03:05+01', datetime(2019, 9, 2, 2, 3, 5, tzinfo=utc)),
+ ('2019-09-02T01:03:05-01', datetime(2019, 9, 2, 2, 3, 5, tzinfo=utc)),
# `YYYY-mm-ddTHH:MM:SS` | `YYYY-mm-dd HH:MM:SS`
- ('2019-09-02T02:03:05', datetime.datetime(2019, 9, 2, 2, 3, 5)),
- ('2019-09-02 02:03:05', datetime.datetime(2019, 9, 2, 2, 3, 5)),
+ ('2019-09-02T02:03:05', datetime(2019, 9, 2, 2, 3, 5, tzinfo=utc)),
+ ('2019-09-02 02:03:05', datetime(2019, 9, 2, 2, 3, 5, tzinfo=utc)),
# `YYYY-mm-ddTHH:MM` | `YYYY-mm-dd HH:MM`
- ('2019-11-12T09:15', datetime.datetime(2019, 11, 12, 9, 15)),
- ('2019-11-12 09:15', datetime.datetime(2019, 11, 12, 9, 15)),
+ ('2019-11-12T09:15', datetime(2019, 11, 12, 9, 15, tzinfo=utc)),
+ ('2019-11-12 09:15', datetime(2019, 11, 12, 9, 15, tzinfo=utc)),
# `YYYY-mm-dd`
- ('2019-04-01', datetime.datetime(2019, 4, 1)),
+ ('2019-04-01', datetime(2019, 4, 1, tzinfo=utc)),
# `YYYY-mm`
- ('2019-02-01', datetime.datetime(2019, 2, 1)),
+ ('2019-02-01', datetime(2019, 2, 1, tzinfo=utc)),
# `YYYY`
- ('2025', datetime.datetime(2025, 1, 1)),
+ ('2025', datetime(2025, 1, 1, tzinfo=utc)),
)
converter = ISODateTime()
@@ -210,7 +211,6 @@ class ConverterTests(unittest.IsolatedAsyncioTestCase):
for datetime_string, expected_dt in test_values:
with self.subTest(datetime_string=datetime_string, expected_dt=expected_dt):
converted_dt = await converter.convert(self.context, datetime_string)
- self.assertIsNone(converted_dt.tzinfo)
self.assertEqual(converted_dt, expected_dt)
async def test_isodatetime_converter_for_invalid(self):
diff --git a/tests/bot/utils/test_time.py b/tests/bot/utils/test_time.py
index 8edffd1c9..a3dcbfc0a 100644
--- a/tests/bot/utils/test_time.py
+++ b/tests/bot/utils/test_time.py
@@ -72,9 +72,9 @@ class TimeTests(unittest.TestCase):
def test_format_infraction_with_duration_custom_units(self):
"""format_infraction_with_duration should work for custom max_units."""
test_cases = (
- ('3000-12-12T00:01:00Z', datetime(3000, 12, 11, 12, 5, 5), 6,
+ ('3000-12-12T00:01:00Z', datetime(3000, 12, 11, 12, 5, 5, tzinfo=timezone.utc), 6,
'<t:32533488060:f> (11 hours, 55 minutes and 55 seconds)'),
- ('3000-11-23T20:09:00Z', datetime(3000, 4, 25, 20, 15), 20,
+ ('3000-11-23T20:09:00Z', datetime(3000, 4, 25, 20, 15, tzinfo=timezone.utc), 20,
'<t:32531918940:f> (6 months, 28 days, 23 hours and 54 minutes)')
)
@@ -84,16 +84,21 @@ class TimeTests(unittest.TestCase):
def test_format_infraction_with_duration_normal_usage(self):
"""format_infraction_with_duration should work for normal usage, across various durations."""
+ utc = timezone.utc
test_cases = (
- ('2019-12-12T00:01:00Z', datetime(2019, 12, 11, 12, 0, 5), 2, '<t:1576108860:f> (12 hours and 55 seconds)'),
- ('2019-12-12T00:01:00Z', datetime(2019, 12, 11, 12, 0, 5), 1, '<t:1576108860:f> (12 hours)'),
- ('2019-12-12T00:00:00Z', datetime(2019, 12, 11, 23, 59), 2, '<t:1576108800:f> (1 minute)'),
- ('2019-11-23T20:09:00Z', datetime(2019, 11, 15, 20, 15), 2, '<t:1574539740:f> (7 days and 23 hours)'),
- ('2019-11-23T20:09:00Z', datetime(2019, 4, 25, 20, 15), 2, '<t:1574539740:f> (6 months and 28 days)'),
- ('2019-11-23T20:58:00Z', datetime(2019, 11, 23, 20, 53), 2, '<t:1574542680:f> (5 minutes)'),
- ('2019-11-24T00:00:00Z', datetime(2019, 11, 23, 23, 59, 0), 2, '<t:1574553600:f> (1 minute)'),
- ('2019-11-23T23:59:00Z', datetime(2017, 7, 21, 23, 0), 2, '<t:1574553540:f> (2 years and 4 months)'),
- ('2019-11-23T23:59:00Z', datetime(2019, 11, 23, 23, 49, 5), 2,
+ ('2019-12-12T00:01:00Z', datetime(2019, 12, 11, 12, 0, 5, tzinfo=utc), 2,
+ '<t:1576108860:f> (12 hours and 55 seconds)'),
+ ('2019-12-12T00:01:00Z', datetime(2019, 12, 11, 12, 0, 5, tzinfo=utc), 1, '<t:1576108860:f> (12 hours)'),
+ ('2019-12-12T00:00:00Z', datetime(2019, 12, 11, 23, 59, tzinfo=utc), 2, '<t:1576108800:f> (1 minute)'),
+ ('2019-11-23T20:09:00Z', datetime(2019, 11, 15, 20, 15, tzinfo=utc), 2,
+ '<t:1574539740:f> (7 days and 23 hours)'),
+ ('2019-11-23T20:09:00Z', datetime(2019, 4, 25, 20, 15, tzinfo=utc), 2,
+ '<t:1574539740:f> (6 months and 28 days)'),
+ ('2019-11-23T20:58:00Z', datetime(2019, 11, 23, 20, 53, tzinfo=utc), 2, '<t:1574542680:f> (5 minutes)'),
+ ('2019-11-24T00:00:00Z', datetime(2019, 11, 23, 23, 59, 0, tzinfo=utc), 2, '<t:1574553600:f> (1 minute)'),
+ ('2019-11-23T23:59:00Z', datetime(2017, 7, 21, 23, 0, tzinfo=utc), 2,
+ '<t:1574553540:f> (2 years and 4 months)'),
+ ('2019-11-23T23:59:00Z', datetime(2019, 11, 23, 23, 49, 5, tzinfo=utc), 2,
'<t:1574553540:f> (9 minutes and 55 seconds)'),
(None, datetime(2019, 11, 23, 23, 49, 5), 2, None),
)