Skip to content

Commit f6bb2e7

Browse files
committed
Refactor thread management
1 parent 7456f4f commit f6bb2e7

File tree

1 file changed

+218
-0
lines changed

1 file changed

+218
-0
lines changed

utils/modmail.py

Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
import discord
2+
from discord.ext import commands
3+
import asyncio
4+
import functools
5+
import re
6+
7+
class Thread:
8+
def __init__(self, manager, recipient):
9+
self.manager = manager
10+
self.bot = manager.bot
11+
self.id = recipient.id if recipient else None
12+
self.recipient = recipient
13+
self.channel = None
14+
self.ready_event = asyncio.Event()
15+
16+
def wait_until_ready(self):
17+
'''Blocks execution until the thread is fully set up.'''
18+
return self.ready_event.wait()
19+
20+
@property
21+
def ready(self):
22+
return self.ready_event.is_set()
23+
24+
@ready.setter
25+
def ready(self, flag):
26+
if flag is True:
27+
self.ready_event.set()
28+
29+
def close(self):
30+
del self.manager.cache[self.id]
31+
return self.channel.delete()
32+
33+
async def _edit_thread_message(self, channel, message_id, message):
34+
async for msg in channel.history():
35+
if msg.embeds:
36+
embed = msg.embeds[0]
37+
if f'Moderator - {message_id}' in embed.footer.text:
38+
if ' - (Edited)' not in embed.footer.text:
39+
embed.set_footer(text=embed.footer.text + ' - (Edited)')
40+
embed.description = message
41+
await msg.edit(embed=embed)
42+
break
43+
44+
def edit_message(self, message_id, message):
45+
return asyncio.gather(
46+
self._edit_thread_message(self.recipient, message_id, message),
47+
self._edit_thread_message(self.channel, message_id, message)
48+
)
49+
50+
async def reply(self, message):
51+
if not message.content and not message.attachments:
52+
raise commands.UserInputError('msg is a required argument.')
53+
if not self.recipient:
54+
return await message.channel.send('This user does not share any servers with the bot and is thus unreachable.')
55+
await asyncio.gather(
56+
self.send(message, self.channel, from_mod=True), # in thread channel
57+
self.send(message, self.recipient, from_mod=True) # to user
58+
)
59+
60+
async def send(self, message, destination=None, from_mod=False, delete_message=True):
61+
destination = destination or self.channel
62+
if from_mod and not isinstance(destination, discord.User):
63+
asyncio.create_task(self.bot.modmail_api.append_log(message))
64+
elif not from_mod:
65+
asyncio.create_task(self.bot.modmail_api.append_log(message, destination.id))
66+
67+
author = message.author
68+
69+
em = discord.Embed(
70+
description=message.content,
71+
timestamp=message.created_at
72+
)
73+
74+
image_types = ['.png', '.jpg', '.gif', '.jpeg', '.webp']
75+
is_image_url = lambda u: any(urlparse(u).path.endswith(x) for x in image_types)
76+
77+
delete_message = not bool(message.attachments)
78+
attachments = list(filter(lambda a: not is_image_url(a.url), message.attachments))
79+
80+
image_urls = [a.url for a in message.attachments]
81+
image_urls.extend(re.findall(r'(https?://[^\s]+)', message.content))
82+
image_urls = list(filter(is_image_url, image_urls))
83+
84+
if image_urls:
85+
em.set_image(url=image_urls[0])
86+
87+
if attachments:
88+
att = attachments[0]
89+
em.add_field(name='File Attachment', value=f'[{att.filename}]({att.url})')
90+
91+
if from_mod:
92+
em.color=discord.Color.green()
93+
em.set_author(name=str(author), icon_url=author.avatar_url)
94+
em.set_footer(text=f'Moderator - {message.id}')
95+
else:
96+
em.color=discord.Color.gold()
97+
em.set_author(name=str(author), icon_url=author.avatar_url)
98+
em.set_footer(text=f'User - {message.id}')
99+
100+
101+
await destination.trigger_typing()
102+
await destination.send(embed=em)
103+
104+
if delete_message:
105+
try:
106+
await message.delete()
107+
except:
108+
pass
109+
110+
class ThreadManager:
111+
def __init__(self, bot):
112+
self.bot = bot
113+
self.cache = {}
114+
115+
def __len__(self):
116+
return len(self.cache)
117+
118+
def __iter__(self):
119+
return iter(self.cache.values())
120+
121+
def __getitem__(self, item):
122+
return self.cache[item]
123+
124+
async def find(self, *, recipient=None, channel=None):
125+
'''Finds a thread from cache or from discord channel topics.'''
126+
if recipient is None and channel is not None:
127+
return await self._find_from_channel(channel)
128+
try:
129+
thread = self.cache[recipient.id]
130+
except KeyError:
131+
channel = discord.utils.get(
132+
self.bot.guild.text_channels,
133+
topic=f'User ID: {recipient.id}'
134+
)
135+
if not channel:
136+
thread = None
137+
else:
138+
self.cache[recipient.id] = thread = Thread(self, recipient)
139+
thread.channel = channel
140+
thread.ready = True
141+
finally:
142+
return thread
143+
144+
async def _find_from_channel(self, channel):
145+
'''
146+
Tries to find a thread from a channel channel topic,
147+
if channel topic doesnt exist for some reason, falls back to
148+
searching channel history for genesis embed and extracts user_id fron that.
149+
'''
150+
user_id = None
151+
152+
if channel.topic and 'User ID: ' in channel.topic:
153+
user_id = int(re.findall(r'\d+', channel.topic)[0])
154+
elif channel.topic is None:
155+
async for message in channel.history():
156+
if message.embeds:
157+
em = message.embeds[0]
158+
matches = re.findall(r'<@(\d+)>', str(em.description))
159+
if matches:
160+
user_id = int(matches[-1])
161+
break
162+
163+
if user_id is not None:
164+
if user_id in self.cache:
165+
return self.cache[user_id]
166+
167+
recipient = self.bot.get_user(user_id) # this could be None
168+
169+
self.cache[user_id] = thread = Thread(self, recipient)
170+
thread.channel = channel
171+
thread.id = user_id
172+
173+
return thread
174+
175+
async def create(self, recipient, *, creator=None):
176+
'''Creates a modmail thread'''
177+
178+
em = discord.Embed(
179+
title='Thread started' if creator else 'Thanks for the message!',
180+
description='The moderation team will get back to you as soon as possible!',
181+
color=discord.Color.green()
182+
)
183+
184+
if creator is not None:
185+
em.description = f'{creator.mention} has started a modmail thread with you.'
186+
187+
asyncio.create_task(recipient.send(embed=em))
188+
189+
self.cache[recipient.id] = thread = Thread(self, recipient)
190+
191+
channel = await self.bot.guild.create_text_channel(
192+
name=self.bot.format_channel_name(recipient),
193+
category=self.bot.main_category
194+
)
195+
196+
thread.channel = channel
197+
198+
log_url, log_data = await asyncio.gather(
199+
self.bot.modmail_api.get_log_url(recipient, channel, creator or recipient),
200+
self.bot.modmail_api.get_user_logs(recipient.id)
201+
)
202+
203+
log_count = len(log_data)
204+
info_embed = self.bot.format_info(recipient, creator, log_url, log_count)
205+
206+
topic = f'User ID: {recipient.id}'
207+
mention = self.bot.config.get('MENTION', '@here')
208+
209+
await asyncio.gather(
210+
channel.edit(topic=topic),
211+
channel.send(mention, embed=info_embed)
212+
)
213+
214+
thread.ready = True
215+
return thread
216+
217+
async def find_or_create(self, recipient):
218+
return await self.find(recipient=recipient) or await self.create(recipient)

0 commit comments

Comments
 (0)