From ba6b64077d2e6f9212b6bb0fb797dd6ea18fac50 Mon Sep 17 00:00:00 2001 From: Matthew Moss <56257224+mahtoid@users.noreply.github.com> Date: Sun, 20 Nov 2022 16:47:16 +0000 Subject: [PATCH] DiscordChatExporterPy 2.5 (#74) * Support for interactions and verified bots (#71) * Addition of a verified bot tag and support for logging interactions * part 1 fix * part 2 fix * part 3 fix * part 4 fix * part 5 fix * part 6 fix * part 7 fix * part 8 fix * part 9 fix * removed not used css, added underline for interation-link on hover * added interaction command icon * renamed icon to make visible it could be command icon * add support for interactions in references, fix parse mode for command names * remove followup symbol appearing twice for unknown reference * unify code, handle reference & interaction as followup as long as they are the same * code cleanup * fix followup icons * fix followup icons part 2 * fix edge case for references containing only a link to an attachment * okay doesn't work this way * fix formating * CSS Changes Co-authored-by: mahtoid * Add Guild ID to Summary (#72) * Add GUILD_ID * Add Guild ID * Add before and after history support * docstrings * Use MessageType for Message construct * Disnake to use 'None' instead of 'Embed.Empty' Co-authored-by: Lukas Dobler <69309597+doluk@users.noreply.github.com> Co-authored-by: deveninabox <44348797+deveninabox@users.noreply.github.com> --- chat_exporter/chat_exporter.py | 57 +++++++++++++ chat_exporter/construct/assets/embed.py | 4 +- chat_exporter/construct/message.py | 64 +++++++++++---- chat_exporter/construct/transcript.py | 15 +++- chat_exporter/ext/discord_utils.py | 1 + chat_exporter/ext/html_generator.py | 2 + chat_exporter/html/base.html | 81 +++++++++++++++---- .../html/message/bot-tag-verified.html | 5 ++ chat_exporter/html/message/interaction.html | 9 +++ chat_exporter/html/message/reference.html | 10 +-- .../html/message/reference_unknown.html | 6 +- chat_exporter/html/message/start.html | 2 +- 12 files changed, 214 insertions(+), 42 deletions(-) create mode 100644 chat_exporter/html/message/bot-tag-verified.html create mode 100644 chat_exporter/html/message/interaction.html diff --git a/chat_exporter/chat_exporter.py b/chat_exporter/chat_exporter.py index 6d5e12c..2b406a7 100644 --- a/chat_exporter/chat_exporter.py +++ b/chat_exporter/chat_exporter.py @@ -1,3 +1,4 @@ +import datetime import io from typing import List, Optional @@ -10,6 +11,15 @@ async def quick_export( guild: Optional[discord.Guild] = None, bot: Optional[discord.Client] = None, ): + """ + Create a quick export of your Discord channel. + This function will produce the transcript and post it back in to your channel. + :param channel: discord.TextChannel + :param guild: (optional) discord.Guild + :param bot: (optional) discord.Client + :return: discord.Message (posted transcript) + """ + if guild: channel.guild = guild @@ -21,6 +31,8 @@ async def quick_export( pytz_timezone="UTC", military_time=True, fancy_times=True, + before=None, + after=None, support_dev=True, bot=bot, ).export() @@ -46,8 +58,24 @@ async def export( bot: Optional[discord.Client] = None, military_time: Optional[bool] = True, fancy_times: Optional[bool] = True, + before: Optional[datetime.datetime] = None, + after: Optional[datetime.datetime] = None, support_dev: Optional[bool] = True, ): + """ + Create a customised transcript of your Discord channel. + This function will return the transcript which you can then turn in to a file to post wherever. + :param channel: discord.TextChannel - channel to Export + :param limit: (optional) integer - limit of messages to capture + :param tz_info: (optional) TZ Database Name - set the timezone of your transcript + :param guild: (optional) discord.Guild - solution for edpy + :param bot: (optional) discord.Client - set getting member role colour + :param military_time: (optional) boolean - set military time (24hour clock) + :param fancy_times: (optional) boolean - set javascript around time display + :param before: (optional) datetime.datetime - allows before time for history + :param after: (optional) datetime.datetime - allows after time for history + :return: string - transcript file make up + """ if guild: channel.guild = guild @@ -59,6 +87,8 @@ async def export( pytz_timezone=tz_info, military_time=military_time, fancy_times=fancy_times, + before=before, + after=after, support_dev=support_dev, bot=bot, ).export() @@ -75,6 +105,18 @@ async def raw_export( fancy_times: Optional[bool] = True, support_dev: Optional[bool] = True, ): + """ + Create a customised transcript with your own captured Discord messages + This function will return the transcript which you can then turn in to a file to post wherever. + :param channel: discord.TextChannel - channel to Export + :param messages: List[discord.Message] - list of Discord messages to export + :param tz_info: (optional) TZ Database Name - set the timezone of your transcript + :param guild: (optional) discord.Guild - solution for edpy + :param bot: (optional) discord.Client - set getting member role colour + :param military_time: (optional) boolean - set military time (24hour clock) + :param fancy_times: (optional) boolean - set javascript around time display + :return: string - transcript file make up + """ if guild: channel.guild = guild @@ -86,6 +128,8 @@ async def raw_export( pytz_timezone=tz_info, military_time=military_time, fancy_times=fancy_times, + before=None, + after=None, support_dev=support_dev, bot=bot, ).export() @@ -96,6 +140,13 @@ async def quick_link( channel: discord.TextChannel, message: discord.Message ): + """ + Create a quick link for your transcript file. + This function will return an embed with a link to view the transcript online. + :param channel: discord.TextChannel + :param message: discord.Message + :return: discord.Message (posted link) + """ embed = discord.Embed( title="Transcript Link", description=( @@ -110,4 +161,10 @@ async def quick_link( async def link( message: discord.Message ): + """ + Returns a link which you can use to display in a message. + This function will return a string of the link. + :param message: discord.Message + :return: string (link: https://mahto.id/chat-exporter?url=ATTACHMENT_URL) + """ return "https://mahto.id/chat-exporter?url=" + message.attachments[0].url diff --git a/chat_exporter/construct/assets/embed.py b/chat_exporter/construct/assets/embed.py index edab542..d862e35 100644 --- a/chat_exporter/construct/assets/embed.py +++ b/chat_exporter/construct/assets/embed.py @@ -19,9 +19,11 @@ PARSE_MODE_SPECIAL_EMBED, ) +modules_which_use_none = ["nextcord", "disnake"] + def _gather_checker(): - if hasattr(discord.Embed, "Empty") and discord.module != "nextcord": + if discord.module not in modules_which_use_none and hasattr(discord.Embed, "Empty"): return discord.Embed.Empty return None diff --git a/chat_exporter/construct/message.py b/chat_exporter/construct/message.py index deb9b69..e675b8b 100644 --- a/chat_exporter/construct/message.py +++ b/chat_exporter/construct/message.py @@ -1,5 +1,5 @@ import html -from typing import List, Optional +from typing import List, Optional, Union from pytz import timezone from datetime import timedelta @@ -11,12 +11,14 @@ from chat_exporter.ext.html_generator import ( fill_out, bot_tag, + bot_tag_verified, message_body, message_pin, message_thread, message_content, message_reference, message_reference_unknown, + message_interaction, img_attachment, start_message, end_message, @@ -27,7 +29,11 @@ def _gather_user_bot(author: discord.Member): - return bot_tag if author.bot else "" + if author.bot and author.public_flags.verified_bot: + return bot_tag_verified + elif author.bot: + return bot_tag + return "" def _set_edit_at(message_edited_at): @@ -69,9 +75,9 @@ def __init__( async def construct_message( self, ) -> (str, dict): - if "pins_add" in self.message.type: + if discord.MessageType.pins_add == self.message.type: await self.build_pin() - elif "thread_created" in self.message.type: + elif discord.MessageType.thread_created == self.message.type: await self.build_thread() else: await self.build_message() @@ -80,6 +86,7 @@ async def construct_message( async def build_message(self): await self.build_content() await self.build_reference() + await self.build_interaction() await self.build_sticker() await self.build_assets() await self.build_message_template() @@ -146,10 +153,16 @@ async def build_reference(self): is_bot = _gather_user_bot(message.author) user_colour = await self._gather_user_colour(message.author) - if not message.content: + if not message.content and not message.interaction: message.content = "Click to see attachment" + elif not message.content and message.interaction: + message.content = "Click to see command" - attachment_icon = DiscordUtils.reference_attachment_icon if message.embeds or message.attachments else "" + icon = "" + if not message.interaction and (message.embeds or message.attachments): + icon = DiscordUtils.reference_attachment_icon + elif message.interaction: + icon = DiscordUtils.interaction_command_icon _, message_edited_at = self.set_time(message) @@ -165,8 +178,30 @@ async def build_reference(self): ("USER_COLOUR", user_colour, PARSE_MODE_NONE), ("CONTENT", message.content, PARSE_MODE_REFERENCE), ("EDIT", message_edited_at, PARSE_MODE_NONE), - ("ATTACHMENT_ICON", attachment_icon, PARSE_MODE_NONE), - ("MESSAGE_ID", str(self.message.reference.message_id), PARSE_MODE_NONE) + ("ICON", icon, PARSE_MODE_NONE), + ("USER_ID", str(message.author.id), PARSE_MODE_NONE), + ("MESSAGE_ID", str(self.message.reference.message_id), PARSE_MODE_NONE), + ]) + + async def build_interaction(self): + if not self.message.interaction: + self.message.interaction = "" + return + + user: Union[discord.Member, discord.User] = self.message.interaction.user + is_bot = _gather_user_bot(user) + user_colour = await self._gather_user_colour(user) + avatar_url = user.display_avatar if user.display_avatar else DiscordUtils.default_avatar + self.message.interaction = await fill_out(self.guild, message_interaction, [ + ("AVATAR_URL", str(avatar_url), PARSE_MODE_NONE), + ("BOT_TAG", is_bot, PARSE_MODE_NONE), + ("NAME_TAG", "%s#%s" % (user.name, user.discriminator), PARSE_MODE_NONE), + ("NAME", str(html.escape(user.display_name))), + ("USER_COLOUR", user_colour, PARSE_MODE_NONE), + ("FILLER", "used ", PARSE_MODE_NONE), + ("COMMAND", "/" + self.message.interaction.name, PARSE_MODE_NONE), + ("USER_ID", str(user.id), PARSE_MODE_NONE), + ("INTERACTION_ID", str(self.message.interaction.id), PARSE_MODE_NONE), ]) async def build_sticker(self): @@ -223,7 +258,7 @@ async def build_message_template(self): def _generate_message_divider_check(self): return bool( - self.previous_message is None or self.message.reference != "" or + self.previous_message is None or self.message.reference != "" or self.message.interaction != "" or self.previous_message.author.id != self.message.author.id or self.message.webhook_id is not None or self.message.created_at > (self.previous_message.created_at + timedelta(minutes=4)) ) @@ -236,12 +271,12 @@ async def generate_message_divider(self, channel_audit=False): if channel_audit: return - reference_symbol = "" + followup_symbol = "" is_bot = _gather_user_bot(self.message.author) avatar_url = self.message.author.display_avatar if self.message.author.display_avatar else DiscordUtils.default_avatar - if self.message.reference != "": - reference_symbol = "
" + if self.message.reference != "" or self.message.interaction: + followup_symbol = "
" time = self.message.created_at if not self.message.created_at.tzinfo: @@ -250,8 +285,9 @@ async def generate_message_divider(self, channel_audit=False): default_timestamp = time.astimezone(timezone(self.pytz_timezone)).strftime("%d-%m-%Y %H:%M") self.message_html += await fill_out(self.guild, start_message, [ - ("REFERENCE_SYMBOL", reference_symbol, PARSE_MODE_NONE), - ("REFERENCE", self.message.reference, PARSE_MODE_NONE), + ("REFERENCE_SYMBOL", followup_symbol, PARSE_MODE_NONE), + ("REFERENCE", self.message.reference if self.message.reference else self.message.interaction, + PARSE_MODE_NONE), ("AVATAR_URL", str(avatar_url), PARSE_MODE_NONE), ("NAME_TAG", "%s#%s" % (self.message.author.name, self.message.author.discriminator), PARSE_MODE_NONE), ("USER_ID", str(self.message.author.id)), diff --git a/chat_exporter/construct/transcript.py b/chat_exporter/construct/transcript.py index 1540ac8..d08d013 100644 --- a/chat_exporter/construct/transcript.py +++ b/chat_exporter/construct/transcript.py @@ -30,6 +30,8 @@ def __init__( pytz_timezone, military_time: bool, fancy_times: bool, + before: Optional[datetime.datetime], + after: Optional[datetime.datetime], support_dev: bool, bot: Optional[discord.Client], ): @@ -38,6 +40,8 @@ def __init__( self.limit = int(limit) if limit else None self.military_time = military_time self.fancy_times = fancy_times + self.before = before + self.after = after self.support_dev = support_dev self.pytz_timezone = pytz_timezone @@ -127,6 +131,7 @@ async def export_transcript(self, message_html: str, meta_data: str): self.html = await fill_out(self.channel.guild, total, [ ("SERVER_NAME", f"{guild_name}"), + ("GUILD_ID", str(self.channel.guild.id), PARSE_MODE_NONE), ("SERVER_AVATAR_URL", str(guild_icon), PARSE_MODE_NONE), ("CHANNEL_NAME", f"{self.channel.name}"), ("MESSAGE_COUNT", str(len(self.messages))), @@ -146,8 +151,14 @@ async def export_transcript(self, message_html: str, meta_data: str): class Transcript(TranscriptDAO): async def export(self): if not self.messages: - self.messages = [message async for message in self.channel.history(limit=self.limit)] - self.messages.reverse() + self.messages = [message async for message in self.channel.history( + limit=self.limit, + before=self.before, + after=self.after, + )] + + if not self.after: + self.messages.reverse() try: return await super().build_transcript() diff --git a/chat_exporter/ext/discord_utils.py b/chat_exporter/ext/discord_utils.py index ee9f433..2e4e278 100644 --- a/chat_exporter/ext/discord_utils.py +++ b/chat_exporter/ext/discord_utils.py @@ -12,4 +12,5 @@ class DiscordUtils: file_attachment_unknown: str = 'https://cdn.jsdelivr.net/gh/mahtoid/DiscordUtils@master/discord-unknown.svg' button_external_link: str = '' reference_attachment_icon: str = '' + interaction_command_icon: str = '' interaction_dropdown_icon: str = '' \ No newline at end of file diff --git a/chat_exporter/ext/html_generator.py b/chat_exporter/ext/html_generator.py index d30e148..98a4167 100644 --- a/chat_exporter/ext/html_generator.py +++ b/chat_exporter/ext/html_generator.py @@ -49,8 +49,10 @@ def read_file(filename): # MESSAGES start_message = read_file(dir_path + "/html/message/start.html") bot_tag = read_file(dir_path + "/html/message/bot-tag.html") +bot_tag_verified = read_file(dir_path + "/html/message/bot-tag-verified.html") message_content = read_file(dir_path + "/html/message/content.html") message_reference = read_file(dir_path + "/html/message/reference.html") +message_interaction = read_file(dir_path + "/html/message/interaction.html") message_pin = read_file(dir_path + "/html/message/pin.html") message_thread = read_file(dir_path + "/html/message/thread.html") message_reference_unknown = read_file(dir_path + "/html/message/reference_unknown.html") diff --git a/chat_exporter/html/base.html b/chat_exporter/html/base.html index 6c4be8f..42c0439 100644 --- a/chat_exporter/html/base.html +++ b/chat_exporter/html/base.html @@ -11,7 +11,7 @@ {{SERVER_NAME}} - {{CHANNEL_NAME}} - +