Skip to content

Commit b15a6ec

Browse files
authored
Merge pull request #143 from abilash-dev/master
Feature upgrade: DiscordChatExporterPy 2.9.1
2 parents bd97f10 + 6e99583 commit b15a6ec

File tree

10 files changed

+173
-18
lines changed

10 files changed

+173
-18
lines changed

README.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -362,6 +362,50 @@ async def save(ctx: commands.Context):
362362

363363
await ctx.send(file=transcript_file)
364364

365+
```
366+
</details>
367+
368+
<details><summary>AttachmentToWebhookHandler</summary>
369+
370+
Assuming you want to store your attachments in a discord channel using webhook, you can use the `AttachmentToWebhookHandler`.
371+
Please note that discord recent changes regarding content links will result in the attachments links being broken
372+
after 24 hours. While this is therefor not a recommended way to store your attachments, it should give you a good
373+
idea how to perform asynchronous storing of the attachments.
374+
375+
```python
376+
import io
377+
import discord
378+
from discord.ext import commands
379+
import chat_exporter
380+
from chat_exporter import AttachmentToWebhookHandler
381+
382+
...
383+
384+
# Establish the webhook handler
385+
webhook_handler = AttachmentToWebhookHandler(
386+
webhook_link="https://discord.com/api/webhooks/....",
387+
)
388+
389+
@bot.command()
390+
async def save(ctx: commands.Context):
391+
transcript = await chat_exporter.export(
392+
ctx.channel,
393+
attachment_handler=webhook_handler,
394+
)
395+
396+
if transcript is None:
397+
return
398+
399+
# Due to Discord webhook file size limits (8MB),
400+
# Attachments larger than 8MB are not attached directly.
401+
# Instead, it stores a placeholder image saying "Attachment size is too high".
402+
transcript_file = discord.File(
403+
io.BytesIO(transcript.encode()),
404+
filename=f"transcript-{ctx.channel.name}.html",
405+
)
406+
407+
await ctx.send(file=transcript_file)
408+
365409
```
366410
</details>
367411
</ol>

chat_exporter/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
quick_export,
55
AttachmentHandler,
66
AttachmentToLocalFileHostHandler,
7+
AttachmentToWebhookHandler,
78
AttachmentToDiscordChannelHandler)
89

910
__version__ = "2.9.0"
@@ -14,5 +15,6 @@
1415
quick_export,
1516
AttachmentHandler,
1617
AttachmentToLocalFileHostHandler,
18+
AttachmentToWebhookHandler,
1719
AttachmentToDiscordChannelHandler,
1820
)

chat_exporter/chat_exporter.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
from chat_exporter.construct.transcript import Transcript
66
from chat_exporter.ext.discord_import import discord
7-
from chat_exporter.construct.attachment_handler import AttachmentHandler, AttachmentToLocalFileHostHandler, AttachmentToDiscordChannelHandler
7+
from chat_exporter.construct.attachment_handler import AttachmentHandler, AttachmentToLocalFileHostHandler, AttachmentToDiscordChannelHandler, AttachmentToWebhookHandler
88

99

1010
async def quick_export(

chat_exporter/construct/attachment_handler.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
from typing import Union
55
import urllib.parse
66

7+
import asyncio
8+
import os
79

810
import aiohttp
911
from chat_exporter.ext.discord_import import discord
@@ -68,3 +70,37 @@ async def process_asset(self, attachment: discord.Attachment) -> discord.Attachm
6870
except discord.errors.HTTPException as e:
6971
# discords http errors, including missing permissions
7072
raise e
73+
74+
class AttachmentToWebhookHandler(AttachmentHandler):
75+
"""Save the attachment to a discord channel using webhook and embed the assets in the transcript from there."""
76+
77+
def __init__(self, webhook_link: str) -> None:
78+
self.webhook_link = webhook_link
79+
self.size_limit = 8 * 1024 * 1024 # 8 MB = 8 * 1024 KB * 1024 B
80+
self.placeholder_path = os.path.join(os.path.dirname(__file__), "too_large.png")
81+
82+
async def process_asset(self, attachment: discord.Attachment) -> discord.Attachment:
83+
"""Implement this to process the asset and return a url to the stored attachment.
84+
:param attachment: discord.Attachment
85+
:return: str"""
86+
try:
87+
if attachment.size > self.size_limit:
88+
file = discord.File(self.placeholder_path, filename="too_large.png")
89+
else:
90+
file = await attachment.to_file()
91+
92+
async with aiohttp.ClientSession() as session:
93+
webhook = discord.Webhook.from_url(self.webhook_link, session=session)
94+
for i in range(3):
95+
try:
96+
message = await webhook.send(file=file, wait=True)
97+
break
98+
except aiohttp.ClientConnectionError:
99+
print(f"Retry {i+1}/3 | Error - Webhook connection failed.")
100+
await asyncio.sleep(3) # to prevent frequent retries on connection error
101+
102+
except discord.errors.HTTPException as e:
103+
# discords http errors, including missing permissions
104+
raise e
105+
else:
106+
return message.attachments[0]

chat_exporter/construct/message.py

Lines changed: 69 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
message_content,
2525
message_reference,
2626
message_reference_unknown,
27+
message_reference_forwarded,
2728
message_interaction,
2829
img_attachment,
2930
start_message,
@@ -84,6 +85,14 @@ def __init__(
8485

8586
self.message_created_at, self.message_edited_at = self.set_time()
8687
self.meta_data = meta_data
88+
self.forwarded = False
89+
90+
def get_message_snapshots(self):
91+
if hasattr(self.message, "message_snapshots"):
92+
return self.message.message_snapshots
93+
elif hasattr(self.message, "snapshots"):
94+
return self.message.snapshots
95+
return []
8796

8897
async def construct_message(
8998
self,
@@ -149,17 +158,28 @@ async def build_meta_data(self):
149158
]
150159

151160
async def build_content(self):
152-
if not self.message.content:
161+
if not self.message.content and not self.get_message_snapshots():
153162
self.message.content = ""
154163
return
155164

156165
if self.message_edited_at:
157166
self.message_edited_at = _set_edit_at(self.message_edited_at)
158167

159-
self.message.content = html.escape(self.message.content)
168+
snapshots = self.get_message_snapshots()
169+
if snapshots:
170+
combined = f"{self.message.content} {' '.join(s.content for s in snapshots if hasattr(s, 'content'))}"
171+
self.forwarded = True
172+
else:
173+
combined = self.message.content
174+
175+
combined = html.escape(combined or "")
176+
177+
if self.forwarded:
178+
combined = f'<div class="quote">{combined}</div>'
179+
160180
self.message.content = await fill_out(self.guild, message_content, [
161-
("MESSAGE_CONTENT", self.message.content, PARSE_MODE_MARKDOWN),
162-
("EDIT", self.message_edited_at, PARSE_MODE_NONE)
181+
("MESSAGE_CONTENT", combined, PARSE_MODE_MARKDOWN),
182+
("EDIT", self.message_edited_at, PARSE_MODE_NONE),
163183
])
164184

165185
async def build_reference(self):
@@ -174,6 +194,9 @@ async def build_reference(self):
174194
message: discord.Message = await self.message.channel.fetch_message(self.message.reference.message_id)
175195
except (discord.NotFound, discord.HTTPException) as e:
176196
self.message.reference = ""
197+
if self.forwarded:
198+
self.message.reference = message_reference_forwarded
199+
return
177200
if isinstance(e, discord.NotFound):
178201
self.message.reference = message_reference_unknown
179202
return
@@ -251,13 +274,30 @@ async def build_interaction(self):
251274
])
252275

253276
async def build_sticker(self):
254-
if not self.message.stickers or not hasattr(self.message.stickers[0], "url"):
277+
sticker = None
278+
sticker_image_url = None
279+
280+
if self.message.stickers and hasattr(self.message.stickers[0], "url"):
281+
sticker_image_url = self.message.stickers[0].url
282+
if not sticker_image_url:
283+
for snapshot in self.get_message_snapshots():
284+
if hasattr(snapshot, "stickers") and snapshot.stickers and hasattr(snapshot.stickers[0], "url"):
285+
sticker_image_url = snapshot.stickers[0].url
286+
self.message.reference = message_reference_forwarded
287+
break
288+
289+
if not sticker_image_url:
255290
return
256-
257-
sticker_image_url = self.message.stickers[0].url
291+
258292

259293
if sticker_image_url.endswith(".json"):
260-
sticker = await self.message.stickers[0].fetch()
294+
try:
295+
sticker = await self.message.stickers[0].fetch()
296+
except:
297+
for snapshot in self.get_message_snapshots():
298+
if hasattr(snapshot, "stickers") and snapshot.stickers and hasattr(snapshot.stickers[0], "url"):
299+
sticker = await snapshot.stickers[0].fetch()
300+
break
261301
sticker_image_url = (
262302
f"https://cdn.jsdelivr.net/gh/mahtoid/DiscordUtils@master/stickers/{sticker.pack_id}/{sticker.id}.gif"
263303
)
@@ -271,14 +311,34 @@ async def build_assets(self):
271311
for e in self.message.embeds:
272312
self.embeds += await Embed(e, self.guild).flow()
273313

314+
for snapshot in self.get_message_snapshots():
315+
if hasattr(snapshot, "embeds"):
316+
for se in snapshot.embeds:
317+
self.embeds += await Embed(se, self.guild).flow()
318+
self.message.reference = message_reference_forwarded
319+
274320
for a in self.message.attachments:
275321
if self.attachment_handler and isinstance(self.attachment_handler, AttachmentHandler):
276322
a = await self.attachment_handler.process_asset(a)
277323
self.attachments += await Attachment(a, self.guild).flow()
324+
325+
for snapshot in self.get_message_snapshots():
326+
if hasattr(snapshot, "attachments"):
327+
for sa in snapshot.attachments:
328+
if self.attachment_handler:
329+
sa = await self.attachment_handler.process_asset(sa)
330+
self.attachments += await Attachment(sa,self.guild).flow()
331+
self.message.reference = message_reference_forwarded
278332

279333
for c in self.message.components:
280334
self.components += await Component(c, self.guild).flow()
281335

336+
for snapshot in self.get_message_snapshots():
337+
if hasattr(snapshot, "components"):
338+
for ac in snapshot.components:
339+
self.components += await Component(ac,self.guild).flow()
340+
self.message.reference = message_reference_forwarded
341+
282342
for r in self.message.reactions:
283343
self.reactions += await Reaction(r, self.guild).flow()
284344

@@ -497,4 +557,4 @@ async def gather_messages(
497557
previous_message = message
498558

499559
message_html += "</div>"
500-
return message_html, meta_data
560+
return message_html, meta_data
1.2 MB
Loading

chat_exporter/construct/transcript.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import datetime
21
import html
32
import traceback
3+
from datetime import datetime
44

55
import re
66
from typing import List, Optional
@@ -32,8 +32,8 @@ def __init__(
3232
pytz_timezone,
3333
military_time: bool,
3434
fancy_times: bool,
35-
before: Optional[datetime.datetime],
36-
after: Optional[datetime.datetime],
35+
before: Optional[datetime],
36+
after: Optional[datetime],
3737
support_dev: bool,
3838
bot: Optional[discord.Client],
3939
attachment_handler: Optional[AttachmentHandler],
@@ -79,9 +79,9 @@ async def export_transcript(self, message_html: str, meta_data: str):
7979

8080
timezone = pytz.timezone(self.pytz_timezone)
8181
if self.military_time:
82-
time_now = datetime.datetime.now(timezone).strftime("%e %B %Y at %H:%M:%S (%Z)")
82+
time_now = datetime.now(timezone).strftime("%e %B %Y at %H:%M:%S (%Z)")
8383
else:
84-
time_now = datetime.datetime.now(timezone).strftime("%e %B %Y at %I:%M:%S %p (%Z)")
84+
time_now = datetime.now(timezone).strftime("%e %B %Y at %I:%M:%S %p (%Z)")
8585

8686
meta_data_html: str = ""
8787
for data in meta_data:

chat_exporter/ext/html_generator.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ def read_file(filename):
6666
message_thread_remove = read_file(dir_path + "/html/message/thread_remove.html")
6767
message_thread_add = read_file(dir_path + "/html/message/thread_add.html")
6868
message_reference_unknown = read_file(dir_path + "/html/message/reference_unknown.html")
69+
message_reference_forwarded = read_file(dir_path + "/html/message/reference_forwarded.html")
6970
message_body = read_file(dir_path + "/html/message/message.html")
7071
end_message = read_file(dir_path + "/html/message/end.html")
7172
meta_data_temp = read_file(dir_path + "/html/message/meta.html")

chat_exporter/html/base.html

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,18 +14,27 @@
1414
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
1515
<meta name="viewport" content="width=device-width" />
1616
<meta name="title" content="{{SERVER_NAME_SAFE}} - {{CHANNEL_NAME_SAFE}}">
17-
<meta name="description" content="Transcript of channel {{CHANNEL_NAME_SAFE}} ({{CHANNEL_ID}}) from {{SERVER_NAME_SAFE}} ({{GUILD_ID}}) with {{MESSAGE_COUNT}} messages. This transcript was generated on {{DATE_TIME}}.">
17+
<meta name="description" content="Channel: {{CHANNEL_NAME_SAFE}} ({{CHANNEL_ID}})
18+
Server: {{SERVER_NAME_SAFE}} ({{GUILD_ID}})
19+
Message Count: {{MESSAGE_COUNT}} Messages
20+
Generated on: {{DATE_TIME}}"/>
1821
<meta name="theme-color" content="#638dfc" />
1922

2023
<!-- Open Graph / Facebook -->
2124
<meta property="og:type" content="website" />
2225
<meta property="og:title" content="{{SERVER_NAME_SAFE}} - {{CHANNEL_NAME_SAFE}}" />
23-
<meta property="og:description" content="Transcript of channel {{CHANNEL_NAME_SAFE}} ({{CHANNEL_ID}}) from {{SERVER_NAME_SAFE}} ({{GUILD_ID}}) with {{MESSAGE_COUNT}} messages. This transcript was generated on {{DATE_TIME}}." />
26+
<meta property="og:description" content="Channel: {{CHANNEL_NAME_SAFE}} ({{CHANNEL_ID}})
27+
Server: {{SERVER_NAME_SAFE}} ({{GUILD_ID}})
28+
Message Count: {{MESSAGE_COUNT}} Messages
29+
Generated on: {{DATE_TIME}}"/>
2430

2531
<!-- Twitter -->
2632
<meta name="twitter:card" content="summary" />
2733
<meta name='twitter:title' content="{{SERVER_NAME_SAFE}} - {{CHANNEL_NAME_SAFE}}" />
28-
<meta name='twitter:description' content="Transcript of channel {{CHANNEL_NAME_SAFE}} ({{CHANNEL_ID}}) from {{SERVER_NAME_SAFE}} ({{GUILD_ID}}) with {{MESSAGE_COUNT}} messages. This transcript was generated on {{DATE_TIME}}." />
34+
<meta name='twitter:description' content="Channel: {{CHANNEL_NAME_SAFE}} ({{CHANNEL_ID}})
35+
Server: {{SERVER_NAME_SAFE}} ({{GUILD_ID}})
36+
Message Count: {{MESSAGE_COUNT}} Messages
37+
Generated on: {{DATE_TIME}}"/>
2938

3039

3140
<style>
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<div class="chatlog__followup">
2+
<span class="chatlog__reference-forwarded"><em>Forwarded message.</em></span>
3+
</div>

0 commit comments

Comments
 (0)