From 21d8cfc8b5b02bff91ee731f273d2ec375adfb51 Mon Sep 17 00:00:00 2001 From: Matthew Moss <56257224+mahtoid@users.noreply.github.com> Date: Tue, 15 Feb 2022 14:19:03 +0000 Subject: [PATCH] DiscordChatExporterPy 2.0 (#28) * reconstruct * README * README * adjust guild arg * README * Removing discord.py from setup and minors * README update * README update * Support for forks which change namespace * Solved edited messages bug and custom limit type * Solved message reference bugs * Solved emoji in button converting twice * Support for Menu Interactions * Fix dropdown icon alignment * Add support for icons in selectmenus * Emoji exception for SSLError * Revert "Emoji exception for SSLError" This reverts commit 5ac06d7c5088bdbc802edfe02b2f8be33937a884. * Display disabled components as disabled * Removing w3 xmlns reference on msg references * Updating README * Attempt conflict resolve #2 * Versioning --- MANIFEST.in | 2 +- README.rst | 50 +- chat_exporter/__init__.py | 8 +- chat_exporter/build_components.py | 54 - chat_exporter/build_html.py | 87 -- chat_exporter/chat_exporter.py | 479 +-------- chat_exporter/chat_exporter_html/base.html | 828 ---------------- .../__init__.py | 0 chat_exporter/construct/assets/__init__.py | 11 + .../assets/attachment.py} | 19 +- chat_exporter/construct/assets/component.py | 100 ++ .../assets/embed.py} | 98 +- .../assets/reaction.py} | 6 +- chat_exporter/construct/message.py | 289 ++++++ chat_exporter/construct/transcript.py | 62 ++ chat_exporter/ext/__init__.py | 0 chat_exporter/{ => ext}/cache.py | 0 chat_exporter/ext/discord_import.py | 7 + chat_exporter/ext/discord_utils.py | 14 + chat_exporter/{ => ext}/emoji_convert.py | 2 +- chat_exporter/ext/html_generator.py | 90 ++ chat_exporter/html/__init__.py | 0 .../attachment/audio.html | 0 .../attachment/image.html | 0 .../attachment/message.html | 0 .../attachment/video.html | 0 chat_exporter/html/base.html | 929 ++++++++++++++++++ .../component/component_button.html | 2 +- .../html/component/component_menu.html | 6 + .../component/component_menu_options.html | 5 + .../component_menu_options_emoji.html | 12 + .../embed/author.html | 0 .../embed/author_icon.html | 0 .../embed/body.html | 0 .../embed/description.html | 0 .../embed/field-inline.html | 0 .../embed/field.html | 0 .../embed/footer.html | 0 .../embed/footer_image.html | 0 .../embed/image.html | 0 .../embed/thumbnail.html | 0 .../embed/title.html | 0 .../message/bot-tag.html | 0 .../message/content.html | 0 .../message/end.html | 0 .../message/message.html | 0 .../message/pin.html | 0 .../message/reference.html | 2 +- .../message/reference_unknown.html | 2 +- .../message/start.html | 0 .../message/thread.html | 0 .../reaction/custom_emoji.html | 0 .../reaction/emoji.html | 0 chat_exporter/parse/__init__.py | 0 .../{parse_markdown.py => parse/markdown.py} | 2 +- .../{parse_mention.py => parse/mention.py} | 13 +- setup.py | 6 +- 57 files changed, 1679 insertions(+), 1506 deletions(-) delete mode 100644 chat_exporter/build_components.py delete mode 100644 chat_exporter/build_html.py delete mode 100644 chat_exporter/chat_exporter_html/base.html rename chat_exporter/{chat_exporter_html => construct}/__init__.py (100%) create mode 100644 chat_exporter/construct/assets/__init__.py rename chat_exporter/{build_attachments.py => construct/assets/attachment.py} (82%) create mode 100644 chat_exporter/construct/assets/component.py rename chat_exporter/{build_embed.py => construct/assets/embed.py} (61%) rename chat_exporter/{build_reaction.py => construct/assets/reaction.py} (88%) create mode 100644 chat_exporter/construct/message.py create mode 100644 chat_exporter/construct/transcript.py create mode 100644 chat_exporter/ext/__init__.py rename chat_exporter/{ => ext}/cache.py (100%) create mode 100644 chat_exporter/ext/discord_import.py create mode 100644 chat_exporter/ext/discord_utils.py rename chat_exporter/{ => ext}/emoji_convert.py (98%) create mode 100644 chat_exporter/ext/html_generator.py create mode 100644 chat_exporter/html/__init__.py rename chat_exporter/{chat_exporter_html => html}/attachment/audio.html (100%) rename chat_exporter/{chat_exporter_html => html}/attachment/image.html (100%) rename chat_exporter/{chat_exporter_html => html}/attachment/message.html (100%) rename chat_exporter/{chat_exporter_html => html}/attachment/video.html (100%) create mode 100644 chat_exporter/html/base.html rename chat_exporter/{chat_exporter_html => html}/component/component_button.html (61%) create mode 100644 chat_exporter/html/component/component_menu.html create mode 100644 chat_exporter/html/component/component_menu_options.html create mode 100644 chat_exporter/html/component/component_menu_options_emoji.html rename chat_exporter/{chat_exporter_html => html}/embed/author.html (100%) rename chat_exporter/{chat_exporter_html => html}/embed/author_icon.html (100%) rename chat_exporter/{chat_exporter_html => html}/embed/body.html (100%) rename chat_exporter/{chat_exporter_html => html}/embed/description.html (100%) rename chat_exporter/{chat_exporter_html => html}/embed/field-inline.html (100%) rename chat_exporter/{chat_exporter_html => html}/embed/field.html (100%) rename chat_exporter/{chat_exporter_html => html}/embed/footer.html (100%) rename chat_exporter/{chat_exporter_html => html}/embed/footer_image.html (100%) rename chat_exporter/{chat_exporter_html => html}/embed/image.html (100%) rename chat_exporter/{chat_exporter_html => html}/embed/thumbnail.html (100%) rename chat_exporter/{chat_exporter_html => html}/embed/title.html (100%) rename chat_exporter/{chat_exporter_html => html}/message/bot-tag.html (100%) rename chat_exporter/{chat_exporter_html => html}/message/content.html (100%) rename chat_exporter/{chat_exporter_html => html}/message/end.html (100%) rename chat_exporter/{chat_exporter_html => html}/message/message.html (100%) rename chat_exporter/{chat_exporter_html => html}/message/pin.html (100%) rename chat_exporter/{chat_exporter_html => html}/message/reference.html (85%) rename chat_exporter/{chat_exporter_html => html}/message/reference_unknown.html (62%) rename chat_exporter/{chat_exporter_html => html}/message/start.html (100%) rename chat_exporter/{chat_exporter_html => html}/message/thread.html (100%) rename chat_exporter/{chat_exporter_html => html}/reaction/custom_emoji.html (100%) rename chat_exporter/{chat_exporter_html => html}/reaction/emoji.html (100%) create mode 100644 chat_exporter/parse/__init__.py rename chat_exporter/{parse_markdown.py => parse/markdown.py} (99%) rename chat_exporter/{parse_mention.py => parse/mention.py} (95%) diff --git a/MANIFEST.in b/MANIFEST.in index cf4ab05..db2114c 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,4 +1,4 @@ include README.rst include LICENSE -recursive-include chat_exporter/chat_exporter_html *.html +recursive-include chat_exporter/html *.html recursive-include *.py \ No newline at end of file diff --git a/README.rst b/README.rst index 5f2bb2e..82adc12 100644 --- a/README.rst +++ b/README.rst @@ -9,7 +9,7 @@ DiscordChatExporterPy .. |language| image:: https://img.shields.io/github/languages/top/mahtoid/discordchatexporterpy -DiscordChatExporterPy is a Python plugin for your discord.py bot, allowing you to export a discord channels history within a guild. +DiscordChatExporterPy is a Python lib for your discord.py (or forks) bot, allowing you to export Discord channel history in to a HTML file. Installing ---------- @@ -25,6 +25,8 @@ To install the repository, run the command: git clone https://github.com/mahtoid/DiscordChatExporterPy +**NOTE: If you are using discord.py 1.7.3, please use chat-exporter v1.7.3** + Usage ----- **Basic Usage** @@ -44,18 +46,15 @@ Usage @bot.event async def on_ready(): print("Live: " + bot.user.name) - chat_exporter.init_exporter(bot) @bot.command() - async def save(ctx): - await chat_exporter.quick_export(channel, guild) + async def save(ctx: commands.Context): + await chat_exporter.quick_export(ctx.channel) if __name__ == "__main__": bot.run("BOT_TOKEN_HERE") -*Optional: If you want the transcript to display Members (Role) Colours then enable the Members Intent. -Passing 'guild' is optional and is only necessary when using enhanced-dpy.* **Customisable Usage** @@ -66,20 +65,27 @@ Passing 'guild' is optional and is only necessary when using enhanced-dpy.* ... @bot.command() - async def save(ctx, limit: int, tz_info): - transcript = await chat_exporter.export(ctx.channel, guild, limit, tz_info) + async def save(ctx: commands.Context, limit: int, tz_info): + transcript = await chat_exporter.export( + ctx.channel, + limit=limit, + tz_info=tz_info, + ) if transcript is None: return - transcript_file = discord.File(io.BytesIO(transcript.encode()), - filename=f"transcript-{ctx.channel.name}.html") + transcript_file = discord.File( + io.BytesIO(transcript.encode()), + filename=f"transcript-{ctx.channel.name}.html", + ) await ctx.send(file=transcript_file) -*Optional: limit and tz_info are both optional, but can be used to limit the amount of messages transcribed or set a 'local' (pytz) timezone for -the bot to transcribe message times to. Passing 'guild' is optional and is only necessary when using enhanced-dpy.* - +| *Optional: limit and tz_info are both optional.* +| *'limit' is to set the amount of messages to acquire from the history.* +| *'tz_info' is to set your own custom timezone.* +| **Raw Usage** .. code:: py @@ -89,21 +95,27 @@ the bot to transcribe message times to. Passing 'guild' is optional and is only ... @bot.command() - async def purge(ctx, tz_info): + async def purge(ctx: commands.Context, tz_info): deleted_messages = await ctx.channel.purge() - transcript = await chat_exporter.raw_export(channel, guild, deleted_messages, tz_info) + transcript = await chat_exporter.raw_export( + ctx.channel, + messages=deleted_messages, + tz_info=tz_info, + ) if transcript is None: return - transcript_file = discord.File(io.BytesIO(transcript.encode()), - filename=f"transcript-{ctx.channel.name}.html") + transcript_file = discord.File( + io.BytesIO(transcript.encode()), + filename=f"transcript-{ctx.channel.name}.html", + ) await ctx.send(file=transcript_file) -*Optional: tz_info is optional, but can be used to set a 'local' (pytz) timezone for the bot to transcribe message times to. -Passing 'guild' is optional and is only necessary when using enhanced-dpy.* +| *Optional: tz_info is optional.* +| *'tz_info' is to set your own custom timezone.* Screenshots ----------- diff --git a/chat_exporter/__init__.py b/chat_exporter/__init__.py index e2dd8fa..37f36e0 100644 --- a/chat_exporter/__init__.py +++ b/chat_exporter/__init__.py @@ -1 +1,7 @@ -from chat_exporter.chat_exporter import export, raw_export, quick_export, init_exporter +from chat_exporter.chat_exporter import export, raw_export, quick_export + +__all__ = ( + export, + raw_export, + quick_export, +) \ No newline at end of file diff --git a/chat_exporter/build_components.py b/chat_exporter/build_components.py deleted file mode 100644 index 96ab103..0000000 --- a/chat_exporter/build_components.py +++ /dev/null @@ -1,54 +0,0 @@ -import discord - -from chat_exporter.emoji_convert import convert_emoji -from chat_exporter.build_html import fill_out, component_button, PARSE_MODE_NONE, PARSE_MODE_MARKDOWN, PARSE_MODE_EMOJI - - -class BuildComponents: - styles = { - "primary": "#5865F2", - "secondary": "grey", - "success": "#57F287", - "danger": "#ED4245", - "blurple": "#5865F2", - "grey": "grey", - "gray": "grey", - "green": "#57F287", - "red": "#ED4245", - "link": "grey", - } - - components: str = "" - - def __init__(self, component, guild): - self.component = component - self.guild = guild - - async def build_component(self, c): - if isinstance(c, discord.Button): - await self.build_button(c) - - async def build_button(self, c): - icon = "" - url = c.url if c.url else "" - label = c.label if c.label else "" - emoji = await convert_emoji(str(c.emoji)) if c.emoji else "" - style = self.styles[str(c.style).split(".")[1]] - - if url: - icon = ( - ' ' - ) - self.components += await fill_out(self.guild, component_button, [ - ("URL", str(url), PARSE_MODE_NONE), - ("LABEL", str(label), PARSE_MODE_MARKDOWN), - ("EMOJI", str(emoji), PARSE_MODE_EMOJI), - ("ICON", str(icon), PARSE_MODE_NONE), - ("STYLE", style, PARSE_MODE_NONE) - ]) - - async def flow(self): - for c in self.component.children: - await self.build_component(c) - return self.components diff --git a/chat_exporter/build_html.py b/chat_exporter/build_html.py deleted file mode 100644 index f9a0287..0000000 --- a/chat_exporter/build_html.py +++ /dev/null @@ -1,87 +0,0 @@ -import os - -from chat_exporter.parse_mention import ParseMention -from chat_exporter.parse_markdown import ParseMarkdown - -dir_path = os.path.dirname(os.path.realpath(__file__)) - -PARSE_MODE_NONE = 0 -PARSE_MODE_NO_MARKDOWN = 1 -PARSE_MODE_MARKDOWN = 2 -PARSE_MODE_EMBED = 3 -PARSE_MODE_SPECIAL_EMBED = 4 -PARSE_MODE_REFERENCE = 5 -PARSE_MODE_EMOJI = 6 - - -async def fill_out(guild, base, replacements): - for r in replacements: - if len(r) == 2: # default case - k, v = r - r = (k, v, PARSE_MODE_MARKDOWN) - - k, v, mode = r - - if mode != PARSE_MODE_NONE: - v = ParseMention(v, guild).flow() - if mode == PARSE_MODE_MARKDOWN: - v = await ParseMarkdown(v).standard_message_flow() - elif mode == PARSE_MODE_EMBED: - v = await ParseMarkdown(v).standard_embed_flow() - elif mode == PARSE_MODE_SPECIAL_EMBED: - v = await ParseMarkdown(v).special_embed_flow() - elif mode == PARSE_MODE_REFERENCE: - v = await ParseMarkdown(v).message_reference_flow() - elif mode == PARSE_MODE_EMOJI: - v = await ParseMarkdown(v).special_emoji_flow() - - base = base.replace("{{" + k + "}}", v) - - return base - - -def read_file(filename): - with open(filename, "r") as f: - s = f.read() - return s - - -# MESSAGES -start_message = read_file(dir_path + "/chat_exporter_html/message/start.html") -bot_tag = read_file(dir_path + "/chat_exporter_html/message/bot-tag.html") -message_content = read_file(dir_path + "/chat_exporter_html/message/content.html") -message_reference = read_file(dir_path + "/chat_exporter_html/message/reference.html") -message_pin = read_file(dir_path + "/chat_exporter_html/message/pin.html") -message_thread = read_file(dir_path + "/chat_exporter_html/message/thread.html") -message_reference_unknown = read_file(dir_path + "/chat_exporter_html/message/reference_unknown.html") -message_body = read_file(dir_path + "/chat_exporter_html/message/message.html") -end_message = read_file(dir_path + "/chat_exporter_html/message/end.html") - -# COMPONENTS -component_button = read_file(dir_path + "/chat_exporter_html/component/component_button.html") - -# EMBED -embed_body = read_file(dir_path + "/chat_exporter_html/embed/body.html") -embed_title = read_file(dir_path + "/chat_exporter_html/embed/title.html") -embed_description = read_file(dir_path + "/chat_exporter_html/embed/description.html") -embed_field = read_file(dir_path + "/chat_exporter_html/embed/field.html") -embed_field_inline = read_file(dir_path + "/chat_exporter_html/embed/field-inline.html") -embed_footer = read_file(dir_path + "/chat_exporter_html/embed/footer.html") -embed_footer_icon = read_file(dir_path + "/chat_exporter_html/embed/footer_image.html") -embed_image = read_file(dir_path + "/chat_exporter_html/embed/image.html") -embed_thumbnail = read_file(dir_path + "/chat_exporter_html/embed/thumbnail.html") -embed_author = read_file(dir_path + "/chat_exporter_html/embed/author.html") -embed_author_icon = read_file(dir_path + "/chat_exporter_html/embed/author_icon.html") - -# REACTION -emoji = read_file(dir_path + "/chat_exporter_html/reaction/emoji.html") -custom_emoji = read_file(dir_path + "/chat_exporter_html/reaction/custom_emoji.html") - -# ATTACHMENT -img_attachment = read_file(dir_path + "/chat_exporter_html/attachment/image.html") -msg_attachment = read_file(dir_path + "/chat_exporter_html/attachment/message.html") -audio_attachment = read_file(dir_path + "/chat_exporter_html/attachment/audio.html") -video_attachment = read_file(dir_path + "/chat_exporter_html/attachment/video.html") - -# GUILD / FULL TRANSCRIPT -total = read_file(dir_path + "/chat_exporter_html/base.html") diff --git a/chat_exporter/chat_exporter.py b/chat_exporter/chat_exporter.py index 417d757..586e623 100644 --- a/chat_exporter/chat_exporter.py +++ b/chat_exporter/chat_exporter.py @@ -1,462 +1,71 @@ import io -import re -from pytz import timezone -from datetime import timedelta -from dataclasses import dataclass +from typing import List, Optional -from typing import Optional, List +from chat_exporter.construct.transcript import Transcript +from chat_exporter.ext.discord_import import discord -import discord -import traceback -import html -from chat_exporter.build_embed import BuildEmbed -from chat_exporter.build_attachments import BuildAttachment -from chat_exporter.build_components import BuildComponents -from chat_exporter.build_reaction import BuildReaction -from chat_exporter.build_html import fill_out, start_message, bot_tag, message_reference, message_reference_unknown, \ - message_content, message_body, end_message, total, PARSE_MODE_NONE, PARSE_MODE_MARKDOWN, PARSE_MODE_REFERENCE, \ - img_attachment, message_pin, message_thread -from chat_exporter.parse_mention import pass_bot +async def quick_export( + channel: discord.TextChannel, + guild: Optional[discord.Guild] = None, +): + if guild: + channel.guild = guild -from chat_exporter.cache import clear_cache + transcript = ( + await Transcript( + channel=channel, + limit=None, + messages=None, + pytz_timezone="UTC", + ).export() + ).html -bot = None + if not transcript: + return + transcript_embed = discord.Embed( + description=f"**Transcript Name:** transcript-{channel.name}\n\n", + colour=discord.Colour.blurple() + ) -def init_exporter(_bot): - global bot - bot = _bot - pass_bot(bot) + transcript_file = discord.File(io.BytesIO(transcript.encode()), filename=f"transcript-{channel.name}.html") + await channel.send(embed=transcript_embed, file=transcript_file) async def export( channel: discord.TextChannel, - guild: discord.Guild = None, - limit: int = None, - set_timezone="Europe/London", + limit: Optional[int] = None, + tz_info="UTC", + guild: Optional[discord.Guild] = None, ): if guild: channel.guild = guild - # noinspection PyBroadException - try: - return (await Transcript.export(channel, limit, set_timezone)).html - except Exception: - traceback.print_exc() - print(f"Please send a screenshot of the above error to https://www.github.com/mahtoid/DiscordChatExporterPy") + return ( + await Transcript( + channel=channel, + limit=limit, + messages=None, + pytz_timezone=tz_info, + ).export() + ).html async def raw_export( channel: discord.TextChannel, messages: List[discord.Message], - guild: discord.Guild = None, - set_timezone: str = "Europe/London" + tz_info="UTC", + guild: Optional[discord.Guild] = None, ): if guild: channel.guild = guild - # noinspection PyBroadException - try: - return (await Transcript.raw_export(channel, messages, set_timezone)).html - except Exception: - traceback.print_exc() - print(f"Please send a screenshot of the above error to https://www.github.com/mahtoid/DiscordChatExporterPy") - - -async def quick_export( - channel: discord.TextChannel, - guild: discord.Guild = None, -): - if guild: - channel.guild = guild - - # noinspection PyBroadException - try: - transcript = await Transcript.export(channel, None, "Europe/London") - except Exception: - traceback.print_exc() - error_embed = discord.Embed( - title="Transcript Generation Failed!", - description="Whoops! We've stumbled in to an issue here.", - colour=discord.Colour.red() - ) - await channel.send(embed=error_embed) - print(f"Please send a screenshot of the above error to https://www.github.com/mahtoid/DiscordChatExporterPy") - return - - async for m in channel.history(limit=None): - try: - for f in m.attachments: - if f"transcript-{channel.name}.html" in f.filename: - await m.delete() - except TypeError: - continue - - # Save transcript - transcript_embed = discord.Embed( - description=f"**Transcript Name:** transcript-{channel.name}\n\n", - colour=discord.Colour.blurple(), - ) - - transcript_file = discord.File(io.BytesIO(transcript.html.encode()), - filename=f"transcript-{channel.name}.html") - - await channel.send(embed=transcript_embed, file=transcript_file) - - -@dataclass -class Transcript: - guild: discord.Guild - channel: discord.TextChannel - messages: List[discord.Message] - timezone_string: str - html: Optional[str] = None - - @classmethod - async def export( - cls, - channel: discord.TextChannel, - limit: Optional[int], - timezone_string: str = "Europe/London" - ) -> "Transcript": - if limit: - messages = await channel.history(limit=limit).flatten() - messages.reverse() - else: - messages = await channel.history(limit=limit, oldest_first=True).flatten() - - transcript = await Transcript( + return ( + await Transcript( channel=channel, - guild=channel.guild, + limit=None, messages=messages, - timezone_string=timezone(timezone_string) - ).build_transcript() - - return transcript - - @classmethod - async def raw_export( - cls, - channel: discord.TextChannel, - messages: List[discord.Message], - timezone_string: str = 'Europe/London' - ) -> "Transcript": - messages.reverse() - - transcript = await Transcript( - channel=channel, - guild=channel.guild, - messages=messages, - timezone_string=timezone(timezone_string) - ).build_transcript() - - return transcript - - async def build_transcript(self): - previous_message = None - message_html = "" - - for m in self.messages: - message_html += await Message(m, previous_message, self.timezone_string).build_input() - previous_message = m if "pins_add" not in str(m.type) and "thread_created" not in str(m.type)\ - and m.type != 18 else None - - await self.build_guild(message_html) - - clear_cache() - return self - - async def build_guild(self, message_html): - - # discordpy beta - if hasattr(self.guild, "icon_url"): - guild_icon = self.guild.icon_url - else: - guild_icon = self.guild.icon - - if not guild_icon or len(guild_icon) < 2: - guild_icon = "https://cdn.jsdelivr.net/gh/mahtoid/DiscordUtils@master/discord-default.png" - - guild_name = html.escape(self.guild.name) - - self.html = await fill_out(self.guild, total, [ - ("SERVER_NAME", f"Guild: {guild_name}"), - ("SERVER_AVATAR_URL", str(guild_icon), PARSE_MODE_NONE), - ("CHANNEL_NAME", f"Channel: {self.channel.name}"), - ("MESSAGE_COUNT", str(len(self.messages))), - ("MESSAGES", message_html, PARSE_MODE_NONE), - ("TIMEZONE", str(self.timezone_string)), - ]) - - -class Message: - message: discord.Message - previous_message: discord.Message - - message_html: str = "" - embeds: str = "" - attachments: str = "" - components: str = "" - reactions: str = "" - - bot_tag: Optional[str] = None - - transcript: Optional[str] = None - user_colour: Optional[str] = None - - previous_author: Optional[int] = None - previous_timestamp: Optional[int] = None - time_string_create: Optional[str] = None - time_string_edited: Optional[str] = None - - time_format = "%b %d, %Y %I:%M %p" - utc = timezone("UTC") - - def __init__(self, message, previous_message, timezone_string): - self.message = message - self.previous_message = previous_message - self.timezone = timezone_string - self.guild = message.guild - - self.time_string_create, self.time_string_edit = self.set_time() - - async def build_input(self): - self.message.content = html.escape(self.message.content) - self.message.content = re.sub(r"\n", "
", self.message.content) - - if "pins_add" in str(self.message.type): - await self.build_pin() - return self.message_html - - elif "thread_created" in str(self.message.type) or self.message.type == 18: - await self.build_thread() - return self.message_html - - else: - await self.build_message() - return self.message_html - - async def build_pin(self): - await self.generate_message_divider(channel_audit=True) - await self.build_pin_template() - - async def build_thread(self): - await self.generate_message_divider(channel_audit=True) - await self.build_thread_template() - - async def build_message(self): - await self.build_content() - await self.build_reference() - await self.build_sticker() - await self.build_assets() - await self.build_message_template() - - async def build_pin_template(self): - self.message_html += await fill_out(self.guild, message_pin, [ - ("PIN_URL", "https://cdn.jsdelivr.net/gh/mahtoid/DiscordUtils@master/discord-pinned.svg", PARSE_MODE_NONE), - ("USER_COLOUR", self.user_colour_translate(self.message.author)), - ("NAME", str(html.escape(self.message.author.display_name))), - ("NAME_TAG", "%s#%s" % (self.message.author.name, self.message.author.discriminator), PARSE_MODE_NONE), - ("MESSAGE_ID", str(self.message.id), PARSE_MODE_NONE), - ("REF_MESSAGE_ID", str(self.message.reference.message_id), PARSE_MODE_NONE) - ]) - - async def build_thread_template(self): - self.message_html += await fill_out(self.guild, message_thread, [ - ("THREAD_URL", "https://cdn.jsdelivr.net/gh/mahtoid/DiscordUtils@master/discord-thread.svg", - PARSE_MODE_NONE), - ("THREAD_NAME", self.message.content, PARSE_MODE_NONE), - ("USER_COLOUR", self.user_colour_translate(self.message.author)), - ("NAME", str(html.escape(self.message.author.display_name))), - ("NAME_TAG", "%s#%s" % (self.message.author.name, self.message.author.discriminator), PARSE_MODE_NONE), - ("MESSAGE_ID", str(self.message.id), PARSE_MODE_NONE), - ]) - - async def build_assets(self): - for e in self.message.embeds: - self.embeds += await BuildEmbed(e, self.guild).flow() - - for a in self.message.attachments: - self.attachments += await BuildAttachment(a, self.guild).flow() - - # discordpy beta - if hasattr(self.message, "components") and discord.version_info.major == 2: - for c in self.message.components: - self.components += await BuildComponents(c, self.guild).flow() - - for r in self.message.reactions: - self.reactions += await BuildReaction(r, self.guild).flow() - - if self.reactions: - self.reactions = f'
{self.reactions}
' - - if self.components: - self.components = f'
{self.components}
' - - async def build_message_template(self): - await self.generate_message_divider() - - self.message_html += await fill_out(self.guild, message_body, [ - ("MESSAGE_ID", str(self.message.id)), - ("MESSAGE_CONTENT", self.message.content, PARSE_MODE_NONE), - ("EMBEDS", self.embeds, PARSE_MODE_NONE), - ("ATTACHMENTS", self.attachments, PARSE_MODE_NONE), - ("COMPONENTS", self.components, PARSE_MODE_NONE), - ("EMOJI", self.reactions, PARSE_MODE_NONE) - ]) - - return self.message_html - - def _generate_message_divider_check(self): - return bool( - self.previous_message is None or self.message.reference != "" 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)) - ) - - async def generate_message_divider(self, channel_audit=False): - if channel_audit or self._generate_message_divider_check(): - if self.previous_message is not None: - self.message_html += await fill_out(self.guild, end_message, []) - - if channel_audit: - return - - user_colour = self.user_colour_translate(self.message.author) - is_bot = self.check_if_bot(self.message) - - # discordpy beta - if hasattr(self.message.author, "avatar_url"): - avatar_url = str(self.message.author.avatar_url) - else: - avatar_url = str(self.message.author.display_avatar) - - self.message_html += await fill_out(self.guild, start_message, [ - ("REFERENCE", self.message.reference, PARSE_MODE_NONE), - ("AVATAR_URL", 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)), - ("USER_COLOUR", user_colour), - ("NAME", str(html.escape(self.message.author.display_name))), - ("BOT_TAG", is_bot, PARSE_MODE_NONE), - ("TIMESTAMP", self.time_string_create), - ]) - - async def build_content(self): - if not self.message.content: - self.message.content = "" - return - - if self.time_string_edit != "": - self.time_string_edit = ( - f'(edited)' - ) - - self.message.content = await fill_out(self.guild, message_content, [ - ("MESSAGE_CONTENT", self.message.content, PARSE_MODE_MARKDOWN), - ("EDIT", self.time_string_edit, PARSE_MODE_NONE) - ]) - - async def build_sticker(self): - if not self.message.stickers or not hasattr(self.message.stickers[0], "url"): - return - - sticker_image_url = self.message.stickers[0].url - - if sticker_image_url.endswith(".json"): - sticker = await self.message.stickers[0].fetch() - sticker_image_url = ( - f"https://cdn.jsdelivr.net/gh/mahtoid/DiscordUtils@master/stickers/" - f"{sticker.pack_id}/{sticker.id}.gif" - ) - - self.message.content = await fill_out(self.guild, img_attachment, [ - ("ATTACH_URL", str(sticker_image_url), PARSE_MODE_NONE), - ("ATTACH_URL_THUMB", str(sticker_image_url), PARSE_MODE_NONE) - ]) - - async def build_reference(self): - if not self.message.reference: - self.message.reference = "" - return - - try: - message: discord.Message = await self.message.channel.fetch_message(self.message.reference.message_id) - except (discord.NotFound, discord.HTTPException) as e: - self.message.reference = "" - if isinstance(e, discord.NotFound): - self.message.reference = message_reference_unknown - return - - is_bot = self.check_if_bot(message) - user_colour = self.user_colour_translate(message.author) - - if not message.content: - message.content = "Click to see attachment" - - if message.embeds or message.attachments: - attachment_icon = ( - '' - ) - else: - attachment_icon = "" - - _, time_string_edit = self.set_time() - - if time_string_edit != "": - time_string_edit = ( - f'(edited)' - ) - - # discordpy beta - if hasattr(message.author, "avatar_url"): - avatar_url = message.author.avatar_url - else: - avatar_url = message.author.avatar - - self.message.reference = await fill_out(self.guild, message_reference, [ - ("AVATAR_URL", str(avatar_url), PARSE_MODE_NONE), - ("BOT_TAG", is_bot, PARSE_MODE_NONE), - ("NAME_TAG", "%s#%s" % (message.author.name, message.author.discriminator), PARSE_MODE_NONE), - ("NAME", str(html.escape(message.author.display_name))), - ("USER_COLOUR", user_colour, PARSE_MODE_NONE), - ("CONTENT", message.content, PARSE_MODE_REFERENCE), - ("EDIT", time_string_edit, PARSE_MODE_NONE), - ("ATTACHMENT_ICON", attachment_icon, PARSE_MODE_NONE), - ("MESSAGE_ID", str(self.message.reference.message_id), PARSE_MODE_NONE) - ]) - - @staticmethod - def check_if_bot(message): - if message.author.bot: - return bot_tag - else: - return "" - - def user_colour_translate(self, author: discord.Member): - try: - member = self.guild.get_member(author.id) - except discord.NotFound: - member = author - - user_colour = "#FFFFFF" - if member is not None: - if '#000000' not in str(member.colour): - user_colour = member.colour - - return f"color: {user_colour};" - - def set_time(self): - created_at_str = self.to_local_time_str(self.message.created_at) - edited_at_str = self.to_local_time_str(self.message.edited_at) if self.message.edited_at is not None else "" - - return created_at_str, edited_at_str - - def to_local_time_str(self, time): - if not self.message.created_at.tzinfo: - time = timezone("UTC").localize(time) - - local_time = time.astimezone(self.timezone) - return local_time.strftime(self.time_format) + pytz_timezone=tz_info, + ).export() + ).html diff --git a/chat_exporter/chat_exporter_html/base.html b/chat_exporter/chat_exporter_html/base.html deleted file mode 100644 index 01e8c95..0000000 --- a/chat_exporter/chat_exporter_html/base.html +++ /dev/null @@ -1,828 +0,0 @@ - - - - - - - {{SERVER_NAME}} - {{CHANNEL_NAME}} - - - - - - - - - - - - - - - - -
-
- -
-
-
{{SERVER_NAME}}
-
{{CHANNEL_NAME}}
- - -
{{MESSAGE_COUNT}} messages
-
Timezone: {{TIMEZONE}}
- -
-
- -
-{{MESSAGES}} -
- - - diff --git a/chat_exporter/chat_exporter_html/__init__.py b/chat_exporter/construct/__init__.py similarity index 100% rename from chat_exporter/chat_exporter_html/__init__.py rename to chat_exporter/construct/__init__.py diff --git a/chat_exporter/construct/assets/__init__.py b/chat_exporter/construct/assets/__init__.py new file mode 100644 index 0000000..a39ad72 --- /dev/null +++ b/chat_exporter/construct/assets/__init__.py @@ -0,0 +1,11 @@ +from .embed import Embed +from .reaction import Reaction +from .attachment import Attachment +from .component import Component + +__all__ = ( + Embed, + Reaction, + Attachment, + Component, +) \ No newline at end of file diff --git a/chat_exporter/build_attachments.py b/chat_exporter/construct/assets/attachment.py similarity index 82% rename from chat_exporter/build_attachments.py rename to chat_exporter/construct/assets/attachment.py index e7812bf..4efd610 100644 --- a/chat_exporter/build_attachments.py +++ b/chat_exporter/construct/assets/attachment.py @@ -1,6 +1,7 @@ import math -from chat_exporter.build_html import ( +from chat_exporter.ext.discord_utils import DiscordUtils +from chat_exporter.ext.html_generator import ( fill_out, img_attachment, msg_attachment, @@ -10,7 +11,7 @@ ) -class BuildAttachment: +class Attachment: def __init__(self, attachments, guild): self.attachments = attachments self.guild = guild @@ -41,7 +42,7 @@ async def video(self): ]) async def audio(self): - file_icon = "https://cdn.jsdelivr.net/gh/mahtoid/DiscordUtils@master/discord-audio.svg" + file_icon = DiscordUtils.file_attachment_audio file_size = self.get_file_size(self.attachments.size) self.attachments = await fill_out(self.guild, audio_attachment, [ @@ -89,14 +90,14 @@ async def get_file_icon(self) -> str: extension = self.attachments.url.rsplit('.', 1)[1] if extension in acrobat_types: - return "https://cdn.jsdelivr.net/gh/mahtoid/DiscordUtils@master/discord-acrobat.svg" + return DiscordUtils.file_attachment_acrobat elif extension in webcode_types: - return "https://cdn.jsdelivr.net/gh/mahtoid/DiscordUtils@master/discord-webcode.svg" + return DiscordUtils.file_attachment_webcode elif extension in code_types: - return "https://cdn.jsdelivr.net/gh/mahtoid/DiscordUtils@master/discord-code.svg" + return DiscordUtils.file_attachment_code elif extension in document_types: - return "https://cdn.jsdelivr.net/gh/mahtoid/DiscordUtils@master/discord-document.svg" + return DiscordUtils.file_attachment_document elif extension in archive_types: - return "https://cdn.jsdelivr.net/gh/mahtoid/DiscordUtils@master/discord-archive.svg" + return DiscordUtils.file_attachment_archive else: - return "https://cdn.jsdelivr.net/gh/mahtoid/DiscordUtils@master/discord-unknown.svg" + return DiscordUtils.file_attachment_unknown diff --git a/chat_exporter/construct/assets/component.py b/chat_exporter/construct/assets/component.py new file mode 100644 index 0000000..eb9673a --- /dev/null +++ b/chat_exporter/construct/assets/component.py @@ -0,0 +1,100 @@ +from chat_exporter.ext.discord_import import discord + +from chat_exporter.ext.emoji_convert import convert_emoji +from chat_exporter.ext.discord_utils import DiscordUtils +from chat_exporter.ext.html_generator import ( + fill_out, + component_button, + component_menu, + component_menu_options, + component_menu_options_emoji, + PARSE_MODE_NONE, + PARSE_MODE_EMOJI, + PARSE_MODE_MARKDOWN, +) + + +class Component: + styles = { + "primary": "#5865F2", + "secondary": "grey", + "success": "#57F287", + "danger": "#ED4245", + "blurple": "#5865F2", + "grey": "grey", + "gray": "grey", + "green": "#57F287", + "red": "#ED4245", + "link": "grey", + } + + components: str = "" + menu_div_id: int = 0 + + def __init__(self, component, guild): + self.component = component + self.guild = guild + + async def build_component(self, c): + if isinstance(c, discord.Button): + await self.build_button(c) + elif isinstance(c, discord.SelectMenu): + await self.build_menu(c) + Component.menu_div_id += 1 + + async def build_button(self, c): + url = c.url if c.url else "" + label = c.label if c.label else "" + style = self.styles[str(c.style).split(".")[1]] + icon = DiscordUtils.button_external_link if url else "" + emoji = await convert_emoji(str(c.emoji)) if c.emoji else "" + + self.components += await fill_out(self.guild, component_button, [ + ("DISABLED", "chatlog__component-disabled" if c.disabled else "", PARSE_MODE_NONE), + ("URL", str(url), PARSE_MODE_NONE), + ("LABEL", str(label), PARSE_MODE_MARKDOWN), + ("EMOJI", str(emoji), PARSE_MODE_NONE), + ("ICON", str(icon), PARSE_MODE_NONE), + ("STYLE", style, PARSE_MODE_NONE) + ]) + + async def build_menu(self, c): + placeholder = c.placeholder if c.placeholder else "" + options = c.options + content = "" + + if not c.disabled: + content = await self.build_menu_options(options) + + self.components += await fill_out(self.guild, component_menu, [ + ("DISABLED", "chatlog__component-disabled" if c.disabled else "", PARSE_MODE_NONE), + ("ID", str(self.menu_div_id), PARSE_MODE_NONE), + ("PLACEHOLDER", str(placeholder), PARSE_MODE_MARKDOWN), + ("CONTENT", str(content), PARSE_MODE_NONE), + ("ICON", DiscordUtils.interaction_dropdown_icon, PARSE_MODE_NONE), + ]) + + async def build_menu_options(self, options): + content = [] + for option in options: + if option.emoji: + content.append(await fill_out(self.guild, component_menu_options_emoji, [ + ("EMOJI", str(option.emoji), PARSE_MODE_EMOJI), + ("TITLE", str(option.label), PARSE_MODE_MARKDOWN), + ("DESCRIPTION", str(option.description) if option.description else "", PARSE_MODE_MARKDOWN) + ])) + else: + content.append(await fill_out(self.guild, component_menu_options, [ + ("TITLE", str(option.label), PARSE_MODE_MARKDOWN), + ("DESCRIPTION", str(option.description) if option.description else "", PARSE_MODE_MARKDOWN) + ])) + + if content: + content = f'' + + return content + + async def flow(self): + for c in self.component.children: + await self.build_component(c) + return self.components diff --git a/chat_exporter/build_embed.py b/chat_exporter/construct/assets/embed.py similarity index 61% rename from chat_exporter/build_embed.py rename to chat_exporter/construct/assets/embed.py index 5411eea..7cc77b6 100644 --- a/chat_exporter/build_embed.py +++ b/chat_exporter/construct/assets/embed.py @@ -1,11 +1,26 @@ -import discord - -from chat_exporter.build_html import fill_out, embed_body, embed_title, embed_description, embed_field, \ - embed_field_inline, embed_footer, embed_footer_icon, embed_image, embed_thumbnail, embed_author, embed_author_icon, \ - PARSE_MODE_EMBED, PARSE_MODE_SPECIAL_EMBED, PARSE_MODE_NONE, PARSE_MODE_MARKDOWN - - -class BuildEmbed: +from chat_exporter.ext.discord_import import discord + +from chat_exporter.ext.html_generator import ( + fill_out, + embed_body, + embed_title, + embed_description, + embed_field, + embed_field_inline, + embed_footer, + embed_footer_icon, + embed_image, + embed_thumbnail, + embed_author, + embed_author_icon, + PARSE_MODE_NONE, + PARSE_MODE_EMBED, + PARSE_MODE_MARKDOWN, + PARSE_MODE_SPECIAL_EMBED, +) + + +class Embed: r: str g: str b: str @@ -35,26 +50,23 @@ async def flow(self): return self.embed def build_colour(self): - self.r, self.g, self.b = (self.embed.colour.r, self.embed.colour.g, self.embed.colour.b) \ - if self.embed.colour != discord.Embed.Empty \ - else (0x20, 0x22, 0x25) # default colour + self.r, self.g, self.b = ( + (self.embed.colour.r, self.embed.colour.g, self.embed.colour.b) + if self.embed.colour != discord.Embed.Empty else (0x20, 0x22, 0x25) # default colour + ) async def build_title(self): - self.title = self.embed.title \ - if self.embed.title != discord.Embed.Empty \ - else "" + self.title = self.embed.title if self.embed.title != discord.Embed.Empty else "" - if self.title != "": + if self.title: self.title = await fill_out(self.guild, embed_title, [ ("EMBED_TITLE", self.title, PARSE_MODE_MARKDOWN) ]) async def build_description(self): - self.description = self.embed.description \ - if self.embed.description != discord.Embed.Empty \ - else "" + self.description = self.embed.description if self.embed.description != discord.Embed.Empty else "" - if self.description != "": + if self.description: self.description = await fill_out(self.guild, embed_description, [ ("EMBED_DESC", self.embed.description, PARSE_MODE_EMBED) ]) @@ -73,9 +85,7 @@ async def build_fields(self): ("FIELD_VALUE", field.value, PARSE_MODE_EMBED)]) async def build_author(self): - self.author = self.embed.author.name \ - if self.embed.author.name != discord.Embed.Empty \ - else "" + self.author = self.embed.author.name if self.embed.author.name != discord.Embed.Empty else "" self.author = f'{self.author}' \ if self.embed.author.url != discord.Embed.Empty \ @@ -84,9 +94,7 @@ async def build_author(self): author_icon = await fill_out(self.guild, embed_author_icon, [ ("AUTHOR", self.author, PARSE_MODE_NONE), ("AUTHOR_ICON", self.embed.author.icon_url, PARSE_MODE_NONE) - ]) \ - if self.embed.author.icon_url != discord.Embed.Empty \ - else "" + ]) if self.embed.author.icon_url != discord.Embed.Empty else "" if author_icon == "" and self.author != "": self.author = await fill_out(self.guild, embed_author, [("AUTHOR", self.author, PARSE_MODE_NONE)]) @@ -96,36 +104,28 @@ async def build_author(self): async def build_image(self): self.image = await fill_out(self.guild, embed_image, [ ("EMBED_IMAGE", str(self.embed.image.proxy_url), PARSE_MODE_NONE) - ]) \ - if self.embed.image.url != discord.Embed.Empty \ - else "" + ]) if self.embed.image.url != discord.Embed.Empty else "" async def build_thumbnail(self): self.thumbnail = await fill_out(self.guild, embed_thumbnail, [ ("EMBED_THUMBNAIL", str(self.embed.thumbnail.url), PARSE_MODE_NONE)]) \ - if self.embed.thumbnail.url != discord.Embed.Empty \ - else "" + if self.embed.thumbnail.url != discord.Embed.Empty else "" async def build_footer(self): - footer = self.embed.footer.text \ - if self.embed.footer.text != discord.Embed.Empty \ - else "" - footer_icon = self.embed.footer.icon_url \ - if self.embed.footer.icon_url != discord.Embed.Empty \ - else None - - if footer != "": - if footer_icon is not None: - self.footer = await fill_out(self.guild, embed_footer_icon, [ - ("EMBED_FOOTER", footer, PARSE_MODE_NONE), - ("EMBED_FOOTER_ICON", footer_icon, PARSE_MODE_NONE) - ]) - else: - self.footer = await fill_out(self.guild, embed_footer, [ - ("EMBED_FOOTER", footer, PARSE_MODE_NONE), - ]) + self.footer = self.embed.footer.text if self.embed.footer.text != discord.Embed.Empty else "" + footer_icon = self.embed.footer.icon_url if self.embed.footer.icon_url != discord.Embed.Empty else None + + if not self.footer: + return + + if footer_icon is not None: + self.footer = await fill_out(self.guild, embed_footer_icon, [ + ("EMBED_FOOTER", self.footer, PARSE_MODE_NONE), + ("EMBED_FOOTER_ICON", footer_icon, PARSE_MODE_NONE) + ]) else: - self.footer = "" + self.footer = await fill_out(self.guild, embed_footer, [ + ("EMBED_FOOTER", self.footer, PARSE_MODE_NONE)]) async def build_embed(self): self.embed = await fill_out(self.guild, embed_body, [ @@ -138,5 +138,5 @@ async def build_embed(self): ("EMBED_THUMBNAIL", self.thumbnail, PARSE_MODE_NONE), ("EMBED_DESC", self.description, PARSE_MODE_NONE), ("EMBED_FIELDS", self.fields, PARSE_MODE_NONE), - ("EMBED_FOOTER", self.footer, PARSE_MODE_NONE) + ("EMBED_FOOTER", self.footer, PARSE_MODE_NONE), ]) diff --git a/chat_exporter/build_reaction.py b/chat_exporter/construct/assets/reaction.py similarity index 88% rename from chat_exporter/build_reaction.py rename to chat_exporter/construct/assets/reaction.py index d2d71fd..d970b6d 100644 --- a/chat_exporter/build_reaction.py +++ b/chat_exporter/construct/assets/reaction.py @@ -1,10 +1,10 @@ import re -from chat_exporter.emoji_convert import convert_emoji -from chat_exporter.build_html import fill_out, emoji, custom_emoji, PARSE_MODE_NONE +from chat_exporter.ext.emoji_convert import convert_emoji +from chat_exporter.ext.html_generator import fill_out, emoji, custom_emoji, PARSE_MODE_NONE -class BuildReaction: +class Reaction: def __init__(self, reaction, guild): self.reaction = reaction self.guild = guild diff --git a/chat_exporter/construct/message.py b/chat_exporter/construct/message.py new file mode 100644 index 0000000..a41e85a --- /dev/null +++ b/chat_exporter/construct/message.py @@ -0,0 +1,289 @@ +import html +from typing import List, Optional + +from pytz import timezone +from datetime import timedelta + +from chat_exporter.ext.discord_import import discord + +from chat_exporter.construct.assets import Attachment, Component, Embed, Reaction +from chat_exporter.ext.discord_utils import DiscordUtils +from chat_exporter.ext.html_generator import ( + fill_out, + bot_tag, + message_body, + message_pin, + message_thread, + message_content, + message_reference, + message_reference_unknown, + img_attachment, + start_message, + end_message, + PARSE_MODE_NONE, + PARSE_MODE_MARKDOWN, + PARSE_MODE_REFERENCE, +) + + +def _gather_user_bot(author: discord.Member): + return bot_tag if author.bot else "" + + +def _set_edit_at(message_edited_at): + return f'(edited)' + + +class MessageConstruct: + message_html: str = "" + + # Asset Types + embeds: str = "" + reactions: str = "" + components: str = "" + attachments: str = "" + + def __init__( + self, + message: discord.Message, + previous_message: Optional[discord.Message], + pytz_timezone, + guild: discord.Guild, + ): + self.message = message + self.previous_message = previous_message + self.pytz_timezone = pytz_timezone + self.guild = guild + self.message_created_at, self.message_edited_at = self.set_time() + + async def construct_message( + self, + ) -> str: + if self.message.type == "pins_added": + await self.build_pin() + elif self.message.type == "thread_created": + await self.build_thread() + else: + await self.build_message() + return self.message_html + + async def build_message(self): + await self.build_content() + await self.build_reference() + await self.build_sticker() + await self.build_assets() + await self.build_message_template() + + async def build_pin(self): + await self.generate_message_divider(channel_audit=True) + await self.build_pin_template() + + async def build_thread(self): + await self.generate_message_divider(channel_audit=True) + await self.build_thread_template() + + async def build_content(self): + if not self.message.content: + self.message.content = "" + return + + if self.message_edited_at: + self.message_edited_at = _set_edit_at(self.message_edited_at) + + self.message.content = await fill_out(self.guild, message_content, [ + ("MESSAGE_CONTENT", self.message.content, PARSE_MODE_MARKDOWN), + ("EDIT", self.message_edited_at, PARSE_MODE_NONE) + ]) + + async def build_reference(self): + if not self.message.reference: + self.message.reference = "" + return + + try: + message: discord.Message = await self.message.channel.fetch_message(self.message.reference.message_id) + except (discord.NotFound, discord.HTTPException) as e: + self.message.reference = "" + if isinstance(e, discord.NotFound): + self.message.reference = message_reference_unknown + return + + is_bot = _gather_user_bot(message.author) + user_colour = await self._gather_user_colour(message.author) + + if not message.content: + message.content = "Click to see attachment" + + attachment_icon = DiscordUtils.reference_attachment_icon if message.embeds or message.attachments else "" + + _, message_edited_at = self.set_time(message) + + if message_edited_at: + message_edited_at = _set_edit_at(message_edited_at) + + avatar_url = self.message.author.avatar if self.message.author.avatar else DiscordUtils.default_avatar + + self.message.reference = await fill_out(self.guild, message_reference, [ + ("AVATAR_URL", str(avatar_url), PARSE_MODE_NONE), + ("BOT_TAG", is_bot, PARSE_MODE_NONE), + ("NAME_TAG", "%s#%s" % (message.author.name, message.author.discriminator), PARSE_MODE_NONE), + ("NAME", str(html.escape(message.author.display_name))), + ("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) + ]) + + async def build_sticker(self): + if not self.message.stickers or not hasattr(self.message.stickers[0], "url"): + return + + sticker_image_url = self.message.stickers[0].url + + if sticker_image_url.endswith(".json"): + sticker = await self.message.stickers[0].fetch() + sticker_image_url = ( + f"https://cdn.jsdelivr.net/gh/mahtoid/DiscordUtils@master/stickers/{sticker.pack_id}/{sticker.id}.gif" + ) + + self.message.content = await fill_out(self.guild, img_attachment, [ + ("ATTACH_URL", str(sticker_image_url), PARSE_MODE_NONE), + ("ATTACH_URL_THUMB", str(sticker_image_url), PARSE_MODE_NONE) + ]) + + async def build_assets(self): + for e in self.message.embeds: + self.embeds += await Embed(e, self.guild).flow() + + for a in self.message.attachments: + self.attachments += await Attachment(a, self.guild).flow() + + for c in self.message.components: + self.components += await Component(c, self.guild).flow() + + for r in self.message.reactions: + self.reactions += await Reaction(r, self.guild).flow() + + if self.reactions: + self.reactions = f'
{self.reactions}
' + + if self.components: + self.components = f'
{self.components}
' + + async def build_message_template(self): + await self.generate_message_divider() + + self.message_html += await fill_out(self.guild, message_body, [ + ("MESSAGE_ID", str(self.message.id)), + ("MESSAGE_CONTENT", self.message.content, PARSE_MODE_NONE), + ("EMBEDS", self.embeds, PARSE_MODE_NONE), + ("ATTACHMENTS", self.attachments, PARSE_MODE_NONE), + ("COMPONENTS", self.components, PARSE_MODE_NONE), + ("EMOJI", self.reactions, PARSE_MODE_NONE) + ]) + + return self.message_html + + def _generate_message_divider_check(self): + return bool( + self.previous_message is None or self.message.reference != "" 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)) + ) + + async def generate_message_divider(self, channel_audit=False): + if channel_audit or self._generate_message_divider_check(): + if self.previous_message is not None: + self.message_html += await fill_out(self.guild, end_message, []) + + if channel_audit: + return + + user_colour = await self._gather_user_colour(self.message.author) + is_bot = _gather_user_bot(self.message.author) + avatar_url = self.message.author.avatar if self.message.author.avatar else DiscordUtils.default_avatar + + self.message_html += await fill_out(self.guild, start_message, [ + ("REFERENCE", self.message.reference, 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)), + ("USER_COLOUR", str(user_colour)), + ("NAME", str(html.escape(self.message.author.display_name))), + ("BOT_TAG", str(is_bot), PARSE_MODE_NONE), + ("TIMESTAMP", str(self.message_created_at)), + ]) + + async def build_pin_template(self): + self.message_html += await fill_out(self.guild, message_pin, [ + ("PIN_URL", DiscordUtils.pinned_message_icon, PARSE_MODE_NONE), + ("USER_COLOUR", await self._gather_user_colour(self.message.author)), + ("NAME", str(html.escape(self.message.author.display_name))), + ("NAME_TAG", "%s#%s" % (self.message.author.name, self.message.author.discriminator), PARSE_MODE_NONE), + ("MESSAGE_ID", str(self.message.id), PARSE_MODE_NONE), + ("REF_MESSAGE_ID", str(self.message.reference.message_id), PARSE_MODE_NONE) + ]) + + async def build_thread_template(self): + self.message_html += await fill_out(self.guild, message_thread, [ + ("THREAD_URL", DiscordUtils.thread_channel_icon, + PARSE_MODE_NONE), + ("THREAD_NAME", self.message.content, PARSE_MODE_NONE), + ("USER_COLOUR", await self._gather_user_colour(self.message.author)), + ("NAME", str(html.escape(self.message.author.display_name))), + ("NAME_TAG", "%s#%s" % (self.message.author.name, self.message.author.discriminator), PARSE_MODE_NONE), + ("MESSAGE_ID", str(self.message.id), PARSE_MODE_NONE), + ]) + + async def _gather_user_colour(self, author: discord.Member): + member = self.guild.get_member(author.id) + if not member: + try: + member = await self.guild.fetch_member(author.id) + except Exception: + # This is disgusting, but has to be done for NextCord + member = None + user_colour = member.colour if member and str(member.colour) != "#000000" else "#FFFFFF" + return f"color: {user_colour};" + + def set_time(self, message: Optional[discord.Message] = None): + message = message if message else self.message + created_at_str = self.to_local_time_str(message.created_at) + edited_at_str = self.to_local_time_str(message.edited_at) if message.edited_at else "" + + return created_at_str, edited_at_str + + def to_local_time_str(self, time): + if not self.message.created_at.tzinfo: + time = timezone("UTC").localize(time) + + local_time = time.astimezone(timezone(self.pytz_timezone)) + return local_time.strftime("%b %d, %Y %I:%M %p") + + +class Message: + def __init__( + self, + messages: List[discord.Message], + guild: discord.Guild, + pytz_timezone, + ): + self.messages = messages + self.guild = guild + self.pytz_timezone = pytz_timezone + + async def gather(self) -> str: + message_html: str = "" + previous_message: Optional[discord.Message] = None + + for message in self.messages: + message_html += await MessageConstruct( + message, + previous_message, + self.pytz_timezone, + self.guild + ).construct_message() + + previous_message = message + return message_html diff --git a/chat_exporter/construct/transcript.py b/chat_exporter/construct/transcript.py new file mode 100644 index 0000000..e55a346 --- /dev/null +++ b/chat_exporter/construct/transcript.py @@ -0,0 +1,62 @@ +import html +import traceback + +from typing import List, Optional + +from chat_exporter.ext.discord_import import discord + +from chat_exporter.construct.message import Message +from chat_exporter.construct.assets.component import Component + +from chat_exporter.ext.cache import clear_cache +from chat_exporter.ext.discord_utils import DiscordUtils +from chat_exporter.ext.html_generator import fill_out, total, PARSE_MODE_NONE + + +class TranscriptDAO: + html: str + + def __init__( + self, + channel: discord.TextChannel, + limit: Optional[int], + messages: Optional[List[discord.Message]], + pytz_timezone, + ): + self.channel = channel + self.messages = messages + self.limit = int(limit) if limit else None + self.pytz_timezone = pytz_timezone + + async def build_transcript(self): + message_html = await Message(self.messages, self.channel.guild, self.pytz_timezone).gather() + await self.export_transcript(message_html) + clear_cache() + Component.menu_div_id = 0 + return self + + async def export_transcript(self, message_html: str): + guild_icon = self.channel.guild.icon if self.channel.guild.icon and len(self.channel.guild.icon) > 2 else DiscordUtils.default_avatar + guild_name = html.escape(self.channel.guild.name) + + self.html = await fill_out(self.channel.guild, total, [ + ("SERVER_NAME", f"Guild: {guild_name}"), + ("SERVER_AVATAR_URL", str(guild_icon), PARSE_MODE_NONE), + ("CHANNEL_NAME", f"Channel: {self.channel.name}"), + ("MESSAGE_COUNT", str(len(self.messages))), + ("MESSAGES", message_html, PARSE_MODE_NONE), + ("TIMEZONE", str(self.pytz_timezone)), + ]) + + +class Transcript(TranscriptDAO): + async def export(self): + if not self.messages: + self.messages = await self.channel.history(limit=self.limit).flatten() + self.messages.reverse() + + try: + return await super().build_transcript() + except Exception: + traceback.print_exc() + print(f"Please send a screenshot of the above error to https://www.github.com/mahtoid/DiscordChatExporterPy") diff --git a/chat_exporter/ext/__init__.py b/chat_exporter/ext/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/chat_exporter/cache.py b/chat_exporter/ext/cache.py similarity index 100% rename from chat_exporter/cache.py rename to chat_exporter/ext/cache.py diff --git a/chat_exporter/ext/discord_import.py b/chat_exporter/ext/discord_import.py new file mode 100644 index 0000000..08df873 --- /dev/null +++ b/chat_exporter/ext/discord_import.py @@ -0,0 +1,7 @@ +discord_modules = ['disnake', 'nextcord', 'discord'] +for module in discord_modules: + try: + discord = __import__(module) + break + except ImportError: + continue diff --git a/chat_exporter/ext/discord_utils.py b/chat_exporter/ext/discord_utils.py new file mode 100644 index 0000000..a6766e3 --- /dev/null +++ b/chat_exporter/ext/discord_utils.py @@ -0,0 +1,14 @@ +class DiscordUtils: + default_avatar: str = 'https://cdn.jsdelivr.net/gh/mahtoid/DiscordUtils@master/discord-default.png' + pinned_message_icon: str = 'https://cdn.jsdelivr.net/gh/mahtoid/DiscordUtils@master/discord-pinned.svg' + thread_channel_icon: str = 'https://cdn.jsdelivr.net/gh/mahtoid/DiscordUtils@master/discord-thread.svg' + file_attachment_audio: str = 'https://cdn.jsdelivr.net/gh/mahtoid/DiscordUtils@master/discord-audio.svg' + file_attachment_acrobat: str = 'https://cdn.jsdelivr.net/gh/mahtoid/DiscordUtils@master/discord-acrobat.svg' + file_attachment_webcode: str = 'https://cdn.jsdelivr.net/gh/mahtoid/DiscordUtils@master/discord-webcode.svg' + file_attachment_code: str = 'https://cdn.jsdelivr.net/gh/mahtoid/DiscordUtils@master/discord-code.svg' + file_attachment_document: str = 'https://cdn.jsdelivr.net/gh/mahtoid/DiscordUtils@master/discord-document.svg' + file_attachment_archive: str = 'https://cdn.jsdelivr.net/gh/mahtoid/DiscordUtils@master/discord-archive.svg' + file_attachment_unknown: str = 'https://cdn.jsdelivr.net/gh/mahtoid/DiscordUtils@master/discord-unknown.svg' + button_external_link: str = '' + reference_attachment_icon: str = '' + interaction_dropdown_icon: str = '' \ No newline at end of file diff --git a/chat_exporter/emoji_convert.py b/chat_exporter/ext/emoji_convert.py similarity index 98% rename from chat_exporter/emoji_convert.py rename to chat_exporter/ext/emoji_convert.py index 4069ab8..d00975f 100644 --- a/chat_exporter/emoji_convert.py +++ b/chat_exporter/ext/emoji_convert.py @@ -34,7 +34,7 @@ import emoji import aiohttp -from chat_exporter.cache import cache +from chat_exporter.ext.cache import cache cdn_fmt = "https://twemoji.maxcdn.com/v/latest/72x72/{codepoint}.png" diff --git a/chat_exporter/ext/html_generator.py b/chat_exporter/ext/html_generator.py new file mode 100644 index 0000000..99afbb2 --- /dev/null +++ b/chat_exporter/ext/html_generator.py @@ -0,0 +1,90 @@ +import os + +from chat_exporter.parse.mention import ParseMention +from chat_exporter.parse.markdown import ParseMarkdown + +dir_path = os.path.abspath(os.path.join((os.path.dirname(os.path.realpath(__file__))), "..")) + +PARSE_MODE_NONE = 0 +PARSE_MODE_NO_MARKDOWN = 1 +PARSE_MODE_MARKDOWN = 2 +PARSE_MODE_EMBED = 3 +PARSE_MODE_SPECIAL_EMBED = 4 +PARSE_MODE_REFERENCE = 5 +PARSE_MODE_EMOJI = 6 + + +async def fill_out(guild, base, replacements): + for r in replacements: + if len(r) == 2: # default case + k, v = r + r = (k, v, PARSE_MODE_MARKDOWN) + + k, v, mode = r + + if mode != PARSE_MODE_NONE: + v = ParseMention(v, guild).flow() + if mode == PARSE_MODE_MARKDOWN: + v = await ParseMarkdown(v).standard_message_flow() + elif mode == PARSE_MODE_EMBED: + v = await ParseMarkdown(v).standard_embed_flow() + elif mode == PARSE_MODE_SPECIAL_EMBED: + v = await ParseMarkdown(v).special_embed_flow() + elif mode == PARSE_MODE_REFERENCE: + v = await ParseMarkdown(v).message_reference_flow() + elif mode == PARSE_MODE_EMOJI: + v = await ParseMarkdown(v).special_emoji_flow() + + base = base.replace("{{" + k + "}}", v) + + return base + + +def read_file(filename): + with open(filename, "r") as f: + s = f.read() + return s + + +# MESSAGES +start_message = read_file(dir_path + "/html/message/start.html") +bot_tag = read_file(dir_path + "/html/message/bot-tag.html") +message_content = read_file(dir_path + "/html/message/content.html") +message_reference = read_file(dir_path + "/html/message/reference.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") +message_body = read_file(dir_path + "/html/message/message.html") +end_message = read_file(dir_path + "/html/message/end.html") + +# COMPONENTS +component_button = read_file(dir_path + "/html/component/component_button.html") +component_menu = read_file(dir_path + "/html/component/component_menu.html") +component_menu_options = read_file(dir_path + "/html/component/component_menu_options.html") +component_menu_options_emoji = read_file(dir_path + "/html/component/component_menu_options_emoji.html") + +# EMBED +embed_body = read_file(dir_path + "/html/embed/body.html") +embed_title = read_file(dir_path + "/html/embed/title.html") +embed_description = read_file(dir_path + "/html/embed/description.html") +embed_field = read_file(dir_path + "/html/embed/field.html") +embed_field_inline = read_file(dir_path + "/html/embed/field-inline.html") +embed_footer = read_file(dir_path + "/html/embed/footer.html") +embed_footer_icon = read_file(dir_path + "/html/embed/footer_image.html") +embed_image = read_file(dir_path + "/html/embed/image.html") +embed_thumbnail = read_file(dir_path + "/html/embed/thumbnail.html") +embed_author = read_file(dir_path + "/html/embed/author.html") +embed_author_icon = read_file(dir_path + "/html/embed/author_icon.html") + +# REACTION +emoji = read_file(dir_path + "/html/reaction/emoji.html") +custom_emoji = read_file(dir_path + "/html/reaction/custom_emoji.html") + +# ATTACHMENT +img_attachment = read_file(dir_path + "/html/attachment/image.html") +msg_attachment = read_file(dir_path + "/html/attachment/message.html") +audio_attachment = read_file(dir_path + "/html/attachment/audio.html") +video_attachment = read_file(dir_path + "/html/attachment/video.html") + +# GUILD / FULL TRANSCRIPT +total = read_file(dir_path + "/html/base.html") diff --git a/chat_exporter/html/__init__.py b/chat_exporter/html/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/chat_exporter/chat_exporter_html/attachment/audio.html b/chat_exporter/html/attachment/audio.html similarity index 100% rename from chat_exporter/chat_exporter_html/attachment/audio.html rename to chat_exporter/html/attachment/audio.html diff --git a/chat_exporter/chat_exporter_html/attachment/image.html b/chat_exporter/html/attachment/image.html similarity index 100% rename from chat_exporter/chat_exporter_html/attachment/image.html rename to chat_exporter/html/attachment/image.html diff --git a/chat_exporter/chat_exporter_html/attachment/message.html b/chat_exporter/html/attachment/message.html similarity index 100% rename from chat_exporter/chat_exporter_html/attachment/message.html rename to chat_exporter/html/attachment/message.html diff --git a/chat_exporter/chat_exporter_html/attachment/video.html b/chat_exporter/html/attachment/video.html similarity index 100% rename from chat_exporter/chat_exporter_html/attachment/video.html rename to chat_exporter/html/attachment/video.html diff --git a/chat_exporter/html/base.html b/chat_exporter/html/base.html new file mode 100644 index 0000000..ca47442 --- /dev/null +++ b/chat_exporter/html/base.html @@ -0,0 +1,929 @@ + + + + + + + {{SERVER_NAME}} - {{CHANNEL_NAME}} + + + + + + + + + + +
+
+ +
+
+
{{SERVER_NAME}}
+
{{CHANNEL_NAME}}
+ + +
{{MESSAGE_COUNT}} messages
+
Timezone: {{TIMEZONE}}
+ +
+
+ +
+{{MESSAGES}} +
+ + + diff --git a/chat_exporter/chat_exporter_html/component/component_button.html b/chat_exporter/html/component/component_button.html similarity index 61% rename from chat_exporter/chat_exporter_html/component/component_button.html rename to chat_exporter/html/component/component_button.html index 94d7f32..9197925 100644 --- a/chat_exporter/chat_exporter_html/component/component_button.html +++ b/chat_exporter/html/component/component_button.html @@ -1,4 +1,4 @@ -
+
{{EMOJI}}{{LABEL}}{{ICON}} diff --git a/chat_exporter/html/component/component_menu.html b/chat_exporter/html/component/component_menu.html new file mode 100644 index 0000000..91b3d30 --- /dev/null +++ b/chat_exporter/html/component/component_menu.html @@ -0,0 +1,6 @@ +
+ + +
\ No newline at end of file diff --git a/chat_exporter/html/component/component_menu_options.html b/chat_exporter/html/component/component_menu_options.html new file mode 100644 index 0000000..0489d97 --- /dev/null +++ b/chat_exporter/html/component/component_menu_options.html @@ -0,0 +1,5 @@ + + {{TITLE}} +
+ {{DESCRIPTION}} +
\ No newline at end of file diff --git a/chat_exporter/html/component/component_menu_options_emoji.html b/chat_exporter/html/component/component_menu_options_emoji.html new file mode 100644 index 0000000..d82eecd --- /dev/null +++ b/chat_exporter/html/component/component_menu_options_emoji.html @@ -0,0 +1,12 @@ + diff --git a/chat_exporter/chat_exporter_html/embed/author.html b/chat_exporter/html/embed/author.html similarity index 100% rename from chat_exporter/chat_exporter_html/embed/author.html rename to chat_exporter/html/embed/author.html diff --git a/chat_exporter/chat_exporter_html/embed/author_icon.html b/chat_exporter/html/embed/author_icon.html similarity index 100% rename from chat_exporter/chat_exporter_html/embed/author_icon.html rename to chat_exporter/html/embed/author_icon.html diff --git a/chat_exporter/chat_exporter_html/embed/body.html b/chat_exporter/html/embed/body.html similarity index 100% rename from chat_exporter/chat_exporter_html/embed/body.html rename to chat_exporter/html/embed/body.html diff --git a/chat_exporter/chat_exporter_html/embed/description.html b/chat_exporter/html/embed/description.html similarity index 100% rename from chat_exporter/chat_exporter_html/embed/description.html rename to chat_exporter/html/embed/description.html diff --git a/chat_exporter/chat_exporter_html/embed/field-inline.html b/chat_exporter/html/embed/field-inline.html similarity index 100% rename from chat_exporter/chat_exporter_html/embed/field-inline.html rename to chat_exporter/html/embed/field-inline.html diff --git a/chat_exporter/chat_exporter_html/embed/field.html b/chat_exporter/html/embed/field.html similarity index 100% rename from chat_exporter/chat_exporter_html/embed/field.html rename to chat_exporter/html/embed/field.html diff --git a/chat_exporter/chat_exporter_html/embed/footer.html b/chat_exporter/html/embed/footer.html similarity index 100% rename from chat_exporter/chat_exporter_html/embed/footer.html rename to chat_exporter/html/embed/footer.html diff --git a/chat_exporter/chat_exporter_html/embed/footer_image.html b/chat_exporter/html/embed/footer_image.html similarity index 100% rename from chat_exporter/chat_exporter_html/embed/footer_image.html rename to chat_exporter/html/embed/footer_image.html diff --git a/chat_exporter/chat_exporter_html/embed/image.html b/chat_exporter/html/embed/image.html similarity index 100% rename from chat_exporter/chat_exporter_html/embed/image.html rename to chat_exporter/html/embed/image.html diff --git a/chat_exporter/chat_exporter_html/embed/thumbnail.html b/chat_exporter/html/embed/thumbnail.html similarity index 100% rename from chat_exporter/chat_exporter_html/embed/thumbnail.html rename to chat_exporter/html/embed/thumbnail.html diff --git a/chat_exporter/chat_exporter_html/embed/title.html b/chat_exporter/html/embed/title.html similarity index 100% rename from chat_exporter/chat_exporter_html/embed/title.html rename to chat_exporter/html/embed/title.html diff --git a/chat_exporter/chat_exporter_html/message/bot-tag.html b/chat_exporter/html/message/bot-tag.html similarity index 100% rename from chat_exporter/chat_exporter_html/message/bot-tag.html rename to chat_exporter/html/message/bot-tag.html diff --git a/chat_exporter/chat_exporter_html/message/content.html b/chat_exporter/html/message/content.html similarity index 100% rename from chat_exporter/chat_exporter_html/message/content.html rename to chat_exporter/html/message/content.html diff --git a/chat_exporter/chat_exporter_html/message/end.html b/chat_exporter/html/message/end.html similarity index 100% rename from chat_exporter/chat_exporter_html/message/end.html rename to chat_exporter/html/message/end.html diff --git a/chat_exporter/chat_exporter_html/message/message.html b/chat_exporter/html/message/message.html similarity index 100% rename from chat_exporter/chat_exporter_html/message/message.html rename to chat_exporter/html/message/message.html diff --git a/chat_exporter/chat_exporter_html/message/pin.html b/chat_exporter/html/message/pin.html similarity index 100% rename from chat_exporter/chat_exporter_html/message/pin.html rename to chat_exporter/html/message/pin.html diff --git a/chat_exporter/chat_exporter_html/message/reference.html b/chat_exporter/html/message/reference.html similarity index 85% rename from chat_exporter/chat_exporter_html/message/reference.html rename to chat_exporter/html/message/reference.html index 0695b52..bc5b1b9 100644 --- a/chat_exporter/chat_exporter_html/message/reference.html +++ b/chat_exporter/html/message/reference.html @@ -1,4 +1,4 @@ -
+
Avatar diff --git a/chat_exporter/chat_exporter_html/message/reference_unknown.html b/chat_exporter/html/message/reference_unknown.html similarity index 62% rename from chat_exporter/chat_exporter_html/message/reference_unknown.html rename to chat_exporter/html/message/reference_unknown.html index 48295d3..be43df4 100644 --- a/chat_exporter/chat_exporter_html/message/reference_unknown.html +++ b/chat_exporter/html/message/reference_unknown.html @@ -1,4 +1,4 @@ -
+
Original message was deleted. diff --git a/chat_exporter/chat_exporter_html/message/start.html b/chat_exporter/html/message/start.html similarity index 100% rename from chat_exporter/chat_exporter_html/message/start.html rename to chat_exporter/html/message/start.html diff --git a/chat_exporter/chat_exporter_html/message/thread.html b/chat_exporter/html/message/thread.html similarity index 100% rename from chat_exporter/chat_exporter_html/message/thread.html rename to chat_exporter/html/message/thread.html diff --git a/chat_exporter/chat_exporter_html/reaction/custom_emoji.html b/chat_exporter/html/reaction/custom_emoji.html similarity index 100% rename from chat_exporter/chat_exporter_html/reaction/custom_emoji.html rename to chat_exporter/html/reaction/custom_emoji.html diff --git a/chat_exporter/chat_exporter_html/reaction/emoji.html b/chat_exporter/html/reaction/emoji.html similarity index 100% rename from chat_exporter/chat_exporter_html/reaction/emoji.html rename to chat_exporter/html/reaction/emoji.html diff --git a/chat_exporter/parse/__init__.py b/chat_exporter/parse/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/chat_exporter/parse_markdown.py b/chat_exporter/parse/markdown.py similarity index 99% rename from chat_exporter/parse_markdown.py rename to chat_exporter/parse/markdown.py index 1f932f8..a0fa164 100644 --- a/chat_exporter/parse_markdown.py +++ b/chat_exporter/parse/markdown.py @@ -1,5 +1,5 @@ import re -from chat_exporter.emoji_convert import convert_emoji +from chat_exporter.ext.emoji_convert import convert_emoji class ParseMarkdown: diff --git a/chat_exporter/parse_mention.py b/chat_exporter/parse/mention.py similarity index 95% rename from chat_exporter/parse_mention.py rename to chat_exporter/parse/mention.py index a980140..46d568e 100644 --- a/chat_exporter/parse_mention.py +++ b/chat_exporter/parse/mention.py @@ -1,15 +1,4 @@ import re -from typing import Optional - -import discord - - -bot: Optional[discord.Client] = None - - -def pass_bot(_bot): - global bot - bot = _bot class ParseMention: @@ -108,7 +97,7 @@ def member_mention(self): member = None try: - member = self.guild.get_member(member_id) or bot.get_user(member_id) + member = self.guild.get_member(member_id) member_name = member.display_name except AttributeError: member_name = member diff --git a/setup.py b/setup.py index bacca95..8f87926 100644 --- a/setup.py +++ b/setup.py @@ -5,16 +5,16 @@ setup( name="chat_exporter", - version="1.7.3", + version="2.0", author="mahtoid", description="A simple Discord chat exporter for Python Discord bots.", long_description=long_description, url="https://github.com/mahtoid/DiscordChatExporterPy", packages=find_packages(), - package_data={'': [r'chat_exporter/chat_exporter_html/*.html']}, + package_data={'': [r'chat_exporter/html/*.html']}, include_package_data=True, license="GPL", - install_requires=["discord.py", "aiohttp", "pytz", "grapheme", "emoji"], + install_requires=["aiohttp", "pytz", "grapheme", "emoji"], classifiers=[ "Programming Language :: Python :: 3", "Operating System :: OS Independent",