Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ aio-pika = "*"
python-dateutil = "*"
deepdiff = "*"
requests = "*"
dateparser = "*"
more_itertools = "~=7.2"
urllib3 = ">=1.24.2,<1.25"

Expand Down
57 changes: 13 additions & 44 deletions Pipfile.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions bot/cogs/antispam.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
Guild as GuildConfig, Icons,
STAFF_ROLES,
)
from bot.converters import ExpirationDate
from bot.converters import Duration


log = logging.getLogger(__name__)
Expand Down Expand Up @@ -102,7 +102,7 @@ def __init__(self, bot: Bot, validation_errors: bool) -> None:
self.validation_errors = validation_errors
role_id = AntiSpamConfig.punishment['role_id']
self.muted_role = Object(role_id)
self.expiration_date_converter = ExpirationDate()
self.expiration_date_converter = Duration()

self.message_deletion_queue = dict()
self.queue_consumption_tasks = dict()
Expand Down
12 changes: 6 additions & 6 deletions bot/cogs/moderation.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from bot import constants
from bot.cogs.modlog import ModLog
from bot.constants import Colours, Event, Icons, MODERATION_ROLES
from bot.converters import ExpirationDate, InfractionSearchQuery
from bot.converters import Duration, InfractionSearchQuery
from bot.decorators import with_role
from bot.pagination import LinePaginator
from bot.utils.moderation import already_has_active_infraction, post_infraction
Expand Down Expand Up @@ -279,7 +279,7 @@ async def mute(self, ctx: Context, user: Member, *, reason: str = None) -> None:

@with_role(*MODERATION_ROLES)
@command()
async def tempmute(self, ctx: Context, user: Member, duration: ExpirationDate, *, reason: str = None) -> None:
async def tempmute(self, ctx: Context, user: Member, duration: Duration, *, reason: str = None) -> None:
"""
Create a temporary mute infraction for a user with the provided expiration and reason.

Expand Down Expand Up @@ -345,7 +345,7 @@ async def tempmute(self, ctx: Context, user: Member, duration: ExpirationDate, *

@with_role(*MODERATION_ROLES)
@command()
async def tempban(self, ctx: Context, user: UserTypes, duration: ExpirationDate, *, reason: str = None) -> None:
async def tempban(self, ctx: Context, user: UserTypes, duration: Duration, *, reason: str = None) -> None:
"""
Create a temporary ban infraction for a user with the provided expiration and reason.

Expand Down Expand Up @@ -600,7 +600,7 @@ async def shadow_mute(self, ctx: Context, user: Member, *, reason: str = None) -
@with_role(*MODERATION_ROLES)
@command(hidden=True, aliases=["shadowtempmute, stempmute"])
async def shadow_tempmute(
self, ctx: Context, user: Member, duration: ExpirationDate, *, reason: str = None
self, ctx: Context, user: Member, duration: Duration, *, reason: str = None
) -> None:
"""
Create a temporary mute infraction for a user with the provided reason.
Expand Down Expand Up @@ -653,7 +653,7 @@ async def shadow_tempmute(
@with_role(*MODERATION_ROLES)
@command(hidden=True, aliases=["shadowtempban, stempban"])
async def shadow_tempban(
self, ctx: Context, user: UserTypes, duration: ExpirationDate, *, reason: str = None
self, ctx: Context, user: UserTypes, duration: Duration, *, reason: str = None
) -> None:
"""
Create a temporary ban infraction for a user with the provided reason.
Expand Down Expand Up @@ -884,7 +884,7 @@ async def infraction_edit_group(self, ctx: Context) -> None:
@infraction_edit_group.command(name="duration")
async def edit_duration(
self, ctx: Context,
infraction_id: int, expires_at: Union[ExpirationDate, str]
infraction_id: int, expires_at: Union[Duration, str]
) -> None:
"""
Sets the duration of the given infraction, relative to the time of updating.
Expand Down
8 changes: 4 additions & 4 deletions bot/cogs/reminders.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from discord.ext.commands import Bot, Cog, Context, group

from bot.constants import Channels, Icons, NEGATIVE_REPLIES, POSITIVE_REPLIES, STAFF_ROLES
from bot.converters import ExpirationDate
from bot.converters import Duration
from bot.pagination import LinePaginator
from bot.utils.checks import without_role_check
from bot.utils.scheduling import Scheduler
Expand Down Expand Up @@ -118,12 +118,12 @@ async def send_reminder(self, reminder: dict, late: relativedelta = None) -> Non
await self._delete_reminder(reminder["id"])

@group(name="remind", aliases=("reminder", "reminders"), invoke_without_command=True)
async def remind_group(self, ctx: Context, expiration: ExpirationDate, *, content: str) -> None:
async def remind_group(self, ctx: Context, expiration: Duration, *, content: str) -> None:
"""Commands for managing your reminders."""
await ctx.invoke(self.new_reminder, expiration=expiration, content=content)

@remind_group.command(name="new", aliases=("add", "create"))
async def new_reminder(self, ctx: Context, expiration: ExpirationDate, *, content: str) -> Optional[Message]:
async def new_reminder(self, ctx: Context, expiration: Duration, *, content: str) -> Optional[Message]:
"""
Set yourself a simple reminder.

Expand Down Expand Up @@ -237,7 +237,7 @@ async def edit_reminder_group(self, ctx: Context) -> None:
await ctx.invoke(self.bot.get_command("help"), "reminders", "edit")

@edit_reminder_group.command(name="duration", aliases=("time",))
async def edit_reminder_duration(self, ctx: Context, id_: int, expiration: ExpirationDate) -> None:
async def edit_reminder_duration(self, ctx: Context, id_: int, expiration: Duration) -> None:
"""
Edit one of your reminder's expiration.

Expand Down
4 changes: 2 additions & 2 deletions bot/cogs/superstarify/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from bot.cogs.modlog import ModLog
from bot.cogs.superstarify.stars import get_nick
from bot.constants import Icons, MODERATION_ROLES, POSITIVE_REPLIES
from bot.converters import ExpirationDate
from bot.converters import Duration
from bot.decorators import with_role
from bot.utils.moderation import post_infraction

Expand Down Expand Up @@ -153,7 +153,7 @@ async def on_member_join(self, member: Member) -> None:
@command(name='superstarify', aliases=('force_nick', 'star'))
@with_role(*MODERATION_ROLES)
async def superstarify(
self, ctx: Context, member: Member, expiration: ExpirationDate, reason: str = None
self, ctx: Context, member: Member, expiration: Duration, reason: str = None
) -> None:
"""
Force a random superstar name (like Taylor Swift) to be the user's nickname for a specified duration.
Expand Down
50 changes: 34 additions & 16 deletions bot/converters.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import logging
import re
from datetime import datetime
from ssl import CertificateError
from typing import Union

import dateparser
import discord
from aiohttp import ClientConnectorError
from dateutil.relativedelta import relativedelta
from discord.ext.commands import BadArgument, Context, Converter


Expand Down Expand Up @@ -177,23 +178,40 @@ async def convert(ctx: Context, tag_content: str) -> str:
return tag_content


class ExpirationDate(Converter):
"""Convert relative expiration date into UTC datetime using dateparser."""
class Duration(Converter):
"""Convert duration strings into UTC datetime.datetime objects."""

DATEPARSER_SETTINGS = {
'PREFER_DATES_FROM': 'future',
'TIMEZONE': 'UTC',
'TO_TIMEZONE': 'UTC'
}
duration_parser = re.compile(
r"((?P<years>\d+?) ?(years|year|Y|y) ?)?"
r"((?P<months>\d+?) ?(months|month|m) ?)?"
r"((?P<weeks>\d+?) ?(weeks|week|W|w) ?)?"
r"((?P<days>\d+?) ?(days|day|D|d) ?)?"
r"((?P<hours>\d+?) ?(hours|hour|H|h) ?)?"
r"((?P<minutes>\d+?) ?(minutes|minute|M) ?)?"
r"((?P<seconds>\d+?) ?(seconds|second|S|s))?"
)

async def convert(self, ctx: Context, expiration_string: str) -> datetime:
"""Convert relative expiration date into UTC datetime."""
expiry = dateparser.parse(expiration_string, settings=self.DATEPARSER_SETTINGS)
if expiry is None:
raise BadArgument(f"Failed to parse expiration date from `{expiration_string}`")
async def convert(self, ctx: Context, duration: str) -> datetime:
"""
Converts a `duration` string to a datetime object that's `duration` in the future.

The converter supports the following symbols for each unit of time:
- years: `Y`, `y`, `year`, `years`
- months: `m`, `month`, `months`
- weeks: `w`, `W`, `week`, `weeks`
- days: `d`, `D`, `day`, `days`
- hours: `H`, `h`, `hour`, `hours`
- minutes: `M`, `minute`, `minutes`
- seconds: `S`, `s`, `second`, `seconds`

The units need to be provided in descending order of magnitude.
"""
Comment thread
SebastiaanZ marked this conversation as resolved.
match = self.duration_parser.fullmatch(duration)
if not match:
raise BadArgument(f"`{duration}` is not a valid duration string.")

duration_dict = {unit: int(amount) for unit, amount in match.groupdict(default=0).items()}
delta = relativedelta(**duration_dict)
now = datetime.utcnow()
if expiry < now:
expiry = now + (now - expiry)

return expiry
return now + delta
Loading