|
|
@@ -8,7 +8,8 @@ from discord import AuditLogAction, AuditLogEntry, Emoji, Guild, GuildSticker, I
|
|
8
|
8
|
from discord.abc import GuildChannel
|
|
9
|
9
|
from discord.ext import commands
|
|
10
|
10
|
from discord.utils import escape_markdown
|
|
11
|
|
-from typing import List, Optional, Union
|
|
|
11
|
+from typing import List, Optional, Tuple, Union
|
|
|
12
|
+import difflib
|
|
12
|
13
|
import traceback
|
|
13
|
14
|
|
|
14
|
15
|
from config import CONFIG
|
|
|
@@ -451,12 +452,54 @@ class LoggingCog(BaseCog, name='Logging'):
|
|
451
|
452
|
return
|
|
452
|
453
|
if after.author.id == self.bot.user.id:
|
|
453
|
454
|
return
|
|
454
|
|
- if after.content == before.content:
|
|
455
|
|
- # Most likely an embed being updated
|
|
|
455
|
+
|
|
|
456
|
+ content_changed = (after.content != before.content)
|
|
|
457
|
+ attachments_changed = (after.attachments != before.attachments)
|
|
|
458
|
+ embeds_changed = (after.embeds != before.embeds)
|
|
|
459
|
+ embeds_add_only = len(before.embeds or []) == 0 and len(after.embeds or []) > 0
|
|
|
460
|
+
|
|
|
461
|
+ if not content_changed and not attachments_changed and (not embeds_changed or embeds_add_only):
|
|
|
462
|
+ # Most likely an embed being asynchronously populated by server
|
|
456
|
463
|
return
|
|
|
464
|
+ if content_changed:
|
|
|
465
|
+ (before_markdown, after_markdown) = self.__diff(self.__quote_markdown(before.content), \
|
|
|
466
|
+ self.__quote_markdown(after.content))
|
|
|
467
|
+ else:
|
|
|
468
|
+ before_markdown = self.__quote_markdown(before.content)
|
|
|
469
|
+ after_markdown = before_markdown if len(before.content.strip()) == 0 else '> _<content unchanged>_'
|
|
|
470
|
+ if attachments_changed:
|
|
|
471
|
+ if len(before.attachments or []) > 0:
|
|
|
472
|
+ for attachment in before.attachments:
|
|
|
473
|
+ before_markdown += f'\n> * 📎 {attachment.url}'
|
|
|
474
|
+ if attachment not in after.attachments or []:
|
|
|
475
|
+ before_markdown += ' (removed)'
|
|
|
476
|
+ else:
|
|
|
477
|
+ before_markdown += '\n> * _<no attachments>_'
|
|
|
478
|
+ if len(after.attachments or []) > 0:
|
|
|
479
|
+ for attachment in after.attachments:
|
|
|
480
|
+ after_markdown += f'\n> * 📎 {attachment.url}'
|
|
|
481
|
+ if attachment not in before.attachments or []:
|
|
|
482
|
+ after_markdown += ' (added)'
|
|
|
483
|
+ else:
|
|
|
484
|
+ after_markdown += '\n> * _<no attachments>_'
|
|
|
485
|
+ if embeds_changed:
|
|
|
486
|
+ if len(before.embeds or []) > 0:
|
|
|
487
|
+ for embed in before.embeds:
|
|
|
488
|
+ before_markdown += f'\n> * 🔗 {embed.url}'
|
|
|
489
|
+ if embed not in after.embeds or []:
|
|
|
490
|
+ before_markdown += ' (removed)'
|
|
|
491
|
+ else:
|
|
|
492
|
+ before_markdown += '\n> * _<no embeds>_'
|
|
|
493
|
+ if len(after.embeds or []) > 0:
|
|
|
494
|
+ for embed in after.embeds:
|
|
|
495
|
+ after_markdown += f'\n> * 🔗 {embed.url}'
|
|
|
496
|
+ if embed not in before.embeds or []:
|
|
|
497
|
+ after_markdown += ' (added)'
|
|
|
498
|
+ else:
|
|
|
499
|
+ after_markdown += '\n> * _<no embeds>_'
|
|
457
|
500
|
text = f'Message {after.jump_url} edited by {self.__describe_user(after.author)}.\n' + \
|
|
458
|
|
- f'Original markdown:\n{self.__quote_markdown(before.content)}\n' + \
|
|
459
|
|
- f'Updated markdown:\n{self.__quote_markdown(after.content)}'
|
|
|
501
|
+ f'Original:\n{before_markdown}\n' + \
|
|
|
502
|
+ f'Updated:\n{after_markdown}'
|
|
460
|
503
|
bot_message = BotMessage(guild, text, BotMessage.TYPE_LOG, suppress_embeds=True)
|
|
461
|
504
|
await bot_message.update()
|
|
462
|
505
|
|
|
|
@@ -523,6 +566,10 @@ class LoggingCog(BaseCog, name='Logging'):
|
|
523
|
566
|
return
|
|
524
|
567
|
text = f'Message by {self.__describe_user(message.author)} deleted from {message.channel.mention}. ' + \
|
|
525
|
568
|
f'Markdown:\n{self.__quote_markdown(message.content)}'
|
|
|
569
|
+ for attachment in message.attachments or []:
|
|
|
570
|
+ text += f'\n> * 📎 {attachment.url}'
|
|
|
571
|
+ for embed in message.embeds or []:
|
|
|
572
|
+ text += f'\n> * 🔗 {embed.url}'
|
|
526
|
573
|
bot_message = BotMessage(message.guild, text, BotMessage.TYPE_LOG, suppress_embeds=True)
|
|
527
|
574
|
await bot_message.update()
|
|
528
|
575
|
else:
|
|
|
@@ -564,13 +611,17 @@ class LoggingCog(BaseCog, name='Logging'):
|
|
564
|
611
|
text += f' No cached content available for any of them.'
|
|
565
|
612
|
elif uncached_count > 0:
|
|
566
|
613
|
text += f' No cached content available for {uncached_count} of them.'
|
|
567
|
|
- bot_message = BotMessage(guild, text, BotMessage.TYPE_LOG)
|
|
|
614
|
+ bot_message = BotMessage(guild, text, BotMessage.TYPE_LOG, suppress_embeds=True)
|
|
568
|
615
|
await bot_message.update()
|
|
569
|
616
|
|
|
570
|
617
|
for message in payload.cached_messages:
|
|
571
|
618
|
text = f'Message by {self.__describe_user(message.author)} bulk deleted from {message.channel.mention}. ' + \
|
|
572
|
619
|
f'Markdown:\n{self.__quote_markdown(message.content)}'
|
|
573
|
|
- bot_message = BotMessage(guild, text, BotMessage.TYPE_LOG)
|
|
|
620
|
+ for attachment in message.attachments or []:
|
|
|
621
|
+ text += f'\n> * 📎 {attachment.url}'
|
|
|
622
|
+ for embed in message.embeds or []:
|
|
|
623
|
+ text += f'\n> * 🔗 {embed.url}'
|
|
|
624
|
+ bot_message = BotMessage(guild, text, BotMessage.TYPE_LOG, suppress_embeds=True)
|
|
574
|
625
|
await bot_message.update()
|
|
575
|
626
|
|
|
576
|
627
|
# Events - Roles
|
|
|
@@ -665,6 +716,8 @@ class LoggingCog(BaseCog, name='Logging'):
|
|
665
|
716
|
|
|
666
|
717
|
# ------------------------------------------------------------------------
|
|
667
|
718
|
def __quote_markdown(self, s: str) -> str:
|
|
|
719
|
+ if len(s.strip()) == 0:
|
|
|
720
|
+ return '> _<no content>_'
|
|
668
|
721
|
return '> ' + escape_markdown(s).replace('\n', '\n> ')
|
|
669
|
722
|
|
|
670
|
723
|
def __describe_user(self, user: Union[User, Member]) -> str:
|
|
|
@@ -672,3 +725,42 @@ class LoggingCog(BaseCog, name='Logging'):
|
|
672
|
725
|
Standardized markdown describing a user or member.
|
|
673
|
726
|
"""
|
|
674
|
727
|
return f'**{user.name}** ({user.display_name} {user.id})'
|
|
|
728
|
+
|
|
|
729
|
+ def __diff(self, a: str, b: str) -> Tuple[str, str]:
|
|
|
730
|
+ deletion_start = '~~'
|
|
|
731
|
+ deletion_end = '~~'
|
|
|
732
|
+ addition_start = '**'
|
|
|
733
|
+ addition_end = '**'
|
|
|
734
|
+ markdown_a = ''
|
|
|
735
|
+ markdown_b = ''
|
|
|
736
|
+ a_open = False
|
|
|
737
|
+ b_open = False
|
|
|
738
|
+ # FIXME: Handle URLs better. They get mangled.
|
|
|
739
|
+ # URL regex: http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+
|
|
|
740
|
+ for i, s in enumerate(difflib.ndiff(a, b)):
|
|
|
741
|
+ operation = s[0]
|
|
|
742
|
+ content = s[2:]
|
|
|
743
|
+ if operation != '-' and a_open:
|
|
|
744
|
+ markdown_a += deletion_end
|
|
|
745
|
+ a_open = False
|
|
|
746
|
+ if operation != '+' and b_open:
|
|
|
747
|
+ markdown_b += addition_end
|
|
|
748
|
+ b_open = False
|
|
|
749
|
+ if operation == ' ':
|
|
|
750
|
+ markdown_a += content
|
|
|
751
|
+ markdown_b += content
|
|
|
752
|
+ elif operation == '-':
|
|
|
753
|
+ if not a_open:
|
|
|
754
|
+ markdown_a += deletion_start
|
|
|
755
|
+ a_open = True
|
|
|
756
|
+ markdown_a += content
|
|
|
757
|
+ elif operation == '+':
|
|
|
758
|
+ if not b_open:
|
|
|
759
|
+ markdown_b += addition_start
|
|
|
760
|
+ b_open = True
|
|
|
761
|
+ markdown_b += content
|
|
|
762
|
+ if a_open:
|
|
|
763
|
+ markdown_a += deletion_end
|
|
|
764
|
+ if b_open:
|
|
|
765
|
+ markdown_b += addition_end
|
|
|
766
|
+ return (markdown_a, markdown_b)
|