|
|
@@ -5,6 +5,7 @@ changes, and mods can perform actions on the message via emoji reactions.
|
|
5
|
5
|
from typing import Any, Optional, Union
|
|
6
|
6
|
|
|
7
|
7
|
from datetime import datetime
|
|
|
8
|
+
|
|
8
|
9
|
from discord import Guild, Message, PartialEmoji, TextChannel
|
|
9
|
10
|
|
|
10
|
11
|
from config import CONFIG
|
|
|
@@ -142,9 +143,9 @@ class BotMessage:
|
|
142
|
143
|
self.context: Optional[Any] = context
|
|
143
|
144
|
self.quote: Optional[str] = None
|
|
144
|
145
|
self.source_cog = None # Set by `BaseCog.post_message()`
|
|
145
|
|
- self.__posted_text: str = None # last text posted, to test for changes
|
|
|
146
|
+ self.__posted_text: list[str] = [] # last text posted, to test for changes
|
|
146
|
147
|
self.__posted_emoji: set[str] = set() # last emoji list posted
|
|
147
|
|
- self.__message: Optional[Message] = None # set once the message has been posted
|
|
|
148
|
+ self.__messages: list[Message] = [] # set once the message has been posted
|
|
148
|
149
|
self.__reply_to: Optional[Message] = reply_to
|
|
149
|
150
|
self.__suppress_embeds = suppress_embeds
|
|
150
|
151
|
self.__reactions: list[BotMessageReaction] = []
|
|
|
@@ -155,18 +156,19 @@ class BotMessage:
|
|
155
|
156
|
continue returning False even after calling BaseCog.post_message if
|
|
156
|
157
|
the guild has no configured warning channel.
|
|
157
|
158
|
"""
|
|
158
|
|
- return self.__message is not None
|
|
|
159
|
+ return len(self.__messages) > 0
|
|
159
|
160
|
|
|
160
|
|
- def message_id(self) -> Optional[int]:
|
|
161
|
|
- 'Returns the Message id or None if not sent.'
|
|
162
|
|
- return self.__message.id if self.__message else None
|
|
|
161
|
+ def message_ids(self) -> list[int]:
|
|
|
162
|
+ """Returns the ids of all actual Messages sent. One bot message may be
|
|
|
163
|
+ broken into multiple Discord messages."""
|
|
|
164
|
+ return [ m.id for m in self.__messages ]
|
|
163
|
165
|
|
|
164
|
166
|
def message_sent_at(self) -> Optional[datetime]:
|
|
165
|
|
- 'Returns when the message was sent or None if not sent.'
|
|
166
|
|
- return self.__message.created_at if self.__message else None
|
|
|
167
|
+ """Returns when the message was sent or None if not sent."""
|
|
|
168
|
+ return self.__messages[0].created_at if len(self.__messages) > 0 else None
|
|
167
|
169
|
|
|
168
|
170
|
def has_reactions(self) -> bool:
|
|
169
|
|
- 'Whether this message has any reactions defined.'
|
|
|
171
|
+ """Whether this message has any reactions defined."""
|
|
170
|
172
|
return len(self.__reactions) > 0
|
|
171
|
173
|
|
|
172
|
174
|
async def set_text(self, new_text: str) -> None:
|
|
|
@@ -222,7 +224,7 @@ class BotMessage:
|
|
222
|
224
|
"""
|
|
223
|
225
|
for i, existing in enumerate(self.__reactions):
|
|
224
|
226
|
if (isinstance(reaction_or_emoji, str) and existing.emoji == reaction_or_emoji) or \
|
|
225
|
|
- (isinstance(reaction_or_emoji, BotMessageReaction) and \
|
|
|
227
|
+ (isinstance(reaction_or_emoji, BotMessageReaction) and
|
|
226
|
228
|
existing.emoji == reaction_or_emoji.emoji):
|
|
227
|
229
|
self.__reactions.pop(i)
|
|
228
|
230
|
await self.update_if_sent()
|
|
|
@@ -246,7 +248,7 @@ class BotMessage:
|
|
246
|
248
|
the guild, otherwise does nothing. Does not need to be called by
|
|
247
|
249
|
BaseCog subclasses.
|
|
248
|
250
|
"""
|
|
249
|
|
- if self.__message:
|
|
|
251
|
+ if len(self.__messages) > 0:
|
|
250
|
252
|
await self.update()
|
|
251
|
253
|
|
|
252
|
254
|
async def update(self) -> None:
|
|
|
@@ -254,49 +256,66 @@ class BotMessage:
|
|
254
|
256
|
Sends or updates an already sent message based on BotMessage state.
|
|
255
|
257
|
Does not need to be called by BaseCog subclasses.
|
|
256
|
258
|
"""
|
|
257
|
|
- content: str = self.__formatted_message()
|
|
258
|
|
- if self.__message:
|
|
259
|
|
- if content != self.__posted_text:
|
|
260
|
|
- await self.__message.edit(content=content)
|
|
261
|
|
- self.__posted_text = content
|
|
262
|
|
- else:
|
|
263
|
|
- if self.__reply_to:
|
|
264
|
|
- self.__message = await self.__reply_to.reply(content=content, mention_author=False)
|
|
265
|
|
- self.__posted_text = content
|
|
266
|
|
- else:
|
|
267
|
|
- channel_id = Storage.get_config_value(self.guild, ConfigKey.WARNING_CHANNEL_ID)
|
|
268
|
|
- if channel_id is None:
|
|
269
|
|
- bot_log(self.guild, \
|
|
270
|
|
- type(self.source_cog) if self.source_cog else None, \
|
|
271
|
|
- '\u0007No warning channel set! No warning issued.')
|
|
272
|
|
- return
|
|
273
|
|
- channel: TextChannel = self.guild.get_channel(channel_id)
|
|
274
|
|
- if channel is None:
|
|
275
|
|
- channel = await self.guild.fetch_channel(channel_id)
|
|
276
|
|
- if channel is None:
|
|
277
|
|
- bot_log(self.guild, \
|
|
278
|
|
- type(self.source_cog) if self.source_cog else None, \
|
|
279
|
|
- f'\u0007Configured warning channel does not exist for guild {self.guild.name} ({self.guild.id})!')
|
|
280
|
|
- return
|
|
281
|
|
- self.__message = await channel.send(content=content, suppress_embeds=self.__suppress_embeds)
|
|
282
|
|
- self.__posted_text = content
|
|
|
259
|
+ message_bodies: list[str] = self.__formatted_message()
|
|
|
260
|
+ if len(self.__messages) > 0:
|
|
|
261
|
+ if message_bodies != self.__posted_text:
|
|
|
262
|
+ while len(self.__messages) > len(message_bodies):
|
|
|
263
|
+ last_message = self.__messages.pop(-1)
|
|
|
264
|
+ await last_message.delete()
|
|
|
265
|
+ del Storage.get_bot_messages(self.guild)[last_message.id]
|
|
|
266
|
+ for i in range(min(len(message_bodies), len(self.__messages))):
|
|
|
267
|
+ await self.__messages[i].edit(content=message_bodies[i])
|
|
|
268
|
+ while len(self.__messages) < len(message_bodies):
|
|
|
269
|
+ body = message_bodies[len(self.__messages)]
|
|
|
270
|
+ message = await self.__messages[0].channel.send(content=body, suppress_embeds=self.__suppress_embeds)
|
|
|
271
|
+ Storage.get_bot_messages(self.guild)[message.id] = self
|
|
|
272
|
+ self.__messages.append(message)
|
|
|
273
|
+ self.__posted_text = message_bodies
|
|
|
274
|
+ else: # No messages posted yet
|
|
|
275
|
+ channel: Optional[TextChannel] = None
|
|
|
276
|
+ for index, body in enumerate(message_bodies):
|
|
|
277
|
+ if index == 0 and self.__reply_to:
|
|
|
278
|
+ message = await self.__reply_to.reply(content=body, mention_author=False)
|
|
|
279
|
+ Storage.get_bot_messages(self.guild)[message.id] = self
|
|
|
280
|
+ self.__messages.append(message)
|
|
|
281
|
+ channel = self.__reply_to.channel
|
|
|
282
|
+ else:
|
|
|
283
|
+ if channel is None:
|
|
|
284
|
+ channel_id = Storage.get_config_value(self.guild, ConfigKey.WARNING_CHANNEL_ID)
|
|
|
285
|
+ if channel_id is None:
|
|
|
286
|
+ bot_log(self.guild,
|
|
|
287
|
+ type(self.source_cog) if self.source_cog else None,
|
|
|
288
|
+ '\u0007No warning channel set! No warning issued.')
|
|
|
289
|
+ return
|
|
|
290
|
+ channel: TextChannel = self.guild.get_channel(channel_id) or await self.guild.fetch_channel(channel_id)
|
|
|
291
|
+ if channel is None:
|
|
|
292
|
+ bot_log(self.guild,
|
|
|
293
|
+ type(self.source_cog) if self.source_cog else None,
|
|
|
294
|
+ f'\u0007Configured warning channel does not exist for guild {self.guild.name} ({self.guild.id})!')
|
|
|
295
|
+ return
|
|
|
296
|
+ message = await channel.send(content=body, suppress_embeds=self.__suppress_embeds)
|
|
|
297
|
+ Storage.get_bot_messages(self.guild)[message.id] = self
|
|
|
298
|
+ self.__messages.append(message)
|
|
|
299
|
+ self.__posted_text = message_bodies
|
|
|
300
|
+
|
|
283
|
301
|
emoji_to_remove = self.__posted_emoji.copy()
|
|
284
|
302
|
for reaction in self.__reactions:
|
|
285
|
303
|
if reaction.is_enabled:
|
|
286
|
304
|
if reaction.emoji not in self.__posted_emoji:
|
|
287
|
|
- await self.__message.add_reaction(reaction.emoji)
|
|
|
305
|
+ await self.__messages[-1].add_reaction(reaction.emoji)
|
|
288
|
306
|
self.__posted_emoji.add(reaction.emoji)
|
|
289
|
307
|
if reaction.emoji in emoji_to_remove:
|
|
290
|
308
|
emoji_to_remove.remove(reaction.emoji)
|
|
291
|
309
|
for emoji in emoji_to_remove:
|
|
292
|
|
- await self.__message.clear_reaction(emoji)
|
|
|
310
|
+ await self.__messages[-1].clear_reaction(emoji)
|
|
293
|
311
|
if emoji in self.__posted_emoji:
|
|
294
|
312
|
self.__posted_emoji.remove(emoji)
|
|
295
|
313
|
|
|
296
|
|
- def __formatted_message(self) -> str:
|
|
|
314
|
+ def __formatted_message(self) -> list[str]:
|
|
297
|
315
|
"""
|
|
298
|
|
- Composes the entire message markdown from components. Includes the main
|
|
299
|
|
- message, quoted text, summary of available reactions, etc.
|
|
|
316
|
+ Composes the entire message Markdown from components. Includes the main
|
|
|
317
|
+ message, quoted text, summary of available reactions, etc. Returned as
|
|
|
318
|
+ array of message bodies small enough to fit in a Discord message.
|
|
300
|
319
|
"""
|
|
301
|
320
|
s: str = ''
|
|
302
|
321
|
|
|
|
@@ -328,4 +347,33 @@ class BotMessage:
|
|
328
|
347
|
else:
|
|
329
|
348
|
s += f'\n {reaction.description}'
|
|
330
|
349
|
|
|
331
|
|
- return s
|
|
|
350
|
+ # API complains if *request* is >2000. Unsure how much of that payload is overhead, so will define a max length
|
|
|
351
|
+ # conservatively of 85-90% of that.
|
|
|
352
|
+ ideal_message_length = 1700
|
|
|
353
|
+ max_message_length = 1800
|
|
|
354
|
+ # First time cutting up message
|
|
|
355
|
+ bodies = [s]
|
|
|
356
|
+ while len(bodies[-1]) > max_message_length:
|
|
|
357
|
+ # Try to cut at last newline before length limit, otherwise last space, otherwise hard cut at limit
|
|
|
358
|
+ last_body = bodies.pop(-1)
|
|
|
359
|
+ cut_before = ideal_message_length
|
|
|
360
|
+ cut_after = ideal_message_length
|
|
|
361
|
+ last_newline_index = last_body.rfind('\n', max_message_length // 2, max_message_length)
|
|
|
362
|
+ if last_newline_index >= 0:
|
|
|
363
|
+ cut_before = last_newline_index
|
|
|
364
|
+ cut_after = last_newline_index + 1
|
|
|
365
|
+ else:
|
|
|
366
|
+ last_space_index = last_body.rfind(' ', max_message_length // 2, max_message_length)
|
|
|
367
|
+ if last_space_index >= 0:
|
|
|
368
|
+ cut_before = last_space_index
|
|
|
369
|
+ cut_after = last_space_index + 1
|
|
|
370
|
+ body = last_body[:cut_before].strip()
|
|
|
371
|
+ remainder = last_body[cut_after:].strip()
|
|
|
372
|
+ while body.endswith('\n>'):
|
|
|
373
|
+ body = body[:-2]
|
|
|
374
|
+ while remainder.endswith('\n>'):
|
|
|
375
|
+ remainder = remainder[:-2]
|
|
|
376
|
+ bodies.append(body)
|
|
|
377
|
+ bodies.append(remainder)
|
|
|
378
|
+
|
|
|
379
|
+ return bodies
|