Skip to content

Commit 4d20da4

Browse files
authored
Improved close command + custom thread creation response. (#124)
* Add close after functionality to the thread class. * Add a ctx.thread for convienience. * Move thread close logging within thread class * Update close-cmd branch with master (#123) * Add support for multiple image and file attachments * Add support for custom sent emoji and blocked emoji resolves #112 * Fix emoji handling * Fixed a bug where blocked users are still able to message modmail * Improve about command style * Increment version * Fix changelog version parsing * Add changelog docstring * Add dhooks to requirements for eval stuff * Add discord server badge * Update README.md added forks badge * Use blurple color * Use stars instead * Let anyone use alias invoked eval commands * Add an instances badge * Update README.md * Update README.md * Update README.md * Remove the first desc * Update README.md * Add patreon in readme * Update README.md * Improve logs format * Update README.md * Rainbow colors + patreon * Change order * Test * Revert "Test" This reverts commit 56e68fc. * Basic closing after a certain amount. * Increment version * Update requirements.txt * Add UserFriendlyTime conveter from Rapptz * Use user friendly time converter. * Add support for custom close message * Default arg to None * Add ability to silently close. * fix * Add ability to set custom thread creation response * Change thread creation title * Update thread.py
1 parent 27a33dd commit 4d20da4

File tree

7 files changed

+355
-87
lines changed

7 files changed

+355
-87
lines changed

CHANGELOG.md

+13-1
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,20 @@ 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-
# v[unreleased]
7+
8+
9+
# v2.1.0
10+
11+
### Added
12+
- Ability to set a custom thread creation response message.
13+
- Do this via `config set thread_creation_response [message]`
14+
15+
### Changed
816
- Improve logs command format.
17+
- Improve thread log channel message to have more relevant info.
18+
- Improve close command.
19+
- You now can close the thread after a delay and use a custom thread close message.
20+
- You also now have the ability to close a thread silently.
921

1022
# v2.0.10
1123

bot.py

+13-38
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
SOFTWARE.
2323
"""
2424

25-
__version__ = '2.0.10'
25+
__version__ = '2.1.0'
2626

2727
import asyncio
2828
import textwrap
@@ -284,46 +284,21 @@ async def on_message(self, message):
284284
async def on_guild_channel_delete(self, channel):
285285
if channel.guild != self.modmail_guild:
286286
return
287-
thread = await self.threads.find(channel=channel)
288-
if thread:
289-
del self.threads.cache[thread.id]
290-
291-
mod = None
292-
293-
audit_logs = self.modmail_guild.audit_logs()
294-
entry = await audit_logs.find(lambda e: e.target.id == channel.id)
295-
mod = entry.user
296-
if mod.bot:
297-
return
298-
299-
log_data = await self.modmail_api.post_log(channel.id, {
300-
'open': False,
301-
'closed_at': str(datetime.datetime.utcnow()),
302-
'closer': {
303-
'id': str(mod.id),
304-
'name': mod.name,
305-
'discriminator': mod.discriminator,
306-
'avatar_url': mod.avatar_url,
307-
'mod': True
308-
}})
309-
310-
em = discord.Embed(title='Thread Closed')
311-
em.description = f'{mod.mention} has closed this modmail thread.'
312-
em.color = discord.Color.red()
313287

314-
try:
315-
await thread.recipient.send(embed=em)
316-
except:
317-
pass
318-
319-
log_url = f"https://logs.modmail.tk/{log_data['user_id']}/{log_data['key']}"
288+
mod = None
289+
audit_logs = self.modmail_guild.audit_logs()
290+
entry = await audit_logs.find(lambda e: e.target.id == channel.id)
291+
mod = entry.user
320292

321-
user = thread.recipient.mention if thread.recipient else f'`{thread.id}`'
293+
if mod.bot:
294+
return
295+
296+
thread = await self.threads.find(channel=channel)
297+
if not thread:
298+
return
299+
300+
await thread.close(closer=mod, silent=True, delete_channel=False)
322301

323-
desc = f"[`{log_data['key']}`]({log_url}) {mod.mention} closed a thread with {user}"
324-
em = discord.Embed(description=desc, color=em.color)
325-
em.set_author(name='Thread closed', url=log_url)
326-
await self.log_channel.send(embed=em)
327302

328303
async def on_message_delete(self, message):
329304
"""Support for deleting linked messages"""

cogs/modmail.py

+54-38
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
11

2+
3+
import datetime
4+
from typing import Optional, Union
5+
import re
6+
27
import discord
38
from discord.ext import commands
4-
import datetime
59
import dateutil.parser
6-
from typing import Optional, Union
10+
711
from core.decorators import trigger_typing
812
from core.paginator import PaginatorSession
13+
from core.time import UserFriendlyTime, human_timedelta
914

1015

1116
class Modmail:
@@ -117,50 +122,61 @@ async def move(self, ctx, *, category: discord.CategoryChannel):
117122
await thread.channel.edit(category=category)
118123
await ctx.message.add_reaction('✅')
119124

120-
@commands.command(name='close')
121-
@commands.has_permissions(manage_channels=True)
122-
async def _close(self, ctx):
123-
"""Close the current thread."""
124-
125-
thread = await self.bot.threads.find(channel=ctx.channel)
126-
if not thread:
127-
return await ctx.send('This is not a modmail thread.')
128-
129-
await thread.close()
125+
async def send_scheduled_close_message(self, ctx, after, silent=False):
126+
human_delta = human_timedelta(after.dt)
127+
128+
silent = '*silently* ' if silent else ''
130129

131-
em = discord.Embed(title='Thread Closed')
132-
em.description = f'{ctx.author.mention} has closed this modmail thread.'
133-
em.color = discord.Color.red()
130+
em = discord.Embed(
131+
title='Scheduled close',
132+
description=f'This thread will close {silent}in {human_delta}.',
133+
color=discord.Color.red()
134+
)
134135

135-
try:
136-
await thread.recipient.send(embed=em)
137-
except:
138-
pass
136+
if after.arg and not silent:
137+
em.add_field(name='Message', value=after.arg)
138+
139+
em.set_footer(text='Closing will be cancelled if a thread message is sent.')
140+
em.timestamp = after.dt
141+
142+
await ctx.send(embed=em)
139143

140-
# Logging
141-
log_channel = self.bot.log_channel
144+
@commands.command(name='close', usage='[after] [close message]')
145+
async def _close(self, ctx, *, after: UserFriendlyTime=None):
146+
"""Close the current thread.
147+
148+
Close after a period of time:
149+
- `close in 5 hours`
150+
- `close 2m30s`
151+
152+
Custom close messages:
153+
- `close 2 hours The issue has been resolved.`
154+
- `close We will contact you once we find out more.`
142155
143-
log_data = await self.bot.modmail_api.post_log(ctx.channel.id, {
144-
'open': False, 'closed_at': str(datetime.datetime.utcnow()), 'closer': {
145-
'id': str(ctx.author.id),
146-
'name': ctx.author.name,
147-
'discriminator': ctx.author.discriminator,
148-
'avatar_url': ctx.author.avatar_url,
149-
'mod': True
150-
}
151-
})
156+
Silently close a thread (no message)
157+
- `close silently`
158+
- `close in 10m silently`
159+
"""
152160

153-
if isinstance(log_data, str):
154-
print(log_data) # error
161+
thread = await self.bot.threads.find(channel=ctx.channel)
162+
if not thread:
163+
return
164+
165+
now = datetime.datetime.utcnow()
155166

156-
log_url = f"https://logs.modmail.tk/{log_data['user_id']}/{log_data['key']}"
167+
close_after = (after.dt - now).total_seconds() if after else 0
168+
message = after.arg if after else None
169+
silent = str(message).lower() in {'silent', 'silently'}
157170

158-
user = thread.recipient.mention if thread.recipient else f'`{thread.id}`'
171+
if after and after.dt > now:
172+
await self.send_scheduled_close_message(ctx, after, silent)
159173

160-
desc = f"[`{log_data['key']}`]({log_url}) {ctx.author.mention} closed a thread with {user}"
161-
em = discord.Embed(description=desc, color=em.color)
162-
em.set_author(name='Thread closed', url=log_url)
163-
await log_channel.send(embed=em)
174+
await thread.close(
175+
closer=ctx.author,
176+
after=close_after,
177+
message=message,
178+
silent=silent,
179+
)
164180

165181
@commands.command()
166182
async def nsfw(self, ctx):

core/config.py

+7-4
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,18 @@ class ConfigManager:
88

99
allowed_to_change_in_command = {
1010
'status', 'log_channel_id', 'mention', 'disable_autoupdates', 'prefix',
11-
'main_category_id', 'sent_emoji', 'blocked_emoji'
11+
'main_category_id', 'sent_emoji', 'blocked_emoji', 'thread_creation_response'
1212
}
1313

1414
internal_keys = {
15-
'token', 'snippets', 'aliases', 'owners', 'modmail_api_token',
16-
'guild_id', 'modmail_guild_id', 'blocked'
15+
'snippets', 'aliases', 'blocked'
16+
}
17+
18+
protected_keys = {
19+
'token', 'owners', 'modmail_api_token', 'guild_id', 'modmail_guild_id',
1720
}
1821

19-
valid_keys = allowed_to_change_in_command.union(internal_keys)
22+
valid_keys = allowed_to_change_in_command.union(internal_keys).union(protected_keys)
2023

2124
def __init__(self, bot):
2225
self.bot = bot

core/thread.py

+73-6
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ def __init__(self, manager, recipient):
2323
self.recipient = recipient
2424
self.channel = None
2525
self.ready_event = asyncio.Event()
26+
self.close_task = None
27+
self.close_after = 0 # seconds
2628

2729
def __repr__(self):
2830
return f'Thread(recipient="{self.recipient}", channel={self.channel.id})'
@@ -39,10 +41,64 @@ def ready(self):
3941
def ready(self, flag):
4042
if flag is True:
4143
self.ready_event.set()
44+
45+
async def _close_after(self, after, **kwargs):
46+
await asyncio.sleep(after)
47+
await self.close(**kwargs)
48+
49+
async def close(self, *, closer, after=0, silent=False, delete_channel=True, message=None):
50+
'''Close a thread now or after a set time in seconds'''
51+
if after > 0:
52+
if self.close_task is not None and not self.close_task.cancelled():
53+
self.close_task.cancel()
54+
self.close_task = asyncio.create_task(self._close_after(after, closer=closer, silent=silent, message=message))
55+
return
4256

43-
def close(self):
57+
4458
del self.manager.cache[self.id]
45-
return self.channel.delete()
59+
60+
# Logging
61+
log_data = await self.bot.modmail_api.post_log(self.channel.id, {
62+
'open': False, 'closed_at': str(datetime.datetime.utcnow()), 'closer': {
63+
'id': str(closer.id),
64+
'name': closer.name,
65+
'discriminator': closer.discriminator,
66+
'avatar_url': closer.avatar_url,
67+
'mod': True
68+
}
69+
})
70+
71+
if isinstance(log_data, str):
72+
print(log_data) # errored somehow on server
73+
74+
log_url = f"https://logs.modmail.tk/{log_data['user_id']}/{log_data['key']}"
75+
user = self.recipient.mention if self.recipient else f'`{self.id}`'
76+
77+
if log_data['messages']:
78+
msg = log_data['messages'][0]['content']
79+
sneak_peak = msg if len(msg) < 50 else msg[:48] + '...'
80+
else:
81+
sneak_peak = 'No content'
82+
83+
desc = f"[`{log_data['key']}`]({log_url}) {user}: {sneak_peak}"
84+
85+
em = discord.Embed(description=desc, color=discord.Color.red())
86+
em.set_author(name='Thread closed', url=log_url)
87+
em.set_footer(text=f'Closed by: {closer} ({closer.id})')
88+
89+
tasks = [self.bot.log_channel.send(embed=em)]
90+
91+
em = discord.Embed(title='Thread Closed')
92+
em.description = message or f'{closer.mention} has closed this modmail thread.'
93+
em.color = discord.Color.red()
94+
95+
if not silent:
96+
tasks.append(self.recipient.send(embed=em))
97+
98+
if delete_channel:
99+
tasks.append(self.channel.delete())
100+
101+
await asyncio.gather(*tasks)
46102

47103
async def _edit_thread_message(self, channel, message_id, message):
48104
async for msg in channel.history():
@@ -68,12 +124,22 @@ async def reply(self, message):
68124
raise commands.UserInputError
69125
if self.recipient not in self.bot.guild.members:
70126
return await message.channel.send('This user is no longer in the server and is thus unreachable.')
71-
await asyncio.gather(
127+
128+
tasks = [
72129
self.send(message, self.channel, from_mod=True), # in thread channel
73130
self.send(message, self.recipient, from_mod=True) # to user
74-
)
131+
]
132+
133+
if self.close_task is not None and not self.close_task.cancelled():
134+
self.close_task.cancel() # cancel closing if a thread message is sent.
135+
tasks.append(self.channel.send(embed=discord.Embed(color=discord.Color.red(), description='Scheduled close has been cancelled.')))
136+
137+
await asyncio.gather(*tasks)
75138

76139
async def send(self, message, destination=None, from_mod=False, delete_message=True):
140+
if self.close_task is not None and not self.close_task.cancelled():
141+
self.close_task.cancel() # cancel closing if a thread message is sent.
142+
await self.channel.send(embed=discord.Embed(color=discord.Color.red(), description='Scheduled close has been cancelled.'))
77143
if not self.ready:
78144
await self.wait_until_ready()
79145

@@ -123,6 +189,7 @@ async def send(self, message, destination=None, from_mod=False, delete_message=T
123189

124190
file_upload_count = 1
125191

192+
126193
for att in attachments:
127194
em.add_field(name=f'File upload ({file_upload_count})', value=f'[{att[1]}]({att[0]})')
128195
file_upload_count += 1
@@ -224,8 +291,8 @@ async def create(self, recipient, *, creator=None):
224291
"""Creates a modmail thread"""
225292

226293
em = discord.Embed(
227-
title='Thread started' if creator else 'Thanks for the message!',
228-
description='The moderation team will get back to you as soon as possible!',
294+
title='Thread created!',
295+
description=self.bot.config.get('thread_creation_response', 'The moderation team will get back to you as soon as possible!'),
229296
color=discord.Color.green()
230297
)
231298

0 commit comments

Comments
 (0)