Skip to content

Commit 05d8952

Browse files
committed
Merge branch 'master' of https://github.com/verixx/modmail
2 parents 2d2f6c8 + dce7533 commit 05d8952

File tree

9 files changed

+111
-51
lines changed

9 files changed

+111
-51
lines changed

CHANGELOG.md

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,22 @@ All notable changes to this project will be documented in this file.
44
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
55
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
66

7+
8+
# v2.19.1
9+
10+
### Changed
11+
12+
- Ability to force an update despite having the same version number. Helpful to keep up-to-date with the latest GitHub commit.
13+
- `?update force`.
14+
- Plugin developers now have a new event called `on_plugin_ready`, this is coroutine is awaited when all plugins are loaded. Use `on_plugin_ready` instead of `on_ready` since `on_ready` will not get called in plugins.
15+
16+
# v2.19.0
17+
18+
### What's new?
19+
20+
- New config variable `guild_age`, similar to `account_age`, `guild_age` sets a limit as to how long a user has to wait after they joined the server to message Modmail.
21+
- `guild_age` can be set the same way as `account_age`.
22+
723
# v2.18.5
824

925
Fix help command bug when using external plugins.
@@ -68,7 +84,6 @@ When updating to this version, all prior permission settings with guild-based pe
6884

6985
- The help message no longer conceals inaccessible commands due to check failures.
7086

71-
7287
# v2.17.2
7388

7489
### Changed

README.md

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@
88
<a href="https://heroku.com/deploy?template=https://github.com/kyb3r/modmail">
99
<img src="https://img.shields.io/badge/deploy_to-heroku-997FBC.svg?style=for-the-badge">
1010
</a>
11-
11+
<a href="https://github.com/kyb3r/modmail/">
12+
<img src="https://api.modmail.tk/badges/instances.svg" alt="Bot instances">
13+
</a>
1214
<a href="https://discord.gg/j5e9p8w">
1315
<img src="https://img.shields.io/discord/515071617815019520.svg?style=for-the-badge&colorB=7289DA" alt="Support">
1416
</a>
@@ -22,7 +24,6 @@
2224
</a>
2325
</div>
2426

25-
---
2627

2728
## How Does Modmail Work?
2829

@@ -36,23 +37,16 @@ Currently, the easiest and fastest way to set up the bot is by using Heroku, whi
3637

3738
Interactive installation: [**https://taaku18.github.io/modmail/installation**](https://taaku18.github.io/modmail/installation)
3839

39-
---
4040

4141
# Notable Features
4242

4343
## Customizability
4444

45-
Modmail has a range of configuration variables that you can dynamically alter with the `?config` command. You can use them to change the different aspects of the bot, for example, the embed color, responses, reactions, status, etc. Snippets and custom command aliases are also supported. Snippets are shortcuts for predefined messages that you can send. Add or remove snippets with the `?snippets` command. The level of customization is ever growing thanks to our exceptional contributors.
46-
47-
## Linked Messages
48-
49-
<img src="https://i.imgur.com/6L9aaNw.png" align="right" height="350">
50-
51-
Have you sent something with the `?reply` command by accident? Don't fret, you can delete your original message, and the bot will automatically delete the corresponding message sent to the recipient of the thread! You can also use the `?edit` command to edit a message you sent.
45+
Modmail has a range of configuration variables that you can dynamically alter with the `?config` command. You can use them to change the different aspects of the bot, for example, the embed color, responses, reactions, status, etc. Snippets and custom command aliases are also supported. The level of customization is ever growing thanks to our exceptional contributors.
5246

5347
## Thread Logs
5448

55-
Thread conversations are automatically logged with a generated viewable website of the complete thread. Logs are rendered with styled HTML and presented in an aesthetically pleasing way—it blends seamlessly with the mobile version of Discord. An example of a logged conversation: https://modmail-logs.herokuapp.com/logs/02032d65a6f3.
49+
Thread conversations are automatically logged with a generated viewable website of the complete thread. Logs are rendered with styled HTML and presented in an aesthetically pleasing way—it blends seamlessly with the mobile version of Discord. You have the ability to query and search through logs using the bot via commands. An example of a logged conversation: https://logs.modmail.tk/example.
5650

5751
# Contributing
5852

bot.py

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

25-
__version__ = '2.18.5'
25+
__version__ = '2.19.1'
2626

2727
import asyncio
2828
import logging
@@ -111,7 +111,6 @@ def __init__(self):
111111
self.metadata_task = self.loop.create_task(self.metadata_loop())
112112
self.autoupdate_task = self.loop.create_task(self.autoupdate_loop())
113113
self._load_extensions()
114-
self.owner = None
115114

116115
def _configure_logging(self):
117116
level_text = self.config.log_level.upper()
@@ -202,12 +201,12 @@ def run(self, *args, **kwargs):
202201
self.metadata_task.cancel()
203202
self.loop.run_until_complete(self.metadata_task)
204203
except asyncio.CancelledError:
205-
logger.debug('data_task has been cancelled')
204+
logger.debug(info('data_task has been cancelled.'))
206205
try:
207206
self.autoupdate_task.cancel()
208207
self.loop.run_until_complete(self.autoupdate_task)
209208
except asyncio.CancelledError:
210-
logger.debug('autoupdate_task has been cancelled')
209+
logger.debug(info('autoupdate_task has been cancelled.'))
211210

212211
self.loop.run_until_complete(self.logout())
213212
for task in asyncio.Task.all_tasks():
@@ -217,7 +216,7 @@ def run(self, *args, **kwargs):
217216
asyncio.gather(*asyncio.Task.all_tasks())
218217
)
219218
except asyncio.CancelledError:
220-
logger.debug('All pending tasks has been cancelled')
219+
logger.debug(info('All pending tasks has been cancelled.'))
221220
finally:
222221
self.loop.run_until_complete(self.session.close())
223222
self.loop.close()
@@ -467,8 +466,10 @@ async def retrieve_emoji(self):
467466
async def process_modmail(self, message):
468467
"""Processes messages sent to the bot."""
469468
sent_emoji, blocked_emoji = await self.retrieve_emoji()
469+
now = datetime.utcnow()
470470

471471
account_age = self.config.get('account_age')
472+
guild_age = self.config.get('guild_age')
472473
if account_age is None:
473474
account_age = isodate.duration.Duration()
474475
else:
@@ -483,19 +484,41 @@ async def process_modmail(self, message):
483484
await self.config.update()
484485
account_age = isodate.duration.Duration()
485486

487+
if guild_age is None:
488+
guild_age = isodate.duration.Duration()
489+
else:
490+
try:
491+
guild_age = isodate.parse_duration(guild_age)
492+
except isodate.ISO8601Error:
493+
logger.warning('The guild join age limit needs to be a '
494+
'ISO-8601 duration formatted duration string '
495+
f'greater than 0 days, not "%s".', str(guild_age))
496+
del self.config.cache['guild_age']
497+
await self.config.update()
498+
guild_age = isodate.duration.Duration()
499+
486500
reason = self.blocked_users.get(str(message.author.id))
487501
if reason is None:
488502
reason = ''
503+
489504
try:
490505
min_account_age = message.author.created_at + account_age
491506
except ValueError as e:
492507
logger.warning(e.args[0])
493508
del self.config.cache['account_age']
494509
await self.config.update()
495-
min_account_age = message.author.created_at
510+
min_account_age = now
496511

497-
if min_account_age > datetime.utcnow():
498-
# user account has not reached the required time
512+
try:
513+
min_guild_age = self.guild.get_member(message.author.id).joined_at + guild_age
514+
except ValueError as e:
515+
logger.warning(e.args[0])
516+
del self.config.cache['guild_age']
517+
await self.config.update()
518+
min_guild_age = now
519+
520+
if min_account_age > now:
521+
# User account has not reached the required time
499522
reaction = blocked_emoji
500523
changed = False
501524
delta = human_timedelta(min_account_age)
@@ -514,6 +537,26 @@ async def process_modmail(self, message):
514537
color=discord.Color.red()
515538
))
516539

540+
elif min_guild_age > now:
541+
# User has not stayed in the guild for long enough
542+
reaction = blocked_emoji
543+
changed = False
544+
delta = human_timedelta(min_guild_age)
545+
546+
if str(message.author.id) not in self.blocked_users:
547+
new_reason = f'System Message: Recently Joined. Required to wait for {delta}.'
548+
self.config.blocked[str(message.author.id)] = new_reason
549+
await self.config.update()
550+
changed = True
551+
552+
if reason.startswith('System Message: Recently Joined.') or changed:
553+
await message.channel.send(embed=discord.Embed(
554+
title='Message not sent!',
555+
description=f'Your must wait for {delta} '
556+
f'before you can contact {self.user.mention}.',
557+
color=discord.Color.red()
558+
))
559+
517560
elif str(message.author.id) in self.blocked_users:
518561
reaction = blocked_emoji
519562
if reason.startswith('System Message: New Account.'):
@@ -524,8 +567,7 @@ async def process_modmail(self, message):
524567
else:
525568
end_time = re.search(r'%(.+?)%$', reason)
526569
if end_time is not None:
527-
after = (datetime.fromisoformat(end_time.group(1)) -
528-
datetime.utcnow()).total_seconds()
570+
after = (datetime.fromisoformat(end_time.group(1)) - now).total_seconds()
529571
if after <= 0:
530572
# No longer blocked
531573
reaction = sent_emoji
@@ -891,20 +933,20 @@ async def autoupdate_loop(self):
891933
embed.add_field(name='Merge Commit',
892934
value=f"[`{short_sha}`]({html_url}) "
893935
f"{message} - {user['username']}")
894-
logger.info(info('Updating bot.'))
936+
logger.info(info('Bot has been updated.'))
895937
channel = self.log_channel
896938
await channel.send(embed=embed)
897939

898940
await asyncio.sleep(3600)
899941

900942
async def metadata_loop(self):
901943
await self.wait_until_ready()
902-
self.owner = (await self.application_info()).owner
944+
owner = (await self.application_info()).owner
903945

904946
while not self.is_closed():
905947
data = {
906-
"owner_name": str(self.owner),
907-
"owner_id": self.owner.id,
948+
"owner_name": str(owner),
949+
"owner_id": owner.id,
908950
"bot_id": self.user.id,
909951
"bot_name": str(self.user),
910952
"avatar_url": self.user.avatar_url,
@@ -919,13 +961,9 @@ async def metadata_loop(self):
919961
"last_updated": str(datetime.utcnow())
920962
}
921963

964+
async with self.session.post('https://api.modmail.tk/metadata', json=data):
965+
logger.debug(info('Uploading metadata to Modmail server.'))
922966

923-
try:
924-
await self.session.post('https://api.modmail.tk/metadata', json=data)
925-
logger.debug('Posted metadata')
926-
except:
927-
pass
928-
929967
await asyncio.sleep(3600)
930968

931969

cogs/plugins.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import sys
99

1010
from discord.ext import commands
11+
from discord.utils import async_all
1112

1213
from core import checks
1314
from core.models import Bot, PermissionLevel
@@ -63,6 +64,8 @@ async def download_initial_plugins(self):
6364
except DownloadError as exc:
6465
msg = f'{parsed_plugin[0]}/{parsed_plugin[1]} - {exc}'
6566
logger.error(error(msg))
67+
await async_all(env() for env in self.bot.extra_events.get('on_plugin_ready', []))
68+
logger.debug(info('on_plugin_ready called.'))
6669

6770
async def download_plugin_repo(self, username, repo):
6871
try:

cogs/utility.py

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from datetime import datetime
77
from difflib import get_close_matches
88
from io import StringIO
9+
from operator import itemgetter
910
from typing import Union
1011
from json import JSONDecodeError
1112
from pkg_resources import parse_version
@@ -22,7 +23,7 @@
2223
from core.decorators import github_access_token_required, trigger_typing
2324
from core.models import Bot, InvalidConfigError, PermissionLevel
2425
from core.paginator import PaginatorSession, MessagePaginatorSession
25-
from core.utils import cleanup_code, info, error, User, perms_level
26+
from core.utils import cleanup_code, info, error, User, get_perm_level
2627

2728
logger = logging.getLogger('Modmail')
2829

@@ -39,12 +40,12 @@ async def format_cog_help(self, ctx, cog):
3940
prefix = self.bot.prefix
4041

4142
fmts = ['']
42-
for cmd in sorted(self.bot.commands,
43-
key=lambda cmd: perms_level(cmd)):
43+
for perm_level, cmd in sorted(((get_perm_level(c), c) for c in self.bot.commands),
44+
key=itemgetter(0)):
4445
if cmd.instance is cog and not cmd.hidden:
45-
new_fmt = f'`{prefix + cmd.qualified_name}` '
46-
perm_level = perms_level(cmd)
47-
if perm_level is not None:
46+
if perm_level is PermissionLevel.INVALID:
47+
new_fmt = f'`{prefix + cmd.qualified_name}` '
48+
else:
4849
new_fmt = f'`[{perm_level}] {prefix + cmd.qualified_name}` '
4950

5051
new_fmt += f'- {cmd.short_doc}\n'
@@ -83,15 +84,17 @@ async def format_command_help(self, cmd):
8384

8485
prefix = self.bot.prefix
8586

86-
perm_level = perms_level(cmd)
87-
perm_level = f'{perm_level.name} [{perm_level}]' if perm_level is not None else ''
87+
perm_level = get_perm_level(cmd)
88+
if perm_level is not PermissionLevel.INVALID:
89+
perm_level = f'{perm_level.name} [{perm_level}]'
90+
else:
91+
perm_level = ''
8892

8993
embed = Embed(
9094
title=f'`{prefix}{cmd.signature}`',
9195
color=self.bot.main_color,
9296
description=cmd.help
9397
)
94-
9598

9699
if not isinstance(cmd, commands.Group):
97100
embed.set_footer(text=f'Permission level: {perm_level}')
@@ -343,15 +346,19 @@ async def github(self, ctx):
343346
@checks.has_permissions(PermissionLevel.OWNER)
344347
@github_access_token_required
345348
@trigger_typing
346-
async def update(self, ctx):
347-
"""Updates the bot, this only works with heroku users."""
349+
async def update(self, ctx, *, flag: str = ''):
350+
"""Updates the bot, this only works with heroku users.
351+
352+
To stay up-to-date with the latest commit from GitHub, specify "force" as the flag.
353+
"""
354+
348355
changelog = await Changelog.from_url(self.bot)
349356
latest = changelog.latest_version
350357

351358
desc = (f'The latest version is [`{self.bot.version}`]'
352359
'(https://github.com/kyb3r/modmail/blob/master/bot.py#L25)')
353360

354-
if parse_version(self.bot.version) >= parse_version(latest.version):
361+
if parse_version(self.bot.version) >= parse_version(latest.version) and flag.lower() != 'force':
355362
embed = Embed(
356363
title='Already up to date',
357364
description=desc,

core/changelog.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,11 +106,11 @@ class Changelog:
106106
----------------
107107
RAW_CHANGELOG_URL : str
108108
The URL to Modmail changelog.
109+
CHANGELOG_URL : str
110+
The URL to Modmail changelog directly from in GitHub.
109111
VERSION_REGEX : re.Pattern
110112
The regex used to parse the versions.
111113
"""
112-
113-
114114

115115
RAW_CHANGELOG_URL = 'https://raw.githubusercontent.com/kyb3r/modmail/master/CHANGELOG.md'
116116
CHANGELOG_URL = 'https://github.com/kyb3r/modmail/blob/master/CHANGELOG.md'

core/config.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ class ConfigManager(ConfigManagerABC):
1919

2020
# bot settings
2121
'main_category_id', 'disable_autoupdates', 'prefix', 'mention',
22-
'main_color', 'user_typing', 'mod_typing', 'account_age', 'reply_without_command',
22+
'main_color', 'user_typing', 'mod_typing', 'account_age', 'guild_age',
23+
'reply_without_command',
2324

2425
# logging
2526
'log_channel_id',
@@ -71,7 +72,7 @@ class ConfigManager(ConfigManagerABC):
7172
}
7273

7374
time_deltas = {
74-
'account_age'
75+
'account_age', 'guild_age'
7576
}
7677

7778
valid_keys = allowed_to_change_in_command | internal_keys | protected_keys

core/models.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ class PermissionLevel(IntEnum):
2020
MOD = 3
2121
SUPPORTER = 2
2222
REGULAR = 1
23-
NONE = 0
23+
INVALID = -1
2424

2525

2626
class Bot(abc.ABC, commands.Bot):

0 commit comments

Comments
 (0)