Skip to content

Commit 9385f37

Browse files
committed
v3.3.2-dev5 - fix edit/delete
1 parent 611c951 commit 9385f37

File tree

7 files changed

+390
-192
lines changed

7 files changed

+390
-192
lines changed

.github/workflows/stale.yml

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
name: "Close stale issues"
1+
name: "Close Stale Issues"
2+
23
on:
34
schedule:
45
- cron: "0 0 * * *"

CHANGELOG.md

+10-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ This project mostly adheres to [Semantic Versioning](https://semver.org/spec/v2.
77
however, insignificant breaking changes does not guarantee a major version bump, see the reasoning [here](https://github.com/kyb3r/modmail/issues/319).
88

99

10-
# v3.3.2-dev4
10+
# v3.3.2-dev5
11+
12+
(Development update, very likely to be unstable!)
1113

1214
### Added
1315

@@ -21,20 +23,27 @@ however, insignificant breaking changes does not guarantee a major version bump,
2123
- Added "perhaps you meant" section to `?config help`.
2224
- Multi-command alias is now more stable. With support for a single quote escape `\"`.
2325
- New command `?freply`, which behaves exactly like `?reply` with the addition that you can substitute `{channel}`, `{recipient}`, and `{author}` to be their respective values.
26+
- New command `?repair`, repair any broken Modmail thread (with help from @officialpiyush).
27+
- Recipients gets a feedback when they edit message.
2428

2529
### Changed
2630

2731
- The look of alias and snippet when previewing.
32+
- Message ID of the thread embed is saved in DB, instead of the original message.
2833

2934
### Fixed
3035

3136
- Setting config vars using human time wasn't working.
3237
- Fixed some bugs with aliases.
38+
- Fixed a lot of issues with `?edit` and `?delete` and recipient message edit.
39+
- Masked the error: "AttributeError: 'int' object has no attribute 'name'"
40+
- Channel delete event will not be checked until discord.py fixes this issue.
3341

3442
### Internal
3543

3644
- Commit to black format line width max = 99, consistent with pylint.
3745
- Alias parser is rewritten without shlex.
46+
- New checks with thread create / find.
3847

3948
# v3.3.1
4049

bot.py

+35-22
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
__version__ = "3.3.2-dev4"
1+
__version__ = "3.3.2-dev5"
22

33

44
import asyncio
@@ -150,7 +150,7 @@ def session(self) -> ClientSession:
150150
return self._session
151151

152152
@property
153-
def api(self):
153+
def api(self) -> ApiClient:
154154
if self._api is None:
155155
self._api = ApiClient(self)
156156
return self._api
@@ -435,8 +435,13 @@ async def on_ready(self):
435435

436436
for recipient_id, items in tuple(closures.items()):
437437
after = (datetime.fromisoformat(items["time"]) - datetime.utcnow()).total_seconds()
438-
if after < 0:
438+
if after <= 0:
439+
logger.debug("Closing thread for recipient %s.", recipient_id)
439440
after = 0
441+
else:
442+
logger.debug(
443+
"Thread for recipient %s will be closed after %s seconds.", recipient_id, after
444+
)
440445

441446
thread = await self.threads.find(recipient_id=int(recipient_id))
442447

@@ -447,8 +452,6 @@ async def on_ready(self):
447452
await self.config.update()
448453
continue
449454

450-
logger.debug("Closing thread for recipient %s.", recipient_id)
451-
452455
await thread.close(
453456
closer=self.get_user(items["closer_id"]),
454457
after=after,
@@ -977,15 +980,20 @@ async def on_guild_channel_delete(self, channel):
977980
if channel.guild != self.modmail_guild:
978981
return
979982

980-
audit_logs = self.modmail_guild.audit_logs()
981-
entry = await audit_logs.find(lambda e: e.target.id == channel.id)
982-
mod = entry.user
983+
try:
984+
audit_logs = self.modmail_guild.audit_logs()
985+
entry = await audit_logs.find(lambda a: a.target == channel)
986+
mod = entry.user
987+
except AttributeError as e:
988+
# discord.py broken implementation with discord API
989+
logger.warning("Failed to retrieve audit log.", str(e))
990+
return
983991

984992
if mod == self.user:
985993
return
986994

987995
if isinstance(channel, discord.CategoryChannel):
988-
if self.main_category.id == channel.id:
996+
if self.main_category == channel:
989997
logger.debug("Main category was deleted.")
990998
self.config.remove("main_category_id")
991999
await self.config.update()
@@ -994,14 +1002,14 @@ async def on_guild_channel_delete(self, channel):
9941002
if not isinstance(channel, discord.TextChannel):
9951003
return
9961004

997-
if self.log_channel is None or self.log_channel.id == channel.id:
1005+
if self.log_channel is None or self.log_channel == channel:
9981006
logger.info("Log channel deleted.")
9991007
self.config.remove("log_channel_id")
10001008
await self.config.update()
10011009
return
10021010

10031011
thread = await self.threads.find(channel=channel)
1004-
if thread:
1012+
if thread and thread.channel == channel:
10051013
logger.debug("Manually closed channel %s.", channel.name)
10061014
await thread.close(closer=mod, silent=True, delete_channel=False)
10071015

@@ -1044,19 +1052,24 @@ async def on_bulk_message_delete(self, messages):
10441052
await discord.utils.async_all(self.on_message_delete(msg) for msg in messages)
10451053

10461054
async def on_message_edit(self, before, after):
1047-
if before.author.bot:
1055+
if after.author.bot:
10481056
return
1049-
if isinstance(before.channel, discord.DMChannel):
1057+
if isinstance(after.channel, discord.DMChannel):
10501058
thread = await self.threads.find(recipient=before.author)
1051-
async for msg in thread.channel.history():
1052-
if msg.embeds:
1053-
embed = msg.embeds[0]
1054-
matches = str(embed.author.url).split("/")
1055-
if matches and matches[-1] == str(before.id):
1056-
embed.description = after.content
1057-
await msg.edit(embed=embed)
1058-
await self.api.edit_message(str(after.id), after.content)
1059-
break
1059+
try:
1060+
await thread.edit_dm_message(after, after.content)
1061+
except ValueError:
1062+
_, blocked_emoji = await self.retrieve_emoji()
1063+
try:
1064+
await after.add_reaction(blocked_emoji)
1065+
except (discord.HTTPException, discord.InvalidArgument):
1066+
pass
1067+
else:
1068+
embed = discord.Embed(
1069+
description="Successfully Edited Message", color=self.main_color
1070+
)
1071+
embed.set_footer(text=f"Message ID: {after.id}")
1072+
await after.channel.send(embed=embed)
10601073

10611074
async def on_error(self, event_method, *args, **kwargs):
10621075
logger.error("Ignoring exception in %s.", event_method)

cogs/modmail.py

+126-44
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import asyncio
2+
import re
23
from datetime import datetime
34
from itertools import zip_longest
45
from typing import Optional, Union
@@ -14,6 +15,7 @@
1415
from core import checks
1516
from core.models import PermissionLevel, getLogger
1617
from core.paginator import EmbedPaginatorSession
18+
from core.thread import Thread
1719
from core.time import UserFriendlyTime, human_timedelta
1820
from core.utils import (
1921
format_preview,
@@ -22,6 +24,8 @@
2224
format_description,
2325
trigger_typing,
2426
escape_code_block,
27+
match_user_id,
28+
format_channel_name,
2529
)
2630

2731
logger = getLogger(__name__)
@@ -841,23 +845,6 @@ async def note(self, ctx, *, msg: str = ""):
841845
msg = await ctx.thread.note(ctx.message)
842846
await msg.pin()
843847

844-
async def find_linked_message(self, ctx, message_id):
845-
linked_message_id = None
846-
847-
async for msg in ctx.channel.history():
848-
if message_id is None and msg.embeds:
849-
embed = msg.embeds[0]
850-
if embed.color.value != self.bot.mod_color or not embed.author.url:
851-
continue
852-
# TODO: use regex to find the linked message id
853-
linked_message_id = str(embed.author.url).split("/")[-1]
854-
855-
elif message_id and msg.id == message_id:
856-
url = msg.embeds[0].author.url
857-
linked_message_id = str(url).split("/")[-1]
858-
859-
return linked_message_id
860-
861848
@commands.command()
862849
@checks.has_permissions(PermissionLevel.SUPPORTER)
863850
@checks.thread_only()
@@ -867,12 +854,14 @@ async def edit(self, ctx, message_id: Optional[int] = None, *, message: str):
867854
868855
If no `message_id` is provided,
869856
the last message sent by a staff will be edited.
857+
858+
Note: attachments **cannot** be edited.
870859
"""
871860
thread = ctx.thread
872861

873-
linked_message_id = await self.find_linked_message(ctx, message_id)
874-
875-
if linked_message_id is None:
862+
try:
863+
await thread.edit_message(message_id, message)
864+
except ValueError:
876865
return await ctx.send(
877866
embed=discord.Embed(
878867
title="Failed",
@@ -881,16 +870,8 @@ async def edit(self, ctx, message_id: Optional[int] = None, *, message: str):
881870
)
882871
)
883872

884-
await asyncio.gather(
885-
thread.edit_message(linked_message_id, message),
886-
self.bot.api.edit_message(linked_message_id, message),
887-
)
888-
889873
sent_emoji, _ = await self.bot.retrieve_emoji()
890-
try:
891-
await ctx.message.add_reaction(sent_emoji)
892-
except (discord.HTTPException, discord.InvalidArgument):
893-
pass
874+
return await ctx.message.add_reaction(sent_emoji)
894875

895876
@commands.command()
896877
@checks.has_permissions(PermissionLevel.SUPPORTER)
@@ -1168,7 +1149,7 @@ async def unblock(self, ctx, *, user: User = None):
11681149
@commands.command()
11691150
@checks.has_permissions(PermissionLevel.SUPPORTER)
11701151
@checks.thread_only()
1171-
async def delete(self, ctx, message_id: Optional[int] = None):
1152+
async def delete(self, ctx, message_id: int = None):
11721153
"""
11731154
Delete a message that was sent using the reply command or a note.
11741155
@@ -1179,15 +1160,9 @@ async def delete(self, ctx, message_id: Optional[int] = None):
11791160
"""
11801161
thread = ctx.thread
11811162

1182-
if message_id is not None:
1183-
try:
1184-
message_id = int(message_id)
1185-
except ValueError:
1186-
raise commands.BadArgument("A message ID needs to be specified.")
1187-
1188-
linked_message_id = await self.find_linked_message(ctx, message_id)
1189-
1190-
if linked_message_id is None:
1163+
try:
1164+
await thread.delete_message(message_id)
1165+
except ValueError:
11911166
return await ctx.send(
11921167
embed=discord.Embed(
11931168
title="Failed",
@@ -1196,12 +1171,119 @@ async def delete(self, ctx, message_id: Optional[int] = None):
11961171
)
11971172
)
11981173

1199-
await thread.delete_message(linked_message_id)
12001174
sent_emoji, _ = await self.bot.retrieve_emoji()
1201-
try:
1202-
await ctx.message.add_reaction(sent_emoji)
1203-
except (discord.HTTPException, discord.InvalidArgument):
1204-
pass
1175+
return await ctx.message.add_reaction(sent_emoji)
1176+
1177+
@commands.command()
1178+
@checks.has_permissions(PermissionLevel.SUPPORTER)
1179+
async def repair(self, ctx):
1180+
"""
1181+
Repair a thread broken by Discord.
1182+
"""
1183+
sent_emoji, blocked_emoji = await self.bot.retrieve_emoji()
1184+
1185+
if ctx.thread:
1186+
user_id = match_user_id(ctx.channel.topic)
1187+
if user_id == -1:
1188+
logger.info("Setting current channel's topic to User ID.")
1189+
await ctx.channel.edit(topic=f"User ID: {ctx.thread.id}")
1190+
return await ctx.message.add_reaction(sent_emoji)
1191+
1192+
logger.info("Attempting to fix a broken thread %s.", ctx.channel.name)
1193+
1194+
# Search cache for channel
1195+
user_id, thread = next(
1196+
((k, v) for k, v in self.bot.threads.cache.items() if v.channel == ctx.channel),
1197+
(-1, None),
1198+
)
1199+
if thread is not None:
1200+
logger.debug("Found thread with tempered ID.")
1201+
await ctx.channel.edit(reason="Fix broken Modmail thread", topic=f"User ID: {user_id}")
1202+
return await ctx.message.add_reaction(sent_emoji)
1203+
1204+
# find genesis message to retrieve User ID
1205+
async for message in ctx.channel.history(limit=10, oldest_first=True):
1206+
if (
1207+
message.author == self.bot.user
1208+
and message.embeds
1209+
and message.embeds[0].color
1210+
and message.embeds[0].color.value == self.bot.main_color
1211+
and message.embeds[0].footer.text
1212+
):
1213+
user_id = match_user_id(message.embeds[0].footer.text)
1214+
if user_id != -1:
1215+
recipient = self.bot.get_user(user_id)
1216+
if recipient is None:
1217+
self.bot.threads.cache[user_id] = thread = Thread(
1218+
self.bot.threads, user_id, ctx.channel
1219+
)
1220+
else:
1221+
self.bot.threads.cache[user_id] = thread = Thread(
1222+
self.bot.threads, recipient, ctx.channel
1223+
)
1224+
thread.ready = True
1225+
logger.info(
1226+
"Setting current channel's topic to User ID and created new thread."
1227+
)
1228+
await ctx.channel.edit(
1229+
reason="Fix broken Modmail thread", topic=f"User ID: {user_id}"
1230+
)
1231+
return await ctx.message.add_reaction(sent_emoji)
1232+
1233+
else:
1234+
logger.warning("No genesis message found.")
1235+
1236+
# match username from channel name
1237+
# username-1234, username-1234_1, username-1234_2
1238+
m = re.match(r"^(.+)-(\d{4})(?:_\d+)?$", ctx.channel.name)
1239+
if m is not None:
1240+
users = set(
1241+
filter(
1242+
lambda member: member.name == m.group(1)
1243+
and member.discriminator == m.group(2),
1244+
ctx.guild.members,
1245+
)
1246+
)
1247+
if len(users) == 1:
1248+
user = users[0]
1249+
name = format_channel_name(
1250+
user, self.bot.modmail_guild, exclude_channel=ctx.channel
1251+
)
1252+
recipient = self.bot.get_user(user.id)
1253+
if user.id in self.bot.threads.cache:
1254+
thread = self.bot.threads.cache[user.id]
1255+
if thread.channel:
1256+
embed = discord.Embed(
1257+
title="Delete Channel",
1258+
description="This thread channel is no longer in use. "
1259+
f"All messages will be directed to {ctx.channel.mention} instead.",
1260+
color=self.bot.error_color,
1261+
)
1262+
embed.set_footer(
1263+
text='Please manually delete this channel, do not use "{prefix}close".'
1264+
)
1265+
try:
1266+
await thread.channel.send(embed=embed)
1267+
except discord.HTTPException:
1268+
pass
1269+
if recipient is None:
1270+
self.bot.threads.cache[user.id] = thread = Thread(
1271+
self.bot.threads, user_id, ctx.channel
1272+
)
1273+
else:
1274+
self.bot.threads.cache[user.id] = thread = Thread(
1275+
self.bot.threads, recipient, ctx.channel
1276+
)
1277+
thread.ready = True
1278+
logger.info("Setting current channel's topic to User ID and created new thread.")
1279+
await ctx.channel.edit(
1280+
reason="Fix broken Modmail thread", name=name, topic=f"User ID: {user.id}"
1281+
)
1282+
return await ctx.message.add_reaction(sent_emoji)
1283+
1284+
elif len(users) >= 2:
1285+
logger.info("Multiple users with the same name and discriminator.")
1286+
return await ctx.message.add_reaction(blocked_emoji)
12051287

12061288
@commands.command()
12071289
@checks.has_permissions(PermissionLevel.ADMINISTRATOR)

0 commit comments

Comments
 (0)