-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmessage_reaction.py
More file actions
243 lines (195 loc) · 9.74 KB
/
message_reaction.py
File metadata and controls
243 lines (195 loc) · 9.74 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
import discord
import helpers
import database
from logger_config import get_logger
logger = get_logger(__name__)
async def handle_reaction_add(bot, payload: discord.RawReactionActionEvent):
"""Propagate reaction additions to all linked messages."""
await _process_reaction(bot, payload, operation="add")
async def handle_reaction_remove(bot, payload: discord.RawReactionActionEvent):
"""Propagate reaction removals to all linked messages."""
await _process_reaction(bot, payload, operation="remove")
async def _process_reaction(bot, payload: discord.RawReactionActionEvent, operation: str):
"""Route reaction events between regular channels and threads."""
if payload.user_id == bot.user.id:
return
if not payload.guild_id:
logger.debug("Reaction event without guild_id; ignoring.")
return
channel = await _resolve_channel(bot, payload.guild_id, payload.channel_id)
if channel is None:
logger.warning(f"Unable to resolve channel {payload.channel_id} for reaction event.")
return
try:
message = await channel.fetch_message(payload.message_id)
except discord.Forbidden:
logger.error(f"No permission to fetch message {payload.message_id} in channel {channel.id}")
return
except discord.NotFound:
logger.warning(f"Message {payload.message_id} not found in channel {channel.id}")
return
except discord.HTTPException as exc:
logger.error(f"Failed to fetch message {payload.message_id}: {exc}")
return
emoji = payload.emoji
if isinstance(channel, discord.Thread):
if _is_forum_thread(channel):
await _process_forum_thread_message_reaction(bot, message, emoji, operation)
else:
await _process_thread_message_reaction(bot, message, emoji, operation)
else:
await _process_channel_message_reaction(bot, message, emoji, operation)
def _is_forum_thread(thread: discord.Thread) -> bool:
parent = thread.parent
if parent is None:
return False
if isinstance(parent, discord.ForumChannel):
return True
return getattr(parent, "type", None) == discord.ChannelType.forum
async def _process_channel_message_reaction(bot, message: discord.Message, emoji, operation: str):
"""Handle reaction propagation for messages in regular channels."""
channel_id = str(message.channel.id)
target_channel_ids = helpers.find_linked_channels(channel_id)
if target_channel_ids is None:
logger.debug(f"No linked channels configured for channel {channel_id}")
return
group_name = helpers.get_group_name(channel_id)
if not group_name:
logger.warning(f"No group name found for channel {channel_id}")
return
message_entry = database.get_message_group_entry_by_message_id(str(message.id), group_name)
if not message_entry:
logger.debug(f"No message entry found for message {message.id} in group {group_name}")
return
source_guild_id = helpers.get_guild_id_from_channel_id(channel_id)
for entry in message_entry:
if entry["channel_id"] == channel_id and entry["guild_id"] == source_guild_id:
continue
await _apply_reaction_to_entry(bot, entry, emoji, operation)
async def _process_thread_message_reaction(bot, message: discord.Message, emoji, operation: str):
"""Handle reaction propagation for messages posted inside threads."""
thread = message.channel
parent_channel_id = str(thread.parent_id)
target_channel_ids = helpers.find_linked_channels(parent_channel_id)
if target_channel_ids is None:
logger.debug(f"No linked channels configured for parent channel {parent_channel_id}")
return
group_name = helpers.get_group_name(parent_channel_id)
if not group_name:
logger.warning(f"No group name found for parent channel {parent_channel_id}")
return
message_entry = database.get_message_group_entry_by_message_id(str(message.id), group_name)
if not message_entry:
logger.debug(f"No thread message entry found for message {message.id} in group {group_name}")
return
source_guild_id = helpers.get_guild_id_from_channel_id(parent_channel_id)
thread_id = str(thread.id)
for entry in message_entry:
if (
entry["channel_id"] == parent_channel_id
and entry["guild_id"] == source_guild_id
and entry.get("thread_id") == thread_id
):
continue
await _apply_reaction_to_entry(bot, entry, emoji, operation)
async def _process_forum_thread_message_reaction(bot, message: discord.Message, emoji, operation: str):
"""Handle reaction propagation for messages posted inside forum threads."""
thread = message.channel
parent_channel_id = str(thread.parent_id)
target_channel_ids = helpers.find_linked_channels(parent_channel_id)
if target_channel_ids is None:
logger.debug(f"No linked channels configured for forum parent channel {parent_channel_id}")
return
group_name = helpers.get_group_name(parent_channel_id)
if not group_name:
logger.warning(f"No group name found for forum parent channel {parent_channel_id}")
return
message_entry = database.get_message_group_entry_by_message_id(str(message.id), group_name)
if not message_entry:
logger.debug(f"No forum message entry found for message {message.id} in group {group_name}")
return
thread_id = str(thread.id)
for entry in message_entry:
# Forum mappings store thread_id as the actual destination thread ID.
if entry.get("thread_id") == thread_id:
continue
await _apply_reaction_to_forum_entry(bot, entry, emoji, operation)
async def _apply_reaction_to_entry(bot, entry: dict, emoji, operation: str):
"""Fetch the linked message represented by entry and apply the requested reaction operation."""
target_channel = await _resolve_channel(bot, entry.get("guild_id"), entry["channel_id"])
if target_channel is None:
logger.warning(f"Target channel {entry['channel_id']} could not be resolved for reaction sync.")
return
try:
if "thread_id" in entry:
parent_message = await target_channel.fetch_message(int(entry["thread_id"]))
target_thread = parent_message.thread
if not target_thread:
logger.warning(f"No thread found for parent message {entry['thread_id']} in channel {target_channel.id}")
return
target_message = await target_thread.fetch_message(int(entry["message_id"]))
else:
target_message = await target_channel.fetch_message(int(entry["message_id"]))
except discord.Forbidden:
logger.error(f"No permission to fetch linked message {entry['message_id']} in channel {target_channel.id}")
return
except discord.NotFound:
logger.warning(f"Linked message {entry['message_id']} not found in channel {target_channel.id}")
return
except discord.HTTPException as exc:
logger.error(f"Failed to fetch linked message {entry['message_id']}: {exc}")
return
await _apply_reaction(bot, target_message, emoji, operation)
async def _apply_reaction_to_forum_entry(bot, entry: dict, emoji, operation: str):
"""Fetch the linked forum-thread message represented by entry and apply the reaction."""
try:
target_thread = bot.get_channel(int(entry["thread_id"])) or await bot.fetch_channel(int(entry["thread_id"]))
except (discord.NotFound, discord.Forbidden):
logger.warning(f"Forum thread {entry['thread_id']} could not be resolved for reaction sync.")
return
except discord.HTTPException as exc:
logger.error(f"Failed to fetch forum thread {entry['thread_id']}: {exc}")
return
try:
target_message = await target_thread.fetch_message(int(entry["message_id"]))
except discord.Forbidden:
logger.error(f"No permission to fetch linked forum message {entry['message_id']} in thread {entry['thread_id']}")
return
except discord.NotFound:
logger.warning(f"Linked forum message {entry['message_id']} not found in thread {entry['thread_id']}")
return
except discord.HTTPException as exc:
logger.error(f"Failed to fetch linked forum message {entry['message_id']}: {exc}")
return
await _apply_reaction(bot, target_message, emoji, operation)
async def _apply_reaction(bot, message: discord.Message, emoji, operation: str):
"""Add or remove the specified reaction on the provided message."""
try:
if operation == "add":
await message.add_reaction(emoji)
elif operation == "remove":
await message.remove_reaction(emoji, bot.user)
except discord.HTTPException as exc:
if operation == "add" and exc.status == 400:
logger.debug(f"Reaction {emoji} already exists on message {message.id}")
elif operation == "remove" and exc.status == 404:
logger.debug(f"Reaction {emoji} from bot not present on message {message.id}")
else:
logger.error(f"Failed to {operation} reaction {emoji} on message {message.id}: {exc}")
async def _resolve_channel(bot, guild_id, channel_id):
"""Resolve a channel by ID using cache first, then API fetch as fallback."""
channel = bot.get_channel(int(channel_id))
if channel:
return channel
guild = bot.get_guild(int(guild_id)) if guild_id else None
if guild:
channel = guild.get_channel(int(channel_id))
if channel:
return channel
try:
return await bot.fetch_channel(int(channel_id))
except (discord.NotFound, discord.Forbidden):
return None
except discord.HTTPException:
logger.error(f"HTTP error while fetching channel {channel_id}")
return None