Skip to content

Commit e2774f9

Browse files
Taaku18fourjr
authored andcommitted
Implement minimum account age requirement and temp blocks (#180)
Resolves #175 & resolves #120
1 parent 784a760 commit e2774f9

File tree

10 files changed

+310
-165
lines changed

10 files changed

+310
-165
lines changed

CHANGELOG.md

Lines changed: 80 additions & 88 deletions
Large diffs are not rendered by default.

bot.py

Lines changed: 90 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
SOFTWARE.
2323
"""
2424

25-
__version__ = '2.13.10'
25+
__version__ = '2.13.11'
2626

2727
import asyncio
2828
import logging
@@ -37,18 +37,20 @@
3737
from discord.ext import commands
3838
from discord.ext.commands.view import StringView
3939

40+
import isodate
41+
4042
from aiohttp import ClientSession
4143
from colorama import init, Fore, Style
4244
from emoji import UNICODE_EMOJI
4345
from motor.motor_asyncio import AsyncIOMotorClient
4446

4547
from core.changelog import Changelog
46-
from core.clients import ModmailApiClient, SelfHostedClient
47-
from core.clients import PluginDatabaseClient
48+
from core.clients import ModmailApiClient, SelfHostedClient, PluginDatabaseClient
4849
from core.config import ConfigManager
4950
from core.utils import info, error
5051
from core.models import Bot
5152
from core.thread import ThreadManager
53+
from core.time import human_timedelta
5254

5355

5456
init()
@@ -431,33 +433,102 @@ async def on_ready(self):
431433
)
432434
logger.info(LINE)
433435

434-
async def process_modmail(self, message):
435-
"""Processes messages sent to the bot."""
436-
436+
async def retrieve_emoji(self):
437437
ctx = SimpleNamespace(bot=self, guild=self.modmail_guild)
438438
converter = commands.EmojiConverter()
439439

440-
blocked_emoji = self.config.get('blocked_emoji', '🚫')
441440
sent_emoji = self.config.get('sent_emoji', '✅')
441+
blocked_emoji = self.config.get('blocked_emoji', '🚫')
442+
443+
if sent_emoji not in UNICODE_EMOJI:
444+
try:
445+
sent_emoji = await converter.convert(
446+
ctx, sent_emoji.strip(':')
447+
)
448+
except commands.BadArgument:
449+
logger.warning(info(f'Sent Emoji ({sent_emoji}) '
450+
f'is not a valid emoji.'))
451+
del self.config.cache['sent_emoji']
452+
await self.config.update()
442453

443454
if blocked_emoji not in UNICODE_EMOJI:
444455
try:
445456
blocked_emoji = await converter.convert(
446457
ctx, blocked_emoji.strip(':')
447458
)
448459
except commands.BadArgument:
449-
pass
460+
logger.warning(info(f'Blocked emoji ({blocked_emoji}) '
461+
'is not a valid emoji.'))
462+
del self.config.cache['blocked_emoji']
463+
await self.config.update()
464+
return sent_emoji, blocked_emoji
450465

451-
if sent_emoji not in UNICODE_EMOJI:
466+
async def process_modmail(self, message):
467+
"""Processes messages sent to the bot."""
468+
sent_emoji, blocked_emoji = await self.retrieve_emoji()
469+
470+
account_age = self.config.get('account_age')
471+
if account_age is None:
472+
account_age = isodate.duration.Duration()
473+
else:
452474
try:
453-
sent_emoji = await converter.convert(
454-
ctx, sent_emoji.strip(':')
455-
)
456-
except commands.BadArgument:
457-
pass
475+
account_age = isodate.parse_duration(account_age)
476+
except isodate.ISO8601Error:
477+
logger.warning('The account age limit needs to be a '
478+
'ISO-8601 duration formatted duration string '
479+
f'greater than 0 days, not "%s".', str(account_age))
480+
del self.config.cache['account_age']
481+
await self.config.update()
482+
account_age = isodate.duration.Duration()
458483

459-
if str(message.author.id) in self.blocked_users:
484+
reason = self.blocked_users.get(str(message.author.id))
485+
if reason is None:
486+
reason = ''
487+
try:
488+
min_account_age = message.author.created_at + account_age
489+
except ValueError as e:
490+
logger.warning(e.args[0])
491+
del self.config.cache['account_age']
492+
await self.config.update()
493+
min_account_age = message.author.created_at
494+
495+
if min_account_age > datetime.utcnow():
496+
# user account has not reached the required time
460497
reaction = blocked_emoji
498+
changed = False
499+
delta = human_timedelta(min_account_age)
500+
501+
if str(message.author.id) not in self.blocked_users:
502+
new_reason = f'System Message: New Account. Required to wait for {delta}.'
503+
self.config.blocked[str(message.author.id)] = new_reason
504+
await self.config.update()
505+
changed = True
506+
507+
if reason.startswith('System Message: New Account.') or changed:
508+
await message.channel.send(embed=discord.Embed(
509+
title='Message not sent!',
510+
description=f'Your must wait for {delta} '
511+
f'before you can contact {self.user.mention}.',
512+
color=discord.Color.red()
513+
))
514+
515+
elif str(message.author.id) in self.blocked_users:
516+
reaction = blocked_emoji
517+
if reason.startswith('System Message: New Account.'):
518+
# Met the age limit already
519+
reaction = sent_emoji
520+
del self.config.blocked[str(message.author.id)]
521+
await self.config.update()
522+
else:
523+
end_time = re.search(r'%(.+?)%$', reason)
524+
if end_time is not None:
525+
after = (datetime.fromisoformat(end_time.group(1)) -
526+
datetime.utcnow()).total_seconds()
527+
if after <= 0:
528+
# No longer blocked
529+
reaction = sent_emoji
530+
del self.config.blocked[str(message.author.id)]
531+
await self.config.update()
461532
else:
462533
reaction = sent_emoji
463534

@@ -542,8 +613,8 @@ async def on_message(self, message):
542613
'Command "{}" is not found'.format(ctx.invoked_with)
543614
)
544615
self.dispatch('command_error', ctx, exc)
545-
546-
async def on_typing(self, channel, user, when):
616+
617+
async def on_typing(self, channel, user, _):
547618
if isinstance(channel, discord.DMChannel):
548619
if not self.config.get('user_typing'):
549620
return
@@ -720,13 +791,13 @@ async def autoupdate_loop(self):
720791

721792
if self.config.get('disable_autoupdates'):
722793
logger.warning(info('Autoupdates disabled.'))
723-
logger.warning(LINE)
794+
logger.info(LINE)
724795
return
725796

726797
if self.self_hosted and not self.config.get('github_access_token'):
727798
logger.warning(info('GitHub access token not found.'))
728799
logger.warning(info('Autoupdates disabled.'))
729-
logger.warning(LINE)
800+
logger.info(LINE)
730801
return
731802

732803
while not self.is_closed():

cogs/modmail.py

Lines changed: 75 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
import discord
66
from discord.ext import commands
77

8+
import re
9+
810
from dateutil import parser
911
from natural.date import duration
1012

@@ -603,7 +605,7 @@ async def contact(self, ctx,
603605
category: Optional[discord.CategoryChannel] = None, *,
604606
user: Union[discord.Member, discord.User]):
605607
"""Create a thread with a specified member.
606-
608+
607609
If the optional category argument is passed, the thread
608610
will be created in the specified category.
609611
"""
@@ -630,7 +632,7 @@ async def contact(self, ctx,
630632
embed = discord.Embed(
631633
title='Created thread',
632634
description=f'Thread started in {thread.channel.mention} '
633-
f'for {user.mention}',
635+
f'for {user.mention}.',
634636
color=self.bot.main_color
635637
)
636638

@@ -665,15 +667,22 @@ async def blocked(self, ctx):
665667
embed.add_field(name='Unknown', value=val, inline=False)
666668

667669
if not users and not not_reachable:
668-
embed.description = 'Currently there are no blocked users'
670+
embed.description = 'Currently there are no blocked users.'
669671

670672
await ctx.send(embed=embed)
671673

672674
@commands.command()
673675
@trigger_typing
674676
@checks.has_permissions(manage_channels=True)
675-
async def block(self, ctx, user: User = None, *, reason=None):
676-
"""Block a user from using Modmail."""
677+
async def block(self, ctx, user: Optional[User] = None, *,
678+
after: UserFriendlyTime = None):
679+
"""
680+
Block a user from using Modmail.
681+
682+
Note: reasons that start with "System Message: " are reserved for internal
683+
use only.
684+
"""
685+
reason = ''
677686

678687
if user is None:
679688
thread = ctx.thread
@@ -682,22 +691,48 @@ async def block(self, ctx, user: User = None, *, reason=None):
682691
else:
683692
raise commands.UserInputError
684693

694+
if after is not None:
695+
reason = after.arg
696+
if reason.startswith('System Message: '):
697+
raise commands.UserInputError
698+
elif re.search(r'%(.+?)%$', reason) is not None:
699+
raise commands.UserInputError
700+
elif after.dt > after.now:
701+
reason = f'{reason} %{after.dt.isoformat()}%'
702+
703+
if not reason:
704+
reason = None
705+
685706
mention = user.mention if hasattr(user, 'mention') else f'`{user.id}`'
686707

687-
if str(user.id) not in self.bot.blocked_users:
708+
extend = f' for `{reason}`' if reason is not None else ''
709+
msg = self.bot.blocked_users.get(str(user.id))
710+
if msg is None:
711+
msg = ''
712+
713+
if str(user.id) not in self.bot.blocked_users or extend or msg.startswith('System Message: '):
714+
if str(user.id) in self.bot.blocked_users:
715+
716+
old_reason = msg.strip().rstrip('.') or 'no reason'
717+
embed = discord.Embed(
718+
title='Success',
719+
description=f'{mention} was previously blocked for '
720+
f'"{old_reason}". {mention} is now blocked{extend}.',
721+
color=self.bot.main_color
722+
)
723+
else:
724+
embed = discord.Embed(
725+
title='Success',
726+
color=self.bot.main_color,
727+
description=f'{mention} is now blocked{extend}.'
728+
)
688729
self.bot.config.blocked[str(user.id)] = reason
689730
await self.bot.config.update()
690-
extend = f'for `{reason}`' if reason else ''
691-
embed = discord.Embed(
692-
title='Success',
693-
color=self.bot.main_color,
694-
description=f'{mention} is now blocked ' + extend
695-
)
696731
else:
697732
embed = discord.Embed(
698733
title='Error',
699734
color=discord.Color.red(),
700-
description=f'{mention} is already blocked'
735+
description=f'{mention} is already blocked.'
701736
)
702737

703738
return await ctx.send(embed=embed)
@@ -706,7 +741,12 @@ async def block(self, ctx, user: User = None, *, reason=None):
706741
@trigger_typing
707742
@checks.has_permissions(manage_channels=True)
708743
async def unblock(self, ctx, *, user: User = None):
709-
"""Unblocks a user from using Modmail."""
744+
"""
745+
Unblocks a user from using Modmail.
746+
747+
Note: reasons start with "System Message: " are reserved for internal
748+
use only.
749+
"""
710750

711751
if user is None:
712752
thread = ctx.thread
@@ -718,17 +758,32 @@ async def unblock(self, ctx, *, user: User = None):
718758
mention = user.mention if hasattr(user, 'mention') else f'`{user.id}`'
719759

720760
if str(user.id) in self.bot.blocked_users:
761+
msg = self.bot.blocked_users.get(str(user.id))
762+
if msg is None:
763+
msg = ''
721764
del self.bot.config.blocked[str(user.id)]
722765
await self.bot.config.update()
723-
embed = discord.Embed(
724-
title='Success',
725-
color=self.bot.main_color,
726-
description=f'{mention} is no longer blocked'
727-
)
766+
767+
if msg.startswith('System Message: '):
768+
# If the user is blocked internally (for example: below minimum account age)
769+
# Show an extended message stating the original internal message
770+
reason = msg[16:].strip().rstrip('.') or 'no reason'
771+
embed = discord.Embed(
772+
title='Success',
773+
description=f'{mention} was previously blocked internally due to '
774+
f'"{reason}". {mention} is no longer blocked.',
775+
color=self.bot.main_color
776+
)
777+
else:
778+
embed = discord.Embed(
779+
title='Success',
780+
color=self.bot.main_color,
781+
description=f'{mention} is no longer blocked.'
782+
)
728783
else:
729784
embed = discord.Embed(
730785
title='Error',
731-
description=f'{mention} is not blocked',
786+
description=f'{mention} is not blocked.',
732787
color=discord.Color.red()
733788
)
734789

cogs/utility.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -544,8 +544,8 @@ async def set_presence(self, *,
544544
else:
545545
url = None
546546
activity_message = (
547-
activity_message or
548-
self.bot.config.get('activity_message', '')
547+
activity_message or
548+
self.bot.config.get('activity_message', '')
549549
).strip()
550550

551551
if activity_type == ActivityType.listening:
@@ -571,7 +571,6 @@ async def set_presence(self, *,
571571
presence = {'activity': (None, 'No activity has been set.'),
572572
'status': (None, 'No status has been set.')}
573573
if activity is not None:
574-
# TODO: Trim message
575574
to = 'to ' if activity.type == ActivityType.listening else ''
576575
msg = f'Activity set to: {activity.type.name.capitalize()} '
577576
msg += f'{to}{activity.name}.'
@@ -676,7 +675,7 @@ async def set(self, ctx, key: str.lower, *, value):
676675

677676
if key in keys:
678677
try:
679-
value, value_text = self.bot.config.clean_data(key, value)
678+
value, value_text = await self.bot.config.clean_data(key, value)
680679
except InvalidConfigError as exc:
681680
embed = exc.embed
682681
else:

0 commit comments

Comments
 (0)