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}}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-{{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}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+{{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 @@
-