Skip to content

Commit

Permalink
Implement minimum account age requirement and temp blocks (#180)
Browse files Browse the repository at this point in the history
Resolves #175 & resolves #120
  • Loading branch information
Taaku18 authored and fourjr committed Feb 24, 2019
1 parent 784a760 commit e2774f9
Show file tree
Hide file tree
Showing 10 changed files with 310 additions and 165 deletions.
168 changes: 80 additions & 88 deletions CHANGELOG.md

Large diffs are not rendered by default.

109 changes: 90 additions & 19 deletions bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
SOFTWARE.
"""

__version__ = '2.13.10'
__version__ = '2.13.11'

import asyncio
import logging
Expand All @@ -37,18 +37,20 @@
from discord.ext import commands
from discord.ext.commands.view import StringView

import isodate

from aiohttp import ClientSession
from colorama import init, Fore, Style
from emoji import UNICODE_EMOJI
from motor.motor_asyncio import AsyncIOMotorClient

from core.changelog import Changelog
from core.clients import ModmailApiClient, SelfHostedClient
from core.clients import PluginDatabaseClient
from core.clients import ModmailApiClient, SelfHostedClient, PluginDatabaseClient
from core.config import ConfigManager
from core.utils import info, error
from core.models import Bot
from core.thread import ThreadManager
from core.time import human_timedelta


init()
Expand Down Expand Up @@ -431,33 +433,102 @@ async def on_ready(self):
)
logger.info(LINE)

async def process_modmail(self, message):
"""Processes messages sent to the bot."""

async def retrieve_emoji(self):
ctx = SimpleNamespace(bot=self, guild=self.modmail_guild)
converter = commands.EmojiConverter()

blocked_emoji = self.config.get('blocked_emoji', '🚫')
sent_emoji = self.config.get('sent_emoji', '✅')
blocked_emoji = self.config.get('blocked_emoji', '🚫')

if sent_emoji not in UNICODE_EMOJI:
try:
sent_emoji = await converter.convert(
ctx, sent_emoji.strip(':')
)
except commands.BadArgument:
logger.warning(info(f'Sent Emoji ({sent_emoji}) '
f'is not a valid emoji.'))
del self.config.cache['sent_emoji']
await self.config.update()

if blocked_emoji not in UNICODE_EMOJI:
try:
blocked_emoji = await converter.convert(
ctx, blocked_emoji.strip(':')
)
except commands.BadArgument:
pass
logger.warning(info(f'Blocked emoji ({blocked_emoji}) '
'is not a valid emoji.'))
del self.config.cache['blocked_emoji']
await self.config.update()
return sent_emoji, blocked_emoji

if sent_emoji not in UNICODE_EMOJI:
async def process_modmail(self, message):
"""Processes messages sent to the bot."""
sent_emoji, blocked_emoji = await self.retrieve_emoji()

account_age = self.config.get('account_age')
if account_age is None:
account_age = isodate.duration.Duration()
else:
try:
sent_emoji = await converter.convert(
ctx, sent_emoji.strip(':')
)
except commands.BadArgument:
pass
account_age = isodate.parse_duration(account_age)
except isodate.ISO8601Error:
logger.warning('The account age limit needs to be a '
'ISO-8601 duration formatted duration string '
f'greater than 0 days, not "%s".', str(account_age))
del self.config.cache['account_age']
await self.config.update()
account_age = isodate.duration.Duration()

if str(message.author.id) in self.blocked_users:
reason = self.blocked_users.get(str(message.author.id))
if reason is None:
reason = ''
try:
min_account_age = message.author.created_at + account_age
except ValueError as e:
logger.warning(e.args[0])
del self.config.cache['account_age']
await self.config.update()
min_account_age = message.author.created_at

if min_account_age > datetime.utcnow():
# user account has not reached the required time
reaction = blocked_emoji
changed = False
delta = human_timedelta(min_account_age)

if str(message.author.id) not in self.blocked_users:
new_reason = f'System Message: New Account. Required to wait for {delta}.'
self.config.blocked[str(message.author.id)] = new_reason
await self.config.update()
changed = True

if reason.startswith('System Message: New Account.') or changed:
await message.channel.send(embed=discord.Embed(
title='Message not sent!',
description=f'Your must wait for {delta} '
f'before you can contact {self.user.mention}.',
color=discord.Color.red()
))

elif str(message.author.id) in self.blocked_users:
reaction = blocked_emoji
if reason.startswith('System Message: New Account.'):
# Met the age limit already
reaction = sent_emoji
del self.config.blocked[str(message.author.id)]
await self.config.update()
else:
end_time = re.search(r'%(.+?)%$', reason)
if end_time is not None:
after = (datetime.fromisoformat(end_time.group(1)) -
datetime.utcnow()).total_seconds()
if after <= 0:
# No longer blocked
reaction = sent_emoji
del self.config.blocked[str(message.author.id)]
await self.config.update()
else:
reaction = sent_emoji

Expand Down Expand Up @@ -542,8 +613,8 @@ async def on_message(self, message):
'Command "{}" is not found'.format(ctx.invoked_with)
)
self.dispatch('command_error', ctx, exc)
async def on_typing(self, channel, user, when):

async def on_typing(self, channel, user, _):
if isinstance(channel, discord.DMChannel):
if not self.config.get('user_typing'):
return
Expand Down Expand Up @@ -720,13 +791,13 @@ async def autoupdate_loop(self):

if self.config.get('disable_autoupdates'):
logger.warning(info('Autoupdates disabled.'))
logger.warning(LINE)
logger.info(LINE)
return

if self.self_hosted and not self.config.get('github_access_token'):
logger.warning(info('GitHub access token not found.'))
logger.warning(info('Autoupdates disabled.'))
logger.warning(LINE)
logger.info(LINE)
return

while not self.is_closed():
Expand Down
95 changes: 75 additions & 20 deletions cogs/modmail.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
import discord
from discord.ext import commands

import re

from dateutil import parser
from natural.date import duration

Expand Down Expand Up @@ -603,7 +605,7 @@ async def contact(self, ctx,
category: Optional[discord.CategoryChannel] = None, *,
user: Union[discord.Member, discord.User]):
"""Create a thread with a specified member.
If the optional category argument is passed, the thread
will be created in the specified category.
"""
Expand All @@ -630,7 +632,7 @@ async def contact(self, ctx,
embed = discord.Embed(
title='Created thread',
description=f'Thread started in {thread.channel.mention} '
f'for {user.mention}',
f'for {user.mention}.',
color=self.bot.main_color
)

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

if not users and not not_reachable:
embed.description = 'Currently there are no blocked users'
embed.description = 'Currently there are no blocked users.'

await ctx.send(embed=embed)

@commands.command()
@trigger_typing
@checks.has_permissions(manage_channels=True)
async def block(self, ctx, user: User = None, *, reason=None):
"""Block a user from using Modmail."""
async def block(self, ctx, user: Optional[User] = None, *,
after: UserFriendlyTime = None):
"""
Block a user from using Modmail.
Note: reasons that start with "System Message: " are reserved for internal
use only.
"""
reason = ''

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

if after is not None:
reason = after.arg
if reason.startswith('System Message: '):
raise commands.UserInputError
elif re.search(r'%(.+?)%$', reason) is not None:
raise commands.UserInputError
elif after.dt > after.now:
reason = f'{reason} %{after.dt.isoformat()}%'

if not reason:
reason = None

mention = user.mention if hasattr(user, 'mention') else f'`{user.id}`'

if str(user.id) not in self.bot.blocked_users:
extend = f' for `{reason}`' if reason is not None else ''
msg = self.bot.blocked_users.get(str(user.id))
if msg is None:
msg = ''

if str(user.id) not in self.bot.blocked_users or extend or msg.startswith('System Message: '):
if str(user.id) in self.bot.blocked_users:

old_reason = msg.strip().rstrip('.') or 'no reason'
embed = discord.Embed(
title='Success',
description=f'{mention} was previously blocked for '
f'"{old_reason}". {mention} is now blocked{extend}.',
color=self.bot.main_color
)
else:
embed = discord.Embed(
title='Success',
color=self.bot.main_color,
description=f'{mention} is now blocked{extend}.'
)
self.bot.config.blocked[str(user.id)] = reason
await self.bot.config.update()
extend = f'for `{reason}`' if reason else ''
embed = discord.Embed(
title='Success',
color=self.bot.main_color,
description=f'{mention} is now blocked ' + extend
)
else:
embed = discord.Embed(
title='Error',
color=discord.Color.red(),
description=f'{mention} is already blocked'
description=f'{mention} is already blocked.'
)

return await ctx.send(embed=embed)
Expand All @@ -706,7 +741,12 @@ async def block(self, ctx, user: User = None, *, reason=None):
@trigger_typing
@checks.has_permissions(manage_channels=True)
async def unblock(self, ctx, *, user: User = None):
"""Unblocks a user from using Modmail."""
"""
Unblocks a user from using Modmail.
Note: reasons start with "System Message: " are reserved for internal
use only.
"""

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

if str(user.id) in self.bot.blocked_users:
msg = self.bot.blocked_users.get(str(user.id))
if msg is None:
msg = ''
del self.bot.config.blocked[str(user.id)]
await self.bot.config.update()
embed = discord.Embed(
title='Success',
color=self.bot.main_color,
description=f'{mention} is no longer blocked'
)

if msg.startswith('System Message: '):
# If the user is blocked internally (for example: below minimum account age)
# Show an extended message stating the original internal message
reason = msg[16:].strip().rstrip('.') or 'no reason'
embed = discord.Embed(
title='Success',
description=f'{mention} was previously blocked internally due to '
f'"{reason}". {mention} is no longer blocked.',
color=self.bot.main_color
)
else:
embed = discord.Embed(
title='Success',
color=self.bot.main_color,
description=f'{mention} is no longer blocked.'
)
else:
embed = discord.Embed(
title='Error',
description=f'{mention} is not blocked',
description=f'{mention} is not blocked.',
color=discord.Color.red()
)

Expand Down
7 changes: 3 additions & 4 deletions cogs/utility.py
Original file line number Diff line number Diff line change
Expand Up @@ -544,8 +544,8 @@ async def set_presence(self, *,
else:
url = None
activity_message = (
activity_message or
self.bot.config.get('activity_message', '')
activity_message or
self.bot.config.get('activity_message', '')
).strip()

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

if key in keys:
try:
value, value_text = self.bot.config.clean_data(key, value)
value, value_text = await self.bot.config.clean_data(key, value)
except InvalidConfigError as exc:
embed = exc.embed
else:
Expand Down
Loading

0 comments on commit e2774f9

Please sign in to comment.