Skip to content

Commit 4414638

Browse files
committed
Guarantee order of thread messages?
1 parent 8e2dd50 commit 4414638

File tree

2 files changed

+149
-103
lines changed

2 files changed

+149
-103
lines changed

core/decorators.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,21 @@
33
from discord import Embed, Color
44
from discord.ext import commands
55

6+
def queued():
7+
def decorator(coro):
8+
@functools.wraps(coro)
9+
async def wrapper(self, *args, **kwargs):
10+
await self.wait_until_ready()
11+
task = coro(self, *args, **kwargs)
12+
self._message_queue.put_nowait(task)
13+
return wrapper
14+
return decorator
15+
16+
async def ignore(coro):
17+
try:
18+
await coro
19+
except:
20+
pass
621

722
def trigger_typing(func):
823
@functools.wraps(func)

core/thread.py

Lines changed: 134 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
from core.models import Bot, ThreadManagerABC, ThreadABC
1111
from core.utils import is_image_url, days, match_user_id, truncate
12+
from core.decorators import ignore, queued
1213

1314

1415
class Thread(ThreadABC):
@@ -18,7 +19,7 @@ def __init__(self, manager: 'ThreadManager',
1819
recipient: typing.Union[discord.Member, discord.User,
1920
int],
2021
channel: typing.Union[discord.DMChannel,
21-
discord.TextChannel]):
22+
discord.TextChannel]=None):
2223
self.manager = manager
2324
self.bot = manager.bot
2425
if isinstance(recipient, int):
@@ -31,7 +32,9 @@ def __init__(self, manager: 'ThreadManager',
3132
self._recipient = recipient
3233
self._channel = channel
3334
self._ready_event = asyncio.Event()
35+
self._message_queue = asyncio.Queue()
3436
self._close_task = None
37+
self.bot.loop.create_task(self.process_messages())
3538

3639
def __repr__(self):
3740
return (f'Thread(recipient="{self.recipient or self.id}", '
@@ -71,6 +74,80 @@ def ready(self, flag):
7174
self._ready_event.set()
7275
else:
7376
self._ready_event.clear()
77+
78+
async def process_messages(self):
79+
"""Guarantees order of thread messages sent"""
80+
while True:
81+
task = await self._message_queue.get()
82+
await task
83+
84+
async def setup(self, *, creator=None, category=None):
85+
"""Create the thread channel and other io related initilisation tasks"""
86+
87+
recipient = self.recipient
88+
89+
# in case it creates a channel outside of category
90+
overwrites = {
91+
self.bot.modmail_guild.default_role:
92+
discord.PermissionOverwrite(read_messages=False)
93+
}
94+
95+
category = category or self.bot.main_category
96+
97+
if category is not None:
98+
overwrites = None
99+
100+
channel = await self.bot.modmail_guild.create_text_channel(
101+
name=self.manager._format_channel_name(recipient),
102+
category=category,
103+
overwrites=overwrites,
104+
reason='Creating a thread channel'
105+
)
106+
107+
self._channel = channel
108+
109+
log_url, log_data = await asyncio.gather(
110+
self.bot.api.create_log_entry(recipient, channel,
111+
creator or recipient),
112+
self.bot.api.get_user_logs(recipient.id)
113+
)
114+
115+
log_count = sum(1 for log in log_data if not log['open'])
116+
info_embed = self.manager._format_info_embed(recipient, log_url, log_count,
117+
discord.Color.green())
118+
119+
topic = f'User ID: {recipient.id}'
120+
if creator:
121+
mention = None
122+
else:
123+
mention = self.bot.config.get('mention', '@here')
124+
125+
_, msg = await asyncio.gather(
126+
channel.edit(topic=topic),
127+
channel.send(mention, embed=info_embed)
128+
)
129+
130+
self.ready = True
131+
132+
# Once thread is ready, tell the recipient.
133+
thread_creation_response = self.bot.config.get(
134+
'thread_creation_response',
135+
'The moderation team will get back to you as soon as possible!'
136+
)
137+
138+
embed = discord.Embed(
139+
color=self.bot.mod_color,
140+
description=thread_creation_response,
141+
timestamp=datetime.utcnow(),
142+
)
143+
embed.set_footer(text='Your message has been sent',
144+
icon_url=self.bot.guild.icon_url)
145+
embed.set_author(name='Thread Created')
146+
147+
if creator is None:
148+
self.bot.loop.create_task(recipient.send(embed=embed))
149+
150+
await msg.pin()
74151

75152
def _close_after(self, closer, silent, delete_channel, message):
76153
return self.bot.loop.create_task(
@@ -231,33 +308,45 @@ async def reply(self, message, anonymous=False):
231308
if not message.content and not message.attachments:
232309
raise UserInputError
233310
if all(not g.get_member(self.id) for g in self.bot.guilds):
234-
await message.channel.send(
311+
return await message.channel.send(
235312
embed=discord.Embed(
236313
color=discord.Color.red(),
237-
description='This user shares no servers with '
238-
'me and is thus unreachable.'
314+
description='Your message could not be delivered since'
315+
'the recipient shares no servers with the bot'
316+
))
317+
318+
tasks = []
319+
320+
try:
321+
await self.send(message,
322+
destination=self.recipient,
323+
from_mod=True,
324+
anonymous=anonymous)
325+
except Exception as e:
326+
print(e)
327+
tasks.append(message.channel.send(
328+
embed=discord.Embed(
329+
color=discord.Color.red(),
330+
description='Your message could not be delivered because '
331+
'the recipient is only accepting direct '
332+
'messages from friends, or the bot was '
333+
'blocked by the recipient.'
334+
)
335+
))
336+
else:
337+
# Send the same thing in the thread channel.
338+
tasks.append(
339+
self.send(message,
340+
destination=self.channel,
341+
from_mod=True,
342+
anonymous=anonymous)
239343
)
240-
)
241-
return
242-
243-
tasks = [
244-
# in thread channel
245-
self.send(message,
246-
destination=self.channel,
247-
from_mod=True,
248-
anonymous=anonymous),
249-
# to user
250-
self.send(message,
251-
destination=self.recipient,
252-
from_mod=True,
253-
anonymous=anonymous)
254-
]
255344

256-
await self.bot.api.append_log(
257-
message,
258-
self.channel.id,
259-
type_='anonymous' if anonymous else 'thread_message'
260-
)
345+
tasks.append(
346+
self.bot.api.append_log(message,
347+
self.channel.id,
348+
type_='anonymous' if anonymous else 'thread_message'
349+
))
261350

262351
if self.close_task is not None:
263352
# cancel closing if a thread message is sent.
@@ -273,21 +362,23 @@ async def reply(self, message, anonymous=False):
273362

274363
await asyncio.gather(*tasks)
275364

365+
@queued()
276366
async def send(self, message, destination=None,
277367
from_mod=False, note=False, anonymous=False):
278368
if self.close_task is not None:
279369
# cancel closing if a thread message is sent.
280-
await self.cancel_closure()
281-
await self.channel.send(embed=discord.Embed(
282-
color=discord.Color.red(),
283-
description='Scheduled close has been cancelled.'
284-
))
370+
tasks = asyncio.gather(
371+
self.cancel_closure(),
372+
self.channel.send(embed=discord.Embed(
373+
color=discord.Color.red(),
374+
description='Scheduled close has been cancelled.'
375+
)))
376+
self.bot.loop.create_task(tasks)
285377

286378
if not from_mod and not note:
287-
await self.bot.api.append_log(message, self.channel.id)
288-
289-
if not self.ready:
290-
await self.wait_until_ready()
379+
self.bot.loop.create_task(
380+
self.bot.api.append_log(message, self.channel.id)
381+
)
291382

292383
destination = destination or self.channel
293384

@@ -408,17 +499,11 @@ async def send(self, message, destination=None,
408499
mentions = None
409500

410501
await destination.send(mentions, embed=embed)
411-
412502
if additional_images:
413-
self.ready = False
414503
await asyncio.gather(*additional_images)
415-
self.ready = True
416504

417505
if delete_message:
418-
try:
419-
await message.delete()
420-
except discord.HTTPException:
421-
pass
506+
self.bot.loop.create_task(ignore(message.delete()))
422507

423508
def get_notifications(self):
424509
config = self.bot.config
@@ -517,75 +602,21 @@ async def _find_from_channel(self, channel):
517602

518603
return thread
519604

520-
async def create(self, recipient, *, creator=None, category=None):
605+
def create(self, recipient, *, creator=None, category=None):
521606
"""Creates a Modmail thread"""
522-
523-
thread_creation_response = self.bot.config.get(
524-
'thread_creation_response',
525-
'The moderation team will get back to you as soon as possible!'
526-
)
527-
528-
embed = discord.Embed(
529-
color=self.bot.mod_color,
530-
description=thread_creation_response,
531-
timestamp=datetime.utcnow(),
532-
)
533-
embed.set_footer(text='Your message has been sent',
534-
icon_url=self.bot.guild.icon_url)
535-
embed.set_author(name='Thread Created')
536-
537-
if creator is None:
538-
self.bot.loop.create_task(recipient.send(embed=embed))
539-
540-
# in case it creates a channel outside of category
541-
overwrites = {
542-
self.bot.modmail_guild.default_role:
543-
discord.PermissionOverwrite(read_messages=False)
544-
}
545-
546-
category = category or self.bot.main_category
547-
548-
if category is not None:
549-
overwrites = None
550-
551-
channel = await self.bot.modmail_guild.create_text_channel(
552-
name=self._format_channel_name(recipient),
553-
category=category,
554-
overwrites=overwrites,
555-
reason='Creating a thread channel'
556-
)
557-
558-
thread = Thread(self, recipient, channel)
607+
# create thread immediately so messages can be processed
608+
thread = Thread(self, recipient)
559609
self.cache[recipient.id] = thread
560610

561-
log_url, log_data = await asyncio.gather(
562-
self.bot.api.create_log_entry(recipient, channel,
563-
creator or recipient),
564-
self.bot.api.get_user_logs(recipient.id)
565-
)
566-
567-
log_count = sum(1 for log in log_data if not log['open'])
568-
info_embed = self._format_info_embed(recipient, log_url, log_count,
569-
discord.Color.green())
570-
571-
topic = f'User ID: {recipient.id}'
572-
if creator:
573-
mention = None
574-
else:
575-
mention = self.bot.config.get('mention', '@here')
576-
577-
_, msg = await asyncio.gather(
578-
channel.edit(topic=topic),
579-
channel.send(mention, embed=info_embed)
580-
)
581-
582-
thread.ready = True
583-
await msg.pin()
611+
# Schedule thread setup for later
612+
self.bot.loop.create_task(thread.setup(
613+
creator=creator,
614+
category=category
615+
))
584616
return thread
585617

586618
async def find_or_create(self, recipient):
587-
return await self.find(recipient=recipient) or \
588-
await self.create(recipient)
619+
return await self.find(recipient=recipient) or self.create(recipient)
589620

590621
def _format_channel_name(self, author):
591622
"""Sanitises a username for use with text channel names"""

0 commit comments

Comments
 (0)