Skip to content

Commit

Permalink
Merge branch 'master' of https://github.com/verixx/modmail
Browse files Browse the repository at this point in the history
  • Loading branch information
kyb3r committed May 13, 2019
2 parents 2d2f6c8 + dce7533 commit 05d8952
Show file tree
Hide file tree
Showing 9 changed files with 111 additions and 51 deletions.
17 changes: 16 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,22 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).


# v2.19.1

### Changed

- Ability to force an update despite having the same version number. Helpful to keep up-to-date with the latest GitHub commit.
- `?update force`.
- 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.

# v2.19.0

### What's new?

- 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.
- `guild_age` can be set the same way as `account_age`.

# v2.18.5

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

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


# v2.17.2

### Changed
Expand Down
16 changes: 5 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@
<a href="https://heroku.com/deploy?template=https://github.com/kyb3r/modmail">
<img src="https://img.shields.io/badge/deploy_to-heroku-997FBC.svg?style=for-the-badge">
</a>

<a href="https://github.com/kyb3r/modmail/">
<img src="https://api.modmail.tk/badges/instances.svg" alt="Bot instances">
</a>
<a href="https://discord.gg/j5e9p8w">
<img src="https://img.shields.io/discord/515071617815019520.svg?style=for-the-badge&colorB=7289DA" alt="Support">
</a>
Expand All @@ -22,7 +24,6 @@
</a>
</div>

---

## How Does Modmail Work?

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

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

---

# Notable Features

## Customizability

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.

## Linked Messages

<img src="https://i.imgur.com/6L9aaNw.png" align="right" height="350">

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.
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.

## Thread Logs

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.
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.

# Contributing

Expand Down
78 changes: 58 additions & 20 deletions bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
SOFTWARE.
"""

__version__ = '2.18.5'
__version__ = '2.19.1'

import asyncio
import logging
Expand Down Expand Up @@ -111,7 +111,6 @@ def __init__(self):
self.metadata_task = self.loop.create_task(self.metadata_loop())
self.autoupdate_task = self.loop.create_task(self.autoupdate_loop())
self._load_extensions()
self.owner = None

def _configure_logging(self):
level_text = self.config.log_level.upper()
Expand Down Expand Up @@ -202,12 +201,12 @@ def run(self, *args, **kwargs):
self.metadata_task.cancel()
self.loop.run_until_complete(self.metadata_task)
except asyncio.CancelledError:
logger.debug('data_task has been cancelled')
logger.debug(info('data_task has been cancelled.'))
try:
self.autoupdate_task.cancel()
self.loop.run_until_complete(self.autoupdate_task)
except asyncio.CancelledError:
logger.debug('autoupdate_task has been cancelled')
logger.debug(info('autoupdate_task has been cancelled.'))

self.loop.run_until_complete(self.logout())
for task in asyncio.Task.all_tasks():
Expand All @@ -217,7 +216,7 @@ def run(self, *args, **kwargs):
asyncio.gather(*asyncio.Task.all_tasks())
)
except asyncio.CancelledError:
logger.debug('All pending tasks has been cancelled')
logger.debug(info('All pending tasks has been cancelled.'))
finally:
self.loop.run_until_complete(self.session.close())
self.loop.close()
Expand Down Expand Up @@ -467,8 +466,10 @@ async def retrieve_emoji(self):
async def process_modmail(self, message):
"""Processes messages sent to the bot."""
sent_emoji, blocked_emoji = await self.retrieve_emoji()
now = datetime.utcnow()

account_age = self.config.get('account_age')
guild_age = self.config.get('guild_age')
if account_age is None:
account_age = isodate.duration.Duration()
else:
Expand All @@ -483,19 +484,41 @@ async def process_modmail(self, message):
await self.config.update()
account_age = isodate.duration.Duration()

if guild_age is None:
guild_age = isodate.duration.Duration()
else:
try:
guild_age = isodate.parse_duration(guild_age)
except isodate.ISO8601Error:
logger.warning('The guild join age limit needs to be a '
'ISO-8601 duration formatted duration string '
f'greater than 0 days, not "%s".', str(guild_age))
del self.config.cache['guild_age']
await self.config.update()
guild_age = isodate.duration.Duration()

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
min_account_age = now

if min_account_age > datetime.utcnow():
# user account has not reached the required time
try:
min_guild_age = self.guild.get_member(message.author.id).joined_at + guild_age
except ValueError as e:
logger.warning(e.args[0])
del self.config.cache['guild_age']
await self.config.update()
min_guild_age = now

if min_account_age > now:
# User account has not reached the required time
reaction = blocked_emoji
changed = False
delta = human_timedelta(min_account_age)
Expand All @@ -514,6 +537,26 @@ async def process_modmail(self, message):
color=discord.Color.red()
))

elif min_guild_age > now:
# User has not stayed in the guild for long enough
reaction = blocked_emoji
changed = False
delta = human_timedelta(min_guild_age)

if str(message.author.id) not in self.blocked_users:
new_reason = f'System Message: Recently Joined. 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: Recently Joined.') 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.'):
Expand All @@ -524,8 +567,7 @@ async def process_modmail(self, message):
else:
end_time = re.search(r'%(.+?)%$', reason)
if end_time is not None:
after = (datetime.fromisoformat(end_time.group(1)) -
datetime.utcnow()).total_seconds()
after = (datetime.fromisoformat(end_time.group(1)) - now).total_seconds()
if after <= 0:
# No longer blocked
reaction = sent_emoji
Expand Down Expand Up @@ -891,20 +933,20 @@ async def autoupdate_loop(self):
embed.add_field(name='Merge Commit',
value=f"[`{short_sha}`]({html_url}) "
f"{message} - {user['username']}")
logger.info(info('Updating bot.'))
logger.info(info('Bot has been updated.'))
channel = self.log_channel
await channel.send(embed=embed)

await asyncio.sleep(3600)

async def metadata_loop(self):
await self.wait_until_ready()
self.owner = (await self.application_info()).owner
owner = (await self.application_info()).owner

while not self.is_closed():
data = {
"owner_name": str(self.owner),
"owner_id": self.owner.id,
"owner_name": str(owner),
"owner_id": owner.id,
"bot_id": self.user.id,
"bot_name": str(self.user),
"avatar_url": self.user.avatar_url,
Expand All @@ -919,13 +961,9 @@ async def metadata_loop(self):
"last_updated": str(datetime.utcnow())
}

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

try:
await self.session.post('https://api.modmail.tk/metadata', json=data)
logger.debug('Posted metadata')
except:
pass

await asyncio.sleep(3600)


Expand Down
3 changes: 3 additions & 0 deletions cogs/plugins.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import sys

from discord.ext import commands
from discord.utils import async_all

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

async def download_plugin_repo(self, username, repo):
try:
Expand Down
31 changes: 19 additions & 12 deletions cogs/utility.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from datetime import datetime
from difflib import get_close_matches
from io import StringIO
from operator import itemgetter
from typing import Union
from json import JSONDecodeError
from pkg_resources import parse_version
Expand All @@ -22,7 +23,7 @@
from core.decorators import github_access_token_required, trigger_typing
from core.models import Bot, InvalidConfigError, PermissionLevel
from core.paginator import PaginatorSession, MessagePaginatorSession
from core.utils import cleanup_code, info, error, User, perms_level
from core.utils import cleanup_code, info, error, User, get_perm_level

logger = logging.getLogger('Modmail')

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

fmts = ['']
for cmd in sorted(self.bot.commands,
key=lambda cmd: perms_level(cmd)):
for perm_level, cmd in sorted(((get_perm_level(c), c) for c in self.bot.commands),
key=itemgetter(0)):
if cmd.instance is cog and not cmd.hidden:
new_fmt = f'`{prefix + cmd.qualified_name}` '
perm_level = perms_level(cmd)
if perm_level is not None:
if perm_level is PermissionLevel.INVALID:
new_fmt = f'`{prefix + cmd.qualified_name}` '
else:
new_fmt = f'`[{perm_level}] {prefix + cmd.qualified_name}` '

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

prefix = self.bot.prefix

perm_level = perms_level(cmd)
perm_level = f'{perm_level.name} [{perm_level}]' if perm_level is not None else ''
perm_level = get_perm_level(cmd)
if perm_level is not PermissionLevel.INVALID:
perm_level = f'{perm_level.name} [{perm_level}]'
else:
perm_level = ''

embed = Embed(
title=f'`{prefix}{cmd.signature}`',
color=self.bot.main_color,
description=cmd.help
)


if not isinstance(cmd, commands.Group):
embed.set_footer(text=f'Permission level: {perm_level}')
Expand Down Expand Up @@ -343,15 +346,19 @@ async def github(self, ctx):
@checks.has_permissions(PermissionLevel.OWNER)
@github_access_token_required
@trigger_typing
async def update(self, ctx):
"""Updates the bot, this only works with heroku users."""
async def update(self, ctx, *, flag: str = ''):
"""Updates the bot, this only works with heroku users.
To stay up-to-date with the latest commit from GitHub, specify "force" as the flag.
"""

changelog = await Changelog.from_url(self.bot)
latest = changelog.latest_version

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

if parse_version(self.bot.version) >= parse_version(latest.version):
if parse_version(self.bot.version) >= parse_version(latest.version) and flag.lower() != 'force':
embed = Embed(
title='Already up to date',
description=desc,
Expand Down
4 changes: 2 additions & 2 deletions core/changelog.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,11 +106,11 @@ class Changelog:
----------------
RAW_CHANGELOG_URL : str
The URL to Modmail changelog.
CHANGELOG_URL : str
The URL to Modmail changelog directly from in GitHub.
VERSION_REGEX : re.Pattern
The regex used to parse the versions.
"""



RAW_CHANGELOG_URL = 'https://raw.githubusercontent.com/kyb3r/modmail/master/CHANGELOG.md'
CHANGELOG_URL = 'https://github.com/kyb3r/modmail/blob/master/CHANGELOG.md'
Expand Down
5 changes: 3 additions & 2 deletions core/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ class ConfigManager(ConfigManagerABC):

# bot settings
'main_category_id', 'disable_autoupdates', 'prefix', 'mention',
'main_color', 'user_typing', 'mod_typing', 'account_age', 'reply_without_command',
'main_color', 'user_typing', 'mod_typing', 'account_age', 'guild_age',
'reply_without_command',

# logging
'log_channel_id',
Expand Down Expand Up @@ -71,7 +72,7 @@ class ConfigManager(ConfigManagerABC):
}

time_deltas = {
'account_age'
'account_age', 'guild_age'
}

valid_keys = allowed_to_change_in_command | internal_keys | protected_keys
Expand Down
2 changes: 1 addition & 1 deletion core/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ class PermissionLevel(IntEnum):
MOD = 3
SUPPORTER = 2
REGULAR = 1
NONE = 0
INVALID = -1


class Bot(abc.ABC, commands.Bot):
Expand Down
Loading

0 comments on commit 05d8952

Please sign in to comment.