mirror of
https://github.com/Tyrrrz/DiscordChatExporter.git
synced 2026-07-08 15:14:37 +02:00
Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| de773467b6 | |||
| 92766b580b | |||
| abf7498667 | |||
| ae42554621 | |||
| b58e67776c | |||
| 7fc6e0cfe0 | |||
| 35264e8a12 | |||
| 2ab6773c17 | |||
| 650c55bbd1 | |||
| e1726683f8 | |||
| 70a1755c50 | |||
| 524b30b09b | |||
| d39fc212b6 | |||
| 95cd6cb50c | |||
| 79a9ac0462 | |||
| 5b5a53dbf4 | |||
| 2b0fa4213b | |||
| fd9d367bad | |||
| dbc3209160 | |||
| 3bcde0c534 | |||
| 903882e35b | |||
| 3c6d9d74a4 | |||
| 1e53f50ca3 |
@@ -1,27 +1,31 @@
|
|||||||
name: "\U0001F41E Bug report"
|
name: 🐞 Bug report
|
||||||
description: Report broken functionality.
|
description: Report broken functionality.
|
||||||
labels: [bug]
|
labels: [bug]
|
||||||
|
|
||||||
body:
|
body:
|
||||||
- type: markdown
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
value: |
|
value: |
|
||||||
Important:
|
🧐 **Guidelines:**
|
||||||
|
|
||||||
- Please check existing issues (both opened and closed) to ensure that this bug hasn't been reported before.
|
- Search through [existing issues](https://github.com/Tyrrrz/DiscordChatExporter/issues?q=is%3Aissue) first to ensure that this bug has not been reported before.
|
||||||
- Refer to the [contribution guidelines](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/Contributing.md) for info on how to file a good issue.
|
- Write a descriptive title for your issue. Avoid generic or vague titles such as "Something's not working" or "A couple of problems".
|
||||||
|
- Keep your issue focused on one single problem. If you have multiple bug reports, please create separate issues for each of them.
|
||||||
|
- Provide as much context as possible in the details section. Include screenshots, screen recordings, links, references, or anything else you may consider relevant.
|
||||||
- If you want to ask a question instead of reporting a bug, please use [discussions](https://github.com/Tyrrrz/DiscordChatExporter/discussions/new) instead.
|
- If you want to ask a question instead of reporting a bug, please use [discussions](https://github.com/Tyrrrz/DiscordChatExporter/discussions/new) instead.
|
||||||
|
|
||||||
- type: input
|
- type: input
|
||||||
attributes:
|
attributes:
|
||||||
label: Version
|
label: Version
|
||||||
description: "Which version(s) of DiscordChatExporter does this bug affect?"
|
description: Which version of DiscordChatExporter does this bug affect?
|
||||||
|
placeholder: ver X.Y.Z
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
- type: dropdown
|
- type: dropdown
|
||||||
attributes:
|
attributes:
|
||||||
label: Flavor
|
label: Flavor
|
||||||
description: "Which flavor(s) of DiscordChatExporter does this bug affect?"
|
description: Does this bug affect the GUI version, the CLI version, or both?
|
||||||
multiple: true
|
multiple: true
|
||||||
options:
|
options:
|
||||||
- GUI (Graphical User Interface)
|
- GUI (Graphical User Interface)
|
||||||
@@ -32,7 +36,7 @@ body:
|
|||||||
- type: dropdown
|
- type: dropdown
|
||||||
attributes:
|
attributes:
|
||||||
label: Export format
|
label: Export format
|
||||||
description: "Which export format(s) does this bug affect? If this bug is not related to any specific export format, leave this blank."
|
description: Does this bug affect specific export format(s)? If not applicable, leave this field blank.
|
||||||
multiple: true
|
multiple: true
|
||||||
options:
|
options:
|
||||||
- HTML
|
- HTML
|
||||||
@@ -43,13 +47,18 @@ body:
|
|||||||
- type: textarea
|
- type: textarea
|
||||||
attributes:
|
attributes:
|
||||||
label: Details
|
label: Details
|
||||||
description: "Clear and thorough explanation of the bug. If relevant, include screenshots or screen recordings."
|
description: Clear and thorough explanation of the bug.
|
||||||
|
placeholder: I was doing X expecting Y to happen, but Z happened instead.
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
- type: textarea
|
- type: textarea
|
||||||
attributes:
|
attributes:
|
||||||
label: Steps to reproduce
|
label: Steps to reproduce
|
||||||
description: "Minimum steps required to reproduce the bug."
|
description: Minimum steps required to reproduce the bug.
|
||||||
|
placeholder: |
|
||||||
|
- Step 1
|
||||||
|
- Step 2
|
||||||
|
- Step 3
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
blank_issues_enabled: false
|
blank_issues_enabled: false
|
||||||
contact_links:
|
contact_links:
|
||||||
- name: "\U0001F4D6 Read the wiki"
|
- name: 📖 Wiki
|
||||||
url: https://github.com/Tyrrrz/DiscordChatExporter/wiki
|
url: https://github.com/Tyrrrz/DiscordChatExporter/wiki
|
||||||
about: Find answers to most common questions.
|
about: Find usage guides and frequently asked questions.
|
||||||
- name: "\U0001F5E8 Ask a question"
|
- name: 🗨 Discussions
|
||||||
url: https://github.com/Tyrrrz/DiscordChatExporter/discussions/new
|
url: https://github.com/Tyrrrz/DiscordChatExporter/discussions/new
|
||||||
about: Please ask and answer questions here.
|
about: Ask and answer questions.
|
||||||
@@ -1,20 +1,23 @@
|
|||||||
name: "\U00002728 Feature request"
|
name: ✨ Feature request
|
||||||
description: Request a new feature.
|
description: Request a new feature.
|
||||||
labels: [enhancement]
|
labels: [enhancement]
|
||||||
|
|
||||||
body:
|
body:
|
||||||
- type: markdown
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
value: |
|
value: |
|
||||||
Important:
|
🧐 **Guidelines:**
|
||||||
|
|
||||||
- Please check existing issues (both opened and closed) to ensure that this feature hasn't been requested before.
|
- Search through [existing issues](https://github.com/Tyrrrz/DiscordChatExporter/issues?q=is%3Aissue) first to ensure that this feature has not been requested before.
|
||||||
- Refer to the [contribution guidelines](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/Contributing.md) for info on how to file a good issue.
|
- Write a descriptive title for your issue. Avoid generic or vague titles such as "Some suggestions" or "Ideas for improvement".
|
||||||
|
- Keep your issue focused on one single problem. If you have multiple feature requests, please create separate issues for each of them.
|
||||||
|
- Provide as much context as possible in the details section. Include screenshots, screen recordings, links, references, or anything else you may consider relevant.
|
||||||
- If you want to ask a question instead of requesting a feature, please use [discussions](https://github.com/Tyrrrz/DiscordChatExporter/discussions/new) instead.
|
- If you want to ask a question instead of requesting a feature, please use [discussions](https://github.com/Tyrrrz/DiscordChatExporter/discussions/new) instead.
|
||||||
|
|
||||||
- type: dropdown
|
- type: dropdown
|
||||||
attributes:
|
attributes:
|
||||||
label: Flavor
|
label: Flavor
|
||||||
description: "Which flavor(s) of DiscordChatExporter does this feature request apply to? If this feature is not related to any specific flavor, leave this blank."
|
description: Is this feature request related to specific flavor(s) of DiscordChatExporter? If not applicable, leave this field blank.
|
||||||
multiple: true
|
multiple: true
|
||||||
options:
|
options:
|
||||||
- GUI (Graphical User Interface)
|
- GUI (Graphical User Interface)
|
||||||
@@ -23,7 +26,7 @@ body:
|
|||||||
- type: dropdown
|
- type: dropdown
|
||||||
attributes:
|
attributes:
|
||||||
label: Export format
|
label: Export format
|
||||||
description: "Which export format(s) does this feature request apply to? If this feature is not related to any specific export format, leave this blank."
|
description: Is this feature request related to specific export format(s)? If not applicable, leave this field blank.
|
||||||
multiple: true
|
multiple: true
|
||||||
options:
|
options:
|
||||||
- HTML
|
- HTML
|
||||||
@@ -34,6 +37,6 @@ body:
|
|||||||
- type: textarea
|
- type: textarea
|
||||||
attributes:
|
attributes:
|
||||||
label: Details
|
label: Details
|
||||||
description: "Clear and thorough explanation of the feature you have in mind. If relevant, include screenshots or screen recordings."
|
description: Clear and thorough explanation of the feature you have in mind.
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<!--
|
||||||
|
|
||||||
|
**Important:**
|
||||||
|
|
||||||
|
Please make sure that there is an existing issue that describes the problem solved by your pull request. If there isn't one, consider creating it first.
|
||||||
|
An open issue offers a good place to iron out requirements, discuss possible solutions, and ask questions.
|
||||||
|
|
||||||
|
Remember to also:
|
||||||
|
|
||||||
|
- Keep your pull request focused and as small as possible. If you want to contribute multiple unrelated changes, please create separate pull requests for them.
|
||||||
|
- Follow the coding style and conventions already established by the project. When in doubt about which style to use, ask in the comments to your pull request.
|
||||||
|
- If you want to start a discussion regarding a specific change you've made, add a review comment to your own code. This can be used to highlight something important or to seek further input from others.
|
||||||
|
|
||||||
|
-->
|
||||||
|
|
||||||
|
<!-- Please specify the issue addressed by this pull request -->
|
||||||
|
Closes #ISSUE_NUMBER
|
||||||
+25
-11
@@ -1,8 +1,22 @@
|
|||||||
|
### v2.29 (19-Jul-2021)
|
||||||
|
|
||||||
|
- Added support for message filtering, which is accessible as an additional export parameter in the GUI and as the `--filter` option in the CLI. Message filters use a special syntax similar to Discord's search query syntax to allow specifying which messages should be included in the export. For example, you can limit the export to only include messages that contain image attachments (`has:image`) or messages from a certain user (`from:Tyrrrz`) or messages containing certain words (`hello world`), etc. You can also combine filters together (`hi mentions:Tyrrrz`, `from:Tyrrrz | from:96-LB`, `(mentions:Tyrrrz | from:Tyrrrz) has:image`) or negate them (`-has:link`). More thorough documentation coming soon. (Thanks [@Lucas LaBuff](https://github.com/96-LB))
|
||||||
|
- Added support for rendering mentions of voice channels, in addition to text channels.
|
||||||
|
- Added support for rendering unix timestamp markers. Currently, style markers are ignored and timestamps are always rendered with the format configured in settings (or provided via `--date-format` option).
|
||||||
|
- Updated in-app instructions for obtaining a user token. (Thanks [@Yudi](https://github.com/Yudi) and [@virtuallyvlad](https://github.com/virtuallyvlad))
|
||||||
|
- [HTML] Improved visual appearance of the reference symbol (Г-shaped line in replies) to make it more closely resemble Discord's styling.
|
||||||
|
- [HTML] Added more specific fallback text used when rendering referenced messages that contain no text content but have attachments or embeds.
|
||||||
|
- [HTML] Fixed an issue where a broken embed author icon occupied visual space, ruining the embed layout. (Thanks [@Cliff J.](https://github.com/CliffJameston))
|
||||||
|
- [GUI] Improved handling for fatal errors that may occur when attempting to export a channel. These errors are now displayed in a popup, allowing the user to try again instead of having the application crash completely.
|
||||||
|
- [CLI] Fixed an issue where an attempt to export a channel resulted in `NotSupportedException` when the default encoding was set to `UTF8`.
|
||||||
|
- [CLI] Fixed an issue where providing an incorrectly formatted partition limit via `-p|--partition` option resulted in a non-descriptive and unhelpful error.
|
||||||
|
- Improved "Requested resource does not exist" error message by including the ID of the requested resource.
|
||||||
|
|
||||||
### v2.28 (19-Jun-2021)
|
### v2.28 (19-Jun-2021)
|
||||||
|
|
||||||
- [HTML] Added special casing for YouTube video embeds to render them directly using an embedded player. (Thanks [@quentinmay](https://github.com/quentinmay))
|
- [HTML] Added special casing for YouTube video embeds to render them directly using an embedded player. (Thanks [@quentinmay](https://github.com/quentinmay))
|
||||||
- [HTML] Added support for rendering standard emoji by code. Such emoji may sometimes be found in messages sent by bots or through webhooks. (Thanks [@CanePlayz](https://github.com/CanePlayz) and [@Lucas LaBuff](https://github.com/96-LB))
|
- [HTML] Added support for rendering standard emoji by code. Such emoji may sometimes be found in messages sent by bots or through webhooks. (Thanks [@CanePlayz](https://github.com/CanePlayz) and [@Lucas LaBuff](https://github.com/96-LB))
|
||||||
- [HTML] Changed tooltips on standard emoji to show emoji code instead of their raw string representation. (Thanks [@CanePlayz](https://github.com/CanePlayz) and [@Lucas LaBuff](https://github.com/96-LB))
|
- [HTML] Changed tooltips on standard emoji to show emoji code instead of their raw string representations. (Thanks [@CanePlayz](https://github.com/CanePlayz) and [@Lucas LaBuff](https://github.com/96-LB))
|
||||||
- [HTML] Added tooltips on individual messages to show when those messages were sent.
|
- [HTML] Added tooltips on individual messages to show when those messages were sent.
|
||||||
- [HTML] Updated colors and minor styling elements to match Discord's new style direction after rebranding.
|
- [HTML] Updated colors and minor styling elements to match Discord's new style direction after rebranding.
|
||||||
- [HTML] Updated Twemoji URLs to use SVG image variants for better rendering quality.
|
- [HTML] Updated Twemoji URLs to use SVG image variants for better rendering quality.
|
||||||
@@ -96,7 +110,7 @@
|
|||||||
|
|
||||||
### v2.21 (18-Jul-2020)
|
### v2.21 (18-Jul-2020)
|
||||||
|
|
||||||
- Added a new option that enables self-contained exports for all output formats. You can turn it on in the export setup dialog in GUI or using the `--media` option in CLI. When using this, the application will additionally download any media content directly referenced from the exported file instead of linking back to Discord CDN. The files which are downloaded include: guild icons, user avatars, attachments, embedded images, reaction emojis. Note that only files which are actually referenced by the export are downloaded, which means that, for example, user avatars will not be downloaded when using plain text export format. This option is not meant to enable complete offline viewing for HTML exports, but rather to make it easier to archive media content that may eventually get deleted from Discord servers. Also keep in mind that this option may make the export drastically slower and the total file size larger.
|
- Added a new option that enables self-contained exports for all output formats. You can turn it on in the export setup dialog in GUI or using the `--media` option in CLI. When using this, the application will additionally download any media content directly referenced from the exported file instead of linking back to Discord CDN. The files which are downloaded include: guild icons, user avatars, attachments, embedded images, reaction emoji. Note that only files which are actually referenced by the export are downloaded, which means that, for example, user avatars will not be downloaded when using plain text export format. This option is not meant to enable complete offline viewing for HTML exports, but rather to make it easier to archive media content that may eventually get deleted from Discord servers. Also keep in mind that this option may make the export drastically slower and the total file size larger.
|
||||||
- Changed "discordapp.com" to "discord.com" where applicable as Discord is migrating to a new domain. CDN will remain on "cdn.discordapp.com" for the foreseeable future.
|
- Changed "discordapp.com" to "discord.com" where applicable as Discord is migrating to a new domain. CDN will remain on "cdn.discordapp.com" for the foreseeable future.
|
||||||
|
|
||||||
Note that all existing and current HTML exports will likely not render accurately because Discord enabled CORS for their font resources, which prevents them from loading locally. Please refer to [issue #322](https://github.com/Tyrrrz/DiscordChatExporter/issues/322) for discussion on this topic.
|
Note that all existing and current HTML exports will likely not render accurately because Discord enabled CORS for their font resources, which prevents them from loading locally. Please refer to [issue #322](https://github.com/Tyrrrz/DiscordChatExporter/issues/322) for discussion on this topic.
|
||||||
@@ -135,7 +149,7 @@ Note that all existing and current HTML exports will likely not render accuratel
|
|||||||
|
|
||||||
- Fixed an issue where an empty file was produced when exporting a channel with no messages (for specified period). With the new behavior, no file will be created and instead a message will be shown to the user informing of the failure.
|
- Fixed an issue where an empty file was produced when exporting a channel with no messages (for specified period). With the new behavior, no file will be created and instead a message will be shown to the user informing of the failure.
|
||||||
- [HTML/TXT] Added message count to the bottom of the file. This number shows how many messages were exported as part of the current file. Prior to 2.16 this number was present along with the rest of the metadata at the top, then removed due to changes in the exporter, and now brought back at the end of the file instead.
|
- [HTML/TXT] Added message count to the bottom of the file. This number shows how many messages were exported as part of the current file. Prior to 2.16 this number was present along with the rest of the metadata at the top, then removed due to changes in the exporter, and now brought back at the end of the file instead.
|
||||||
- [HTML] Fixed an issue where some emojis were not properly rendered.
|
- [HTML] Fixed an issue where some emoji were not properly rendered.
|
||||||
- [GUI] Added a setting that lets you configure whether to remember the last used token. Previously it was the default behavior, but now you can disable it if you don't want the token persisted on your system.
|
- [GUI] Added a setting that lets you configure whether to remember the last used token. Previously it was the default behavior, but now you can disable it if you don't want the token persisted on your system.
|
||||||
|
|
||||||
### v2.16 (08-Dec-2019)
|
### v2.16 (08-Dec-2019)
|
||||||
@@ -148,7 +162,7 @@ Note that all existing and current HTML exports will likely not render accuratel
|
|||||||
- Fixed various issues that resulted in exceptions during export process.
|
- Fixed various issues that resulted in exceptions during export process.
|
||||||
- [HTML/TXT] Removed message count from the metadata. Due to the fact that the messages are exported as they are streamed, total number of messages is not known ahead of time.
|
- [HTML/TXT] Removed message count from the metadata. Due to the fact that the messages are exported as they are streamed, total number of messages is not known ahead of time.
|
||||||
- [HTML] Changed default color of the embed color pill to match the theme.
|
- [HTML] Changed default color of the embed color pill to match the theme.
|
||||||
- [HTML] Changed emoji parser to be less greedy. As a result it should match fewer character sequences that look like emojis but really aren't, but on the other hand it might miss some actual emojis. This means that some standard emojis (i.e. not custom server emojis) may not look like in Discord. This is a compromise I'm willing to take because detecting emojis in text is really hard to get right and not worth it at all.
|
- [HTML] Changed emoji parser to be less greedy. As a result it should match fewer character sequences that look like emoji but really aren't, but on the other hand it might miss some actual emoji. This means that some standard emoji (i.e. not custom server emoji) may not look like in Discord. This is a compromise I'm willing to take because detecting emoji in text is really hard to get right and not worth it at all.
|
||||||
- [HTML] Some other minor styling adjustments.
|
- [HTML] Some other minor styling adjustments.
|
||||||
- [CSV] Replaced semicolons (;) with (,) as separators.
|
- [CSV] Replaced semicolons (;) with (,) as separators.
|
||||||
|
|
||||||
@@ -189,8 +203,8 @@ Please note that CSV export is most likely not going to receive future updates a
|
|||||||
### v2.12.1 (12-Apr-2019)
|
### v2.12.1 (12-Apr-2019)
|
||||||
|
|
||||||
- [GUI] Fixed an issue where the app crashed when trying to export multiple channels.
|
- [GUI] Fixed an issue where the app crashed when trying to export multiple channels.
|
||||||
- [HTML] Fixed an issue where some of the emojis were rendered via Twemoji while Discord renders them as plain text.
|
- [HTML] Fixed an issue where some of the emoji were rendered via Twemoji while Discord renders them as plain text.
|
||||||
- [HTML] Fixed an issue where emojis weren't jumbo if there were more than one of them.
|
- [HTML] Fixed an issue where emoji weren't jumbo if there were more than one of them.
|
||||||
|
|
||||||
### v2.12 (11-Apr-2019)
|
### v2.12 (11-Apr-2019)
|
||||||
|
|
||||||
@@ -212,15 +226,15 @@ Please note that CSV export is most likely not going to receive future updates a
|
|||||||
|
|
||||||
### v2.10.1 (06-Mar-2019)
|
### v2.10.1 (06-Mar-2019)
|
||||||
|
|
||||||
- [HTML] Fixed an issue where multiple emojis on a single line would get rendered as one emoji.
|
- [HTML] Fixed an issue where multiple emoji on a single line would get rendered as one emoji.
|
||||||
- [HTML] Fixed an issue where an open square bracket character would incorrectly render as an emoji.
|
- [HTML] Fixed an issue where an open square bracket character would incorrectly render as an emoji.
|
||||||
- [HTML] Added alt text to emoji images so that they will be rendered using default font when Twemoji images are not available.
|
- [HTML] Added alt text to emoji images so that they will be rendered using default font when Twemoji images are not available.
|
||||||
|
|
||||||
### v2.10 (03-Mar-2019)
|
### v2.10 (03-Mar-2019)
|
||||||
|
|
||||||
- [HTML] Reworked the entire markdown parsing engine which fixes numerous rendering inconsistencies (over 15 issues fixed).
|
- [HTML] Reworked the entire markdown parsing engine which fixes numerous rendering inconsistencies (over 15 issues fixed).
|
||||||
- [HTML] Added support for animated emojis.
|
- [HTML] Added support for animated emoji.
|
||||||
- [HTML] Standard emojis are now rendered using Twemoji, the same image set that Discord uses.
|
- [HTML] Standard emoji are now rendered using Twemoji, the same image set that Discord uses.
|
||||||
- [HTML] Fixed an issue where video attachments would render incorrectly.
|
- [HTML] Fixed an issue where video attachments would render incorrectly.
|
||||||
- [HTML] Minor fixes in styling.
|
- [HTML] Minor fixes in styling.
|
||||||
- [GUI] Aggregated export progress is now shown in the taskbar.
|
- [GUI] Aggregated export progress is now shown in the taskbar.
|
||||||
@@ -280,7 +294,7 @@ Please note that CSV export is most likely not going to receive future updates a
|
|||||||
- Added support for reactions (HTML only).
|
- Added support for reactions (HTML only).
|
||||||
- Selected date range is now shown in the export (HTML and PlainText).
|
- Selected date range is now shown in the export (HTML and PlainText).
|
||||||
- Added placeholders for guild icons to display while the icons are loading.
|
- Added placeholders for guild icons to display while the icons are loading.
|
||||||
- Added support for jumbo emojis (custom emojis only).
|
- Added support for jumbo emoji (custom emoji only).
|
||||||
- Added basic retry policy for 429 error responses.
|
- Added basic retry policy for 429 error responses.
|
||||||
- Added validation for from/to dates in the UI which prevents choosing invalid date ranges.
|
- Added validation for from/to dates in the UI which prevents choosing invalid date ranges.
|
||||||
- Added an error message when trying to export a deleted channel.
|
- Added an error message when trying to export a deleted channel.
|
||||||
@@ -304,7 +318,7 @@ Please note that CSV export is most likely not going to receive future updates a
|
|||||||
|
|
||||||
- Improved message date filtering, it's now marginally faster.
|
- Improved message date filtering, it's now marginally faster.
|
||||||
- Fixed underscores not recognized as italics in markdown.
|
- Fixed underscores not recognized as italics in markdown.
|
||||||
- Added support for custom emojis.
|
- Added support for custom emoji.
|
||||||
- Added support for user and role mentions.
|
- Added support for user and role mentions.
|
||||||
- Added support for channel mentions.
|
- Added support for channel mentions.
|
||||||
- Fixed text in pre blocks not being wrapped correctly.
|
- Fixed text in pre blocks not being wrapped correctly.
|
||||||
|
|||||||
@@ -1,56 +0,0 @@
|
|||||||
# Contributing
|
|
||||||
|
|
||||||
DiscordChatExporter accepts contributions in the form of issues and pull requests.
|
|
||||||
|
|
||||||
## Creating issues
|
|
||||||
|
|
||||||
If you have a feature suggestion or want to report a bug, you are welcome to create an issue.
|
|
||||||
|
|
||||||
Guidelines:
|
|
||||||
|
|
||||||
- Avoid creating an issue if a similar one already exists. Look through existing open and closed issues first.
|
|
||||||
- Keep your issue focused on one specific problem. If you have multiple suggestions or bug reports, please create separate issues for them.
|
|
||||||
- Provide a descriptive title for your issue. Don't use generic titles like "A couple suggestions" or "Not working".
|
|
||||||
- Provide more context in the body of the issue. If relevant, attach screenshots or screen recordings.
|
|
||||||
- Remain civil and respectful when participating in discussions.
|
|
||||||
|
|
||||||
## Creating pull requests
|
|
||||||
|
|
||||||
If you want to contribute code to the project, you can do so by creating a pull request.
|
|
||||||
|
|
||||||
Guidelines:
|
|
||||||
|
|
||||||
- Make sure that there is an existing issue that describes the problem solved by your pull request. If there isn't one, please create it first.
|
|
||||||
- Add a comment to the issue indicating that you want to submit a pull request for it. This can be a good opportunity to discuss possible solutions, finalize requirements, or ask questions.
|
|
||||||
- Keep your pull request focused and as small as possible. If you want to contribute multiple unrelated changes, please create separate pull requests for them.
|
|
||||||
- Follow the coding style and conventions already established by the project. When in doubt which style to use, ask in comments to your pull request.
|
|
||||||
- If you want to start a discussion regarding a specific change you made, add a review comment to your own code. This can be used to highlight something important or to seek input from others.
|
|
||||||
|
|
||||||
## Building the project locally
|
|
||||||
|
|
||||||
Prerequisites:
|
|
||||||
|
|
||||||
- [.NET 5.0 SDK](https://dotnet.microsoft.com/download/dotnet/5.0)
|
|
||||||
- [.NET 3.1 SDK](https://dotnet.microsoft.com/download/dotnet/3.1) (temporarily as the app still targets .NET 3.1, but requires .NET 5.0 to build)
|
|
||||||
- _(Optional)_ C#/.NET IDE, such as [JetBrains Rider](https://www.jetbrains.com/rider), [VS Code](https://code.visualstudio.com/docs/languages/csharp), or [Visual Studio](https://visualstudio.microsoft.com).
|
|
||||||
|
|
||||||
To build the entire solution run the following command in the root of the repository:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
> dotnet build
|
|
||||||
```
|
|
||||||
|
|
||||||
This will generate runtime artifacts for each project:
|
|
||||||
|
|
||||||
```plaintext
|
|
||||||
./DiscordChatExporter.Gui/bin/[Debug|Release]/[runtime]/*
|
|
||||||
./DiscordChatExporter.Cli/bin/[Debug|Release]/[runtime]/*
|
|
||||||
```
|
|
||||||
|
|
||||||
You can also build and run a specific project directly.
|
|
||||||
To do that, navigate to its directory and use `dotnet run`:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
> cd DiscordChatExporter.Gui
|
|
||||||
> dotnet run
|
|
||||||
```
|
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>netcoreapp3.1</TargetFramework>
|
<TargetFramework>netcoreapp3.1</TargetFramework>
|
||||||
<Version>2.28</Version>
|
<Version>2.29</Version>
|
||||||
<Company>Tyrrrz</Company>
|
<Company>Tyrrrz</Company>
|
||||||
<Copyright>Copyright (c) Alexey Golub</Copyright>
|
<Copyright>Copyright (c) Alexey Golub</Copyright>
|
||||||
<LangVersion>preview</LangVersion>
|
<LangVersion>preview</LangVersion>
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ using DiscordChatExporter.Core.Discord;
|
|||||||
using DiscordChatExporter.Core.Discord.Data;
|
using DiscordChatExporter.Core.Discord.Data;
|
||||||
using DiscordChatExporter.Core.Exceptions;
|
using DiscordChatExporter.Core.Exceptions;
|
||||||
using DiscordChatExporter.Core.Exporting;
|
using DiscordChatExporter.Core.Exporting;
|
||||||
|
using DiscordChatExporter.Core.Exporting.Filtering;
|
||||||
using DiscordChatExporter.Core.Exporting.Partitioning;
|
using DiscordChatExporter.Core.Exporting.Partitioning;
|
||||||
using DiscordChatExporter.Core.Utils.Extensions;
|
using DiscordChatExporter.Core.Utils.Extensions;
|
||||||
using Tyrrrz.Extensions;
|
using Tyrrrz.Extensions;
|
||||||
@@ -32,8 +33,11 @@ namespace DiscordChatExporter.Cli.Commands.Base
|
|||||||
[CommandOption("before", Description = "Only include messages sent before this date or message ID.")]
|
[CommandOption("before", Description = "Only include messages sent before this date or message ID.")]
|
||||||
public Snowflake? Before { get; init; }
|
public Snowflake? Before { get; init; }
|
||||||
|
|
||||||
[CommandOption("partition", 'p', Description = "Split output into partitions, each limited to this number of messages (e.g. 100) or file size (e.g. 10mb).")]
|
[CommandOption("partition", 'p', Description = "Split output into partitions, each limited to this number of messages (e.g. '100') or file size (e.g. '10mb').")]
|
||||||
public PartitionLimit PartitionLimit { get; init; } = NullPartitionLimit.Instance;
|
public PartitionLimit PartitionLimit { get; init; } = PartitionLimit.Null;
|
||||||
|
|
||||||
|
[CommandOption("filter", Description = "Only include messages that satisfy this filter (e.g. 'from:foo#1234' or 'has:image').")]
|
||||||
|
public MessageFilter MessageFilter { get; init; } = MessageFilter.Null;
|
||||||
|
|
||||||
[CommandOption("parallel", Description = "Limits how many channels can be exported in parallel.")]
|
[CommandOption("parallel", Description = "Limits how many channels can be exported in parallel.")]
|
||||||
public int ParallelLimit { get; init; } = 1;
|
public int ParallelLimit { get; init; } = 1;
|
||||||
@@ -76,6 +80,7 @@ namespace DiscordChatExporter.Cli.Commands.Base
|
|||||||
After,
|
After,
|
||||||
Before,
|
Before,
|
||||||
PartitionLimit,
|
PartitionLimit,
|
||||||
|
MessageFilter,
|
||||||
ShouldDownloadMedia,
|
ShouldDownloadMedia,
|
||||||
ShouldReuseMedia,
|
ShouldReuseMedia,
|
||||||
DateFormat
|
DateFormat
|
||||||
@@ -128,8 +133,6 @@ namespace DiscordChatExporter.Cli.Commands.Base
|
|||||||
{
|
{
|
||||||
throw new CommandException("Export failed.");
|
throw new CommandException("Export failed.");
|
||||||
}
|
}
|
||||||
|
|
||||||
await console.Output.WriteLineAsync("Done.");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override ValueTask ExecuteAsync(IConsole console)
|
public override ValueTask ExecuteAsync(IConsole console)
|
||||||
|
|||||||
@@ -31,6 +31,10 @@ namespace DiscordChatExporter.Cli.Commands
|
|||||||
|
|
||||||
await foreach (var channel in Discord.GetGuildChannelsAsync(guild.Id))
|
await foreach (var channel in Discord.GetGuildChannelsAsync(guild.Id))
|
||||||
{
|
{
|
||||||
|
// Skip non-text channels
|
||||||
|
if (!channel.IsTextChannel)
|
||||||
|
continue;
|
||||||
|
|
||||||
channels.Add(channel);
|
channels.Add(channel);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Threading.Tasks;
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using CliFx.Attributes;
|
using CliFx.Attributes;
|
||||||
using CliFx.Infrastructure;
|
using CliFx.Infrastructure;
|
||||||
using DiscordChatExporter.Cli.Commands.Base;
|
using DiscordChatExporter.Cli.Commands.Base;
|
||||||
@@ -17,9 +18,10 @@ namespace DiscordChatExporter.Cli.Commands
|
|||||||
// Get channel metadata
|
// Get channel metadata
|
||||||
await console.Output.WriteLineAsync("Fetching channels...");
|
await console.Output.WriteLineAsync("Fetching channels...");
|
||||||
var channels = await Discord.GetGuildChannelsAsync(Guild.DirectMessages.Id);
|
var channels = await Discord.GetGuildChannelsAsync(Guild.DirectMessages.Id);
|
||||||
|
var textChannels = channels.Where(c => c.IsTextChannel).ToArray();
|
||||||
|
|
||||||
// Export
|
// Export
|
||||||
await ExportAsync(console, channels);
|
await ExportAsync(console, textChannels);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Threading.Tasks;
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using CliFx.Attributes;
|
using CliFx.Attributes;
|
||||||
using CliFx.Infrastructure;
|
using CliFx.Infrastructure;
|
||||||
using DiscordChatExporter.Cli.Commands.Base;
|
using DiscordChatExporter.Cli.Commands.Base;
|
||||||
@@ -20,9 +21,10 @@ namespace DiscordChatExporter.Cli.Commands
|
|||||||
// Get channel metadata
|
// Get channel metadata
|
||||||
await console.Output.WriteLineAsync("Fetching channels...");
|
await console.Output.WriteLineAsync("Fetching channels...");
|
||||||
var channels = await Discord.GetGuildChannelsAsync(GuildId);
|
var channels = await Discord.GetGuildChannelsAsync(GuildId);
|
||||||
|
var textChannels = channels.Where(c => c.IsTextChannel).ToArray();
|
||||||
|
|
||||||
// Export
|
// Export
|
||||||
await ExportAsync(console, channels);
|
await ExportAsync(console, textChannels);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -19,7 +19,13 @@ namespace DiscordChatExporter.Cli.Commands
|
|||||||
{
|
{
|
||||||
var channels = await Discord.GetGuildChannelsAsync(GuildId);
|
var channels = await Discord.GetGuildChannelsAsync(GuildId);
|
||||||
|
|
||||||
foreach (var channel in channels.OrderBy(c => c.Category.Position).ThenBy(c => c.Name))
|
var textChannels = channels
|
||||||
|
.Where(c => c.IsTextChannel)
|
||||||
|
.OrderBy(c => c.Category.Position)
|
||||||
|
.ThenBy(c => c.Name)
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
foreach (var channel in textChannels)
|
||||||
{
|
{
|
||||||
// Channel ID
|
// Channel ID
|
||||||
await console.Output.WriteAsync(channel.Id.ToString());
|
await console.Output.WriteAsync(channel.Id.ToString());
|
||||||
|
|||||||
@@ -16,7 +16,13 @@ namespace DiscordChatExporter.Cli.Commands
|
|||||||
{
|
{
|
||||||
var channels = await Discord.GetGuildChannelsAsync(Guild.DirectMessages.Id);
|
var channels = await Discord.GetGuildChannelsAsync(Guild.DirectMessages.Id);
|
||||||
|
|
||||||
foreach (var channel in channels.OrderBy(c => c.Name))
|
var textChannels = channels
|
||||||
|
.Where(c => c.IsTextChannel)
|
||||||
|
.OrderBy(c => c.Category.Position)
|
||||||
|
.ThenBy(c => c.Name)
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
foreach (var channel in textChannels)
|
||||||
{
|
{
|
||||||
// Channel ID
|
// Channel ID
|
||||||
await console.Output.WriteAsync(channel.Id.ToString());
|
await console.Output.WriteAsync(channel.Id.ToString());
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using CliFx;
|
using CliFx;
|
||||||
using CliFx.Attributes;
|
using CliFx.Attributes;
|
||||||
@@ -16,10 +16,12 @@ namespace DiscordChatExporter.Cli.Commands
|
|||||||
|
|
||||||
console.Output.WriteLine(" 1. Open Discord");
|
console.Output.WriteLine(" 1. Open Discord");
|
||||||
console.Output.WriteLine(" 2. Press Ctrl+Shift+I to show developer tools");
|
console.Output.WriteLine(" 2. Press Ctrl+Shift+I to show developer tools");
|
||||||
console.Output.WriteLine(" 3. Navigate to the Application tab");
|
console.Output.WriteLine(" 3. Press Ctrl+Shift+M to toggle device toolbar");
|
||||||
console.Output.WriteLine(" 4. Select \"Local Storage\" > \"https://discord.com\" on the left");
|
console.Output.WriteLine(" 4. Navigate to the Application tab");
|
||||||
console.Output.WriteLine(" 5. Press Ctrl+R to reload");
|
console.Output.WriteLine(" 5. On the left, expand Local Storage and select https://discord.com");
|
||||||
console.Output.WriteLine(" 6. Find \"token\" at the bottom and copy the value");
|
console.Output.WriteLine(" 6. Type \"token\" into the Filter box");
|
||||||
|
console.Output.WriteLine(" 7. If the token key does not appear, press Ctrl+R to reload");
|
||||||
|
console.Output.WriteLine(" 8. Copy the value of the token key");
|
||||||
console.Output.WriteLine(" * Automating user accounts is technically against TOS, use at your own risk.");
|
console.Output.WriteLine(" * Automating user accounts is technically against TOS, use at your own risk.");
|
||||||
console.Output.WriteLine();
|
console.Output.WriteLine();
|
||||||
|
|
||||||
|
|||||||
@@ -6,8 +6,8 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="CliFx" Version="2.0.4" />
|
<PackageReference Include="CliFx" Version="2.0.6" />
|
||||||
<PackageReference Include="Spectre.Console" Version="0.40.0" />
|
<PackageReference Include="Spectre.Console" Version="0.41.0" />
|
||||||
<PackageReference Include="Gress" Version="1.2.0" />
|
<PackageReference Include="Gress" Version="1.2.0" />
|
||||||
<PackageReference Include="Tyrrrz.Extensions" Version="1.6.5" />
|
<PackageReference Include="Tyrrrz.Extensions" Version="1.6.5" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|||||||
@@ -17,18 +17,19 @@ namespace DiscordChatExporter.Core.Discord.Data
|
|||||||
|
|
||||||
public string FileName { get; }
|
public string FileName { get; }
|
||||||
|
|
||||||
|
public string FileExtension => Path.GetExtension(FileName);
|
||||||
|
|
||||||
public int? Width { get; }
|
public int? Width { get; }
|
||||||
|
|
||||||
public int? Height { get; }
|
public int? Height { get; }
|
||||||
|
|
||||||
public bool IsImage => ImageFileExtensions.Contains(Path.GetExtension(FileName));
|
public bool IsImage => ImageFileExtensions.Contains(FileExtension);
|
||||||
|
|
||||||
public bool IsVideo => VideoFileExtensions.Contains(Path.GetExtension(FileName));
|
public bool IsVideo => VideoFileExtensions.Contains(FileExtension);
|
||||||
|
|
||||||
public bool IsAudio => AudioFileExtensions.Contains(Path.GetExtension(FileName));
|
public bool IsAudio => AudioFileExtensions.Contains(FileExtension);
|
||||||
|
|
||||||
public bool IsSpoiler =>
|
public bool IsSpoiler => FileName.StartsWith("SPOILER_", StringComparison.Ordinal);
|
||||||
(IsImage || IsVideo || IsAudio) && FileName.StartsWith("SPOILER_", StringComparison.Ordinal);
|
|
||||||
|
|
||||||
public FileSize FileSize { get; }
|
public FileSize FileSize { get; }
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ namespace DiscordChatExporter.Core.Discord.Data
|
|||||||
ChannelType.GuildNews or
|
ChannelType.GuildNews or
|
||||||
ChannelType.GuildStore;
|
ChannelType.GuildStore;
|
||||||
|
|
||||||
|
public bool IsVoiceChannel => !IsTextChannel;
|
||||||
|
|
||||||
public Snowflake GuildId { get; }
|
public Snowflake GuildId { get; }
|
||||||
|
|
||||||
public ChannelCategory Category { get; }
|
public ChannelCategory Category { get; }
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ namespace DiscordChatExporter.Core.Discord
|
|||||||
{
|
{
|
||||||
HttpStatusCode.Unauthorized => DiscordChatExporterException.Unauthorized(),
|
HttpStatusCode.Unauthorized => DiscordChatExporterException.Unauthorized(),
|
||||||
HttpStatusCode.Forbidden => DiscordChatExporterException.Forbidden(),
|
HttpStatusCode.Forbidden => DiscordChatExporterException.Forbidden(),
|
||||||
HttpStatusCode.NotFound => DiscordChatExporterException.NotFound(),
|
HttpStatusCode.NotFound => DiscordChatExporterException.NotFound(url),
|
||||||
_ => DiscordChatExporterException.FailedHttpRequest(response)
|
_ => DiscordChatExporterException.FailedHttpRequest(response)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -142,10 +142,6 @@ namespace DiscordChatExporter.Core.Discord
|
|||||||
|
|
||||||
var channel = Channel.Parse(channelJson, category, position);
|
var channel = Channel.Parse(channelJson, category, position);
|
||||||
|
|
||||||
// We are only interested in text channels
|
|
||||||
if (!channel.IsTextChannel)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
position++;
|
position++;
|
||||||
|
|
||||||
yield return channel;
|
yield return channel;
|
||||||
|
|||||||
@@ -8,16 +8,12 @@
|
|||||||
<PackageReference Include="JsonExtensions" Version="1.1.0" />
|
<PackageReference Include="JsonExtensions" Version="1.1.0" />
|
||||||
<PackageReference Include="MiniRazor.CodeGen" Version="2.1.2" />
|
<PackageReference Include="MiniRazor.CodeGen" Version="2.1.2" />
|
||||||
<PackageReference Include="Polly" Version="7.2.2" />
|
<PackageReference Include="Polly" Version="7.2.2" />
|
||||||
|
<PackageReference Include="Superpower" Version="2.3.0" />
|
||||||
<PackageReference Include="Tyrrrz.Extensions" Version="1.6.5" />
|
<PackageReference Include="Tyrrrz.Extensions" Version="1.6.5" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<EmbeddedResource Include="Exporting\Writers\Html\Core.css" />
|
<AdditionalFiles Include="Exporting\Writers\Html\*.cshtml" IsRazorTemplate="true" />
|
||||||
<EmbeddedResource Include="Exporting\Writers\Html\Dark.css" />
|
|
||||||
<EmbeddedResource Include="Exporting\Writers\Html\Light.css" />
|
|
||||||
<AdditionalFiles Include="Exporting\Writers\Html\PreambleTemplate.cshtml" IsRazorTemplate="true" />
|
|
||||||
<AdditionalFiles Include="Exporting\Writers\Html\PostambleTemplate.cshtml" IsRazorTemplate="true" />
|
|
||||||
<AdditionalFiles Include="Exporting\Writers\Html\MessageGroupTemplate.cshtml" IsRazorTemplate="true" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
@@ -36,8 +36,8 @@ Failed to perform an HTTP request.
|
|||||||
internal static DiscordChatExporterException Forbidden() =>
|
internal static DiscordChatExporterException Forbidden() =>
|
||||||
new("Access is forbidden.");
|
new("Access is forbidden.");
|
||||||
|
|
||||||
internal static DiscordChatExporterException NotFound() =>
|
internal static DiscordChatExporterException NotFound(string resourceId) =>
|
||||||
new("Requested resource does not exist.");
|
new($"Requested resource ({resourceId}) does not exist.");
|
||||||
|
|
||||||
internal static DiscordChatExporterException ChannelIsEmpty() =>
|
internal static DiscordChatExporterException ChannelIsEmpty() =>
|
||||||
new("No messages found for the specified period.");
|
new("No messages found for the specified period.");
|
||||||
|
|||||||
@@ -39,6 +39,10 @@ namespace DiscordChatExporter.Core.Exporting
|
|||||||
var encounteredUsers = new HashSet<User>(IdBasedEqualityComparer.Instance);
|
var encounteredUsers = new HashSet<User>(IdBasedEqualityComparer.Instance);
|
||||||
await foreach (var message in _discord.GetMessagesAsync(request.Channel.Id, request.After, request.Before, progress))
|
await foreach (var message in _discord.GetMessagesAsync(request.Channel.Id, request.After, request.Before, progress))
|
||||||
{
|
{
|
||||||
|
// Skips any messages that fail to pass the supplied filter
|
||||||
|
if (!request.MessageFilter.Filter(message))
|
||||||
|
continue;
|
||||||
|
|
||||||
// Resolve members for referenced users
|
// Resolve members for referenced users
|
||||||
foreach (var referencedUser in message.MentionedUsers.Prepend(message.Author))
|
foreach (var referencedUser in message.MentionedUsers.Prepend(message.Author))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ using System.Text;
|
|||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using DiscordChatExporter.Core.Discord;
|
using DiscordChatExporter.Core.Discord;
|
||||||
using DiscordChatExporter.Core.Discord.Data;
|
using DiscordChatExporter.Core.Discord.Data;
|
||||||
|
using DiscordChatExporter.Core.Exporting.Filtering;
|
||||||
using DiscordChatExporter.Core.Exporting.Partitioning;
|
using DiscordChatExporter.Core.Exporting.Partitioning;
|
||||||
using DiscordChatExporter.Core.Utils;
|
using DiscordChatExporter.Core.Utils;
|
||||||
|
|
||||||
@@ -31,6 +32,8 @@ namespace DiscordChatExporter.Core.Exporting
|
|||||||
|
|
||||||
public PartitionLimit PartitionLimit { get; }
|
public PartitionLimit PartitionLimit { get; }
|
||||||
|
|
||||||
|
public MessageFilter MessageFilter { get; }
|
||||||
|
|
||||||
public bool ShouldDownloadMedia { get; }
|
public bool ShouldDownloadMedia { get; }
|
||||||
|
|
||||||
public bool ShouldReuseMedia { get; }
|
public bool ShouldReuseMedia { get; }
|
||||||
@@ -45,6 +48,7 @@ namespace DiscordChatExporter.Core.Exporting
|
|||||||
Snowflake? after,
|
Snowflake? after,
|
||||||
Snowflake? before,
|
Snowflake? before,
|
||||||
PartitionLimit partitionLimit,
|
PartitionLimit partitionLimit,
|
||||||
|
MessageFilter messageFilter,
|
||||||
bool shouldDownloadMedia,
|
bool shouldDownloadMedia,
|
||||||
bool shouldReuseMedia,
|
bool shouldReuseMedia,
|
||||||
string dateFormat)
|
string dateFormat)
|
||||||
@@ -56,6 +60,7 @@ namespace DiscordChatExporter.Core.Exporting
|
|||||||
After = after;
|
After = after;
|
||||||
Before = before;
|
Before = before;
|
||||||
PartitionLimit = partitionLimit;
|
PartitionLimit = partitionLimit;
|
||||||
|
MessageFilter = messageFilter;
|
||||||
ShouldDownloadMedia = shouldDownloadMedia;
|
ShouldDownloadMedia = shouldDownloadMedia;
|
||||||
ShouldReuseMedia = shouldReuseMedia;
|
ShouldReuseMedia = shouldReuseMedia;
|
||||||
DateFormat = dateFormat;
|
DateFormat = dateFormat;
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
namespace DiscordChatExporter.Core.Exporting.Filtering
|
||||||
|
{
|
||||||
|
internal enum BinaryExpressionKind
|
||||||
|
{
|
||||||
|
Or,
|
||||||
|
And
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
using System;
|
||||||
|
using DiscordChatExporter.Core.Discord.Data;
|
||||||
|
|
||||||
|
namespace DiscordChatExporter.Core.Exporting.Filtering
|
||||||
|
{
|
||||||
|
internal class BinaryExpressionMessageFilter : MessageFilter
|
||||||
|
{
|
||||||
|
private readonly MessageFilter _first;
|
||||||
|
private readonly MessageFilter _second;
|
||||||
|
private readonly BinaryExpressionKind _kind;
|
||||||
|
|
||||||
|
public BinaryExpressionMessageFilter(MessageFilter first, MessageFilter second, BinaryExpressionKind kind)
|
||||||
|
{
|
||||||
|
_first = first;
|
||||||
|
_second = second;
|
||||||
|
_kind = kind;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool Filter(Message message) => _kind switch
|
||||||
|
{
|
||||||
|
BinaryExpressionKind.Or => _first.Filter(message) || _second.Filter(message),
|
||||||
|
BinaryExpressionKind.And => _first.Filter(message) && _second.Filter(message),
|
||||||
|
_ => throw new InvalidOperationException($"Unknown binary expression kind '{_kind}'.")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using DiscordChatExporter.Core.Discord.Data;
|
||||||
|
|
||||||
|
namespace DiscordChatExporter.Core.Exporting.Filtering
|
||||||
|
{
|
||||||
|
internal class ContainsMessageFilter : MessageFilter
|
||||||
|
{
|
||||||
|
private readonly string _text;
|
||||||
|
|
||||||
|
public ContainsMessageFilter(string text) => _text = text;
|
||||||
|
|
||||||
|
public override bool Filter(Message message) => Regex.IsMatch(
|
||||||
|
message.Content,
|
||||||
|
"\\b" + _text + "\\b",
|
||||||
|
RegexOptions.IgnoreCase | RegexOptions.CultureInvariant
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
using System;
|
||||||
|
using DiscordChatExporter.Core.Discord.Data;
|
||||||
|
|
||||||
|
namespace DiscordChatExporter.Core.Exporting.Filtering
|
||||||
|
{
|
||||||
|
internal class FromMessageFilter : MessageFilter
|
||||||
|
{
|
||||||
|
private readonly string _value;
|
||||||
|
|
||||||
|
public FromMessageFilter(string value) => _value = value;
|
||||||
|
|
||||||
|
public override bool Filter(Message message) =>
|
||||||
|
string.Equals(_value, message.Author.Name, StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
string.Equals(_value, message.Author.FullName, StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
string.Equals(_value, message.Author.Id.ToString(), StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using DiscordChatExporter.Core.Discord.Data;
|
||||||
|
|
||||||
|
namespace DiscordChatExporter.Core.Exporting.Filtering
|
||||||
|
{
|
||||||
|
internal class HasMessageFilter : MessageFilter
|
||||||
|
{
|
||||||
|
private readonly MessageContentMatchKind _kind;
|
||||||
|
|
||||||
|
public HasMessageFilter(MessageContentMatchKind kind) => _kind = kind;
|
||||||
|
|
||||||
|
public override bool Filter(Message message) => _kind switch
|
||||||
|
{
|
||||||
|
MessageContentMatchKind.Link => Regex.IsMatch(message.Content, "https?://\\S*[^\\.,:;\"\'\\s]"),
|
||||||
|
MessageContentMatchKind.Embed => message.Embeds.Any(),
|
||||||
|
MessageContentMatchKind.File => message.Attachments.Any(),
|
||||||
|
MessageContentMatchKind.Video => message.Attachments.Any(file => file.IsVideo),
|
||||||
|
MessageContentMatchKind.Image => message.Attachments.Any(file => file.IsImage),
|
||||||
|
MessageContentMatchKind.Sound => message.Attachments.Any(file => file.IsAudio),
|
||||||
|
_ => throw new InvalidOperationException($"Unknown message content match kind '{_kind}'.")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using DiscordChatExporter.Core.Discord.Data;
|
||||||
|
|
||||||
|
namespace DiscordChatExporter.Core.Exporting.Filtering
|
||||||
|
{
|
||||||
|
internal class MentionsMessageFilter : MessageFilter
|
||||||
|
{
|
||||||
|
private readonly string _value;
|
||||||
|
|
||||||
|
public MentionsMessageFilter(string value) => _value = value;
|
||||||
|
|
||||||
|
public override bool Filter(Message message) => message.MentionedUsers.Any(user =>
|
||||||
|
string.Equals(_value, user.Name, StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
string.Equals(_value, user.FullName, StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
string.Equals(_value, user.Id.ToString(), StringComparison.OrdinalIgnoreCase)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
namespace DiscordChatExporter.Core.Exporting.Filtering
|
||||||
|
{
|
||||||
|
internal enum MessageContentMatchKind
|
||||||
|
{
|
||||||
|
Link,
|
||||||
|
Embed,
|
||||||
|
File,
|
||||||
|
Video,
|
||||||
|
Image,
|
||||||
|
Sound
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
using DiscordChatExporter.Core.Discord.Data;
|
||||||
|
using DiscordChatExporter.Core.Exporting.Filtering.Parsing;
|
||||||
|
using Superpower;
|
||||||
|
|
||||||
|
namespace DiscordChatExporter.Core.Exporting.Filtering
|
||||||
|
{
|
||||||
|
public abstract partial class MessageFilter
|
||||||
|
{
|
||||||
|
public abstract bool Filter(Message message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public partial class MessageFilter
|
||||||
|
{
|
||||||
|
public static MessageFilter Null { get; } = new NullMessageFilter();
|
||||||
|
|
||||||
|
public static MessageFilter Parse(string value) => FilterGrammar.Filter.Parse(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
using DiscordChatExporter.Core.Discord.Data;
|
||||||
|
|
||||||
|
namespace DiscordChatExporter.Core.Exporting.Filtering
|
||||||
|
{
|
||||||
|
internal class NegatedMessageFilter : MessageFilter
|
||||||
|
{
|
||||||
|
private readonly MessageFilter _filter;
|
||||||
|
|
||||||
|
public NegatedMessageFilter(MessageFilter filter) => _filter = filter;
|
||||||
|
|
||||||
|
public override bool Filter(Message message) => !_filter.Filter(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
using DiscordChatExporter.Core.Discord.Data;
|
||||||
|
|
||||||
|
namespace DiscordChatExporter.Core.Exporting.Filtering
|
||||||
|
{
|
||||||
|
internal class NullMessageFilter : MessageFilter
|
||||||
|
{
|
||||||
|
public override bool Filter(Message message) => true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
using System.Linq;
|
||||||
|
using DiscordChatExporter.Core.Utils.Extensions;
|
||||||
|
using Superpower;
|
||||||
|
using Superpower.Parsers;
|
||||||
|
|
||||||
|
namespace DiscordChatExporter.Core.Exporting.Filtering.Parsing
|
||||||
|
{
|
||||||
|
internal static class FilterGrammar
|
||||||
|
{
|
||||||
|
// Choice(a, b) looks cleaner than a.Or(b)
|
||||||
|
private static TextParser<T> Choice<T>(params TextParser<T>[] parsers) =>
|
||||||
|
parsers.Aggregate((current, next) => current.Or(next));
|
||||||
|
|
||||||
|
private static readonly TextParser<char> EscapedCharacter =
|
||||||
|
Character.EqualTo('\\').IgnoreThen(Character.AnyChar);
|
||||||
|
|
||||||
|
private static readonly TextParser<string> QuotedString =
|
||||||
|
from open in Character.In('"', '\'')
|
||||||
|
from value in Choice(EscapedCharacter, Character.Except(open)).Many().Text()
|
||||||
|
from close in Character.EqualTo(open)
|
||||||
|
select value;
|
||||||
|
|
||||||
|
private static readonly TextParser<char> FreeCharacter =
|
||||||
|
Character.Matching(c =>
|
||||||
|
!char.IsWhiteSpace(c) &&
|
||||||
|
// Avoid all special tokens used by the grammar
|
||||||
|
c is not ('(' or ')' or '"' or '\'' or '-' or '|' or '&'),
|
||||||
|
"any character except whitespace or `(`, `)`, `\"`, `'`, `-`, `|`, `&`"
|
||||||
|
);
|
||||||
|
|
||||||
|
private static readonly TextParser<string> UnquotedString =
|
||||||
|
Choice(EscapedCharacter, FreeCharacter).AtLeastOnce().Text();
|
||||||
|
|
||||||
|
private static readonly TextParser<string> String =
|
||||||
|
Choice(QuotedString, UnquotedString).Named("text string");
|
||||||
|
|
||||||
|
private static readonly TextParser<MessageFilter> ContainsFilter =
|
||||||
|
String.Select(v => (MessageFilter) new ContainsMessageFilter(v));
|
||||||
|
|
||||||
|
private static readonly TextParser<MessageFilter> FromFilter = Span
|
||||||
|
.EqualToIgnoreCase("from:")
|
||||||
|
.IgnoreThen(String)
|
||||||
|
.Select(v => (MessageFilter) new FromMessageFilter(v))
|
||||||
|
.Named("from:<value>");
|
||||||
|
|
||||||
|
private static readonly TextParser<MessageFilter> MentionsFilter = Span
|
||||||
|
.EqualToIgnoreCase("mentions:")
|
||||||
|
.IgnoreThen(String)
|
||||||
|
.Select(v => (MessageFilter) new MentionsMessageFilter(v))
|
||||||
|
.Named("mentions:<value>");
|
||||||
|
|
||||||
|
private static readonly TextParser<MessageFilter> HasFilter = Span
|
||||||
|
.EqualToIgnoreCase("has:")
|
||||||
|
.IgnoreThen(Choice(
|
||||||
|
Span.EqualToIgnoreCase("link").IgnoreThen(Parse.Return(MessageContentMatchKind.Link)),
|
||||||
|
Span.EqualToIgnoreCase("embed").IgnoreThen(Parse.Return(MessageContentMatchKind.Embed)),
|
||||||
|
Span.EqualToIgnoreCase("video").IgnoreThen(Parse.Return(MessageContentMatchKind.Video)),
|
||||||
|
Span.EqualToIgnoreCase("image").IgnoreThen(Parse.Return(MessageContentMatchKind.Image)),
|
||||||
|
Span.EqualToIgnoreCase("sound").IgnoreThen(Parse.Return(MessageContentMatchKind.Sound))
|
||||||
|
))
|
||||||
|
.Select(k => (MessageFilter) new HasMessageFilter(k))
|
||||||
|
.Named("has:<value>");
|
||||||
|
|
||||||
|
private static readonly TextParser<MessageFilter> NegatedFilter = Character
|
||||||
|
.EqualTo('-')
|
||||||
|
.IgnoreThen(Parse.Ref(() => StandaloneFilter))
|
||||||
|
.Select(f => (MessageFilter) new NegatedMessageFilter(f));
|
||||||
|
|
||||||
|
private static readonly TextParser<MessageFilter> GroupedFilter =
|
||||||
|
from open in Character.EqualTo('(')
|
||||||
|
from content in Parse.Ref(() => BinaryExpressionFilter).Token()
|
||||||
|
from close in Character.EqualTo(')')
|
||||||
|
select content;
|
||||||
|
|
||||||
|
private static readonly TextParser<MessageFilter> StandaloneFilter = Choice(
|
||||||
|
GroupedFilter,
|
||||||
|
FromFilter,
|
||||||
|
MentionsFilter,
|
||||||
|
HasFilter,
|
||||||
|
ContainsFilter
|
||||||
|
);
|
||||||
|
|
||||||
|
private static readonly TextParser<MessageFilter> UnaryExpressionFilter = Choice(
|
||||||
|
NegatedFilter,
|
||||||
|
StandaloneFilter
|
||||||
|
);
|
||||||
|
|
||||||
|
private static readonly TextParser<MessageFilter> BinaryExpressionFilter = Parse.Chain(
|
||||||
|
Choice(
|
||||||
|
// Explicit operator
|
||||||
|
Character.In('|', '&').Token().Try(),
|
||||||
|
// Implicit operator (resolves to 'and')
|
||||||
|
Character.WhiteSpace.AtLeastOnce().IgnoreThen(Parse.Return(' '))
|
||||||
|
),
|
||||||
|
UnaryExpressionFilter,
|
||||||
|
(op, left, right) => op switch
|
||||||
|
{
|
||||||
|
'|' => new BinaryExpressionMessageFilter(left, right, BinaryExpressionKind.Or),
|
||||||
|
_ => new BinaryExpressionMessageFilter(left, right, BinaryExpressionKind.And)
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
public static readonly TextParser<MessageFilter> Filter =
|
||||||
|
BinaryExpressionFilter.Token().AtEnd();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
namespace DiscordChatExporter.Core.Exporting.Partitioning
|
namespace DiscordChatExporter.Core.Exporting.Partitioning
|
||||||
{
|
{
|
||||||
public class FileSizePartitionLimit : PartitionLimit
|
internal class FileSizePartitionLimit : PartitionLimit
|
||||||
{
|
{
|
||||||
private readonly long _limit;
|
private readonly long _limit;
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
namespace DiscordChatExporter.Core.Exporting.Partitioning
|
namespace DiscordChatExporter.Core.Exporting.Partitioning
|
||||||
{
|
{
|
||||||
public class MessageCountPartitionLimit : PartitionLimit
|
internal class MessageCountPartitionLimit : PartitionLimit
|
||||||
{
|
{
|
||||||
private readonly long _limit;
|
private readonly long _limit;
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
namespace DiscordChatExporter.Core.Exporting.Partitioning
|
namespace DiscordChatExporter.Core.Exporting.Partitioning
|
||||||
{
|
{
|
||||||
public class NullPartitionLimit : PartitionLimit
|
internal class NullPartitionLimit : PartitionLimit
|
||||||
{
|
{
|
||||||
public static NullPartitionLimit Instance { get; } = new();
|
|
||||||
|
|
||||||
public override bool IsReached(long messagesWritten, long bytesWritten) => false;
|
public override bool IsReached(long messagesWritten, long bytesWritten) => false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -11,6 +11,8 @@ namespace DiscordChatExporter.Core.Exporting.Partitioning
|
|||||||
|
|
||||||
public partial class PartitionLimit
|
public partial class PartitionLimit
|
||||||
{
|
{
|
||||||
|
public static PartitionLimit Null { get; } = new NullPartitionLimit();
|
||||||
|
|
||||||
private static long? TryParseFileSizeBytes(string value, IFormatProvider? formatProvider = null)
|
private static long? TryParseFileSizeBytes(string value, IFormatProvider? formatProvider = null)
|
||||||
{
|
{
|
||||||
var match = Regex.Match(value, @"^\s*(\d+[\.,]?\d*)\s*(\w)?b\s*$", RegexOptions.IgnoreCase);
|
var match = Regex.Match(value, @"^\s*(\d+[\.,]?\d*)\s*(\w)?b\s*$", RegexOptions.IgnoreCase);
|
||||||
|
|||||||
@@ -1,542 +0,0 @@
|
|||||||
/* General */
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: Whitney;
|
|
||||||
src: url(https://cdn.jsdelivr.net/gh/Tyrrrz/DiscordFonts@master/whitney-300.woff);
|
|
||||||
font-weight: 300;
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: Whitney;
|
|
||||||
src: url(https://cdn.jsdelivr.net/gh/Tyrrrz/DiscordFonts@master/whitney-400.woff);
|
|
||||||
font-weight: 400;
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: Whitney;
|
|
||||||
src: url(https://cdn.jsdelivr.net/gh/Tyrrrz/DiscordFonts@master/whitney-500.woff);
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: Whitney;
|
|
||||||
src: url(https://cdn.jsdelivr.net/gh/Tyrrrz/DiscordFonts@master/whitney-600.woff);
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: Whitney;
|
|
||||||
src: url(https://cdn.jsdelivr.net/gh/Tyrrrz/DiscordFonts@master/whitney-700.woff);
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
font-family: "Whitney", "Helvetica Neue", Helvetica, Arial, sans-serif;
|
|
||||||
font-size: 17px;
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
a:hover {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
img {
|
|
||||||
object-fit: contain;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown {
|
|
||||||
max-width: 100%;
|
|
||||||
line-height: 1.3;
|
|
||||||
overflow-wrap: break-word;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preserve-whitespace {
|
|
||||||
white-space: pre-wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.spoiler {
|
|
||||||
/* width: fit-content; */
|
|
||||||
display: inline-block;
|
|
||||||
/* This is more consistent across browsers, the old attribute worked well under Chrome but not FireFox. */
|
|
||||||
}
|
|
||||||
|
|
||||||
.spoiler--hidden {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.spoiler-text {
|
|
||||||
border-radius: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.spoiler--hidden .spoiler-text {
|
|
||||||
color: rgba(0, 0, 0, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
.spoiler--hidden .spoiler-text::selection {
|
|
||||||
color: rgba(0, 0, 0, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
.spoiler-image {
|
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
|
||||||
border-radius: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.spoiler--hidden .spoiler-image {
|
|
||||||
box-shadow: 0 0 1px 1px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.spoiler--hidden .spoiler-image * {
|
|
||||||
filter: blur(44px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.spoiler--hidden .spoiler-image:after {
|
|
||||||
content: "SPOILER";
|
|
||||||
color: #dcddde;
|
|
||||||
background-color: rgba(0, 0, 0, 0.6);
|
|
||||||
position: absolute;
|
|
||||||
left: 50%;
|
|
||||||
top: 50%;
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
font-weight: 600;
|
|
||||||
padding: 100%;
|
|
||||||
border-radius: 20px;
|
|
||||||
letter-spacing: 0.05em;
|
|
||||||
font-size: 0.9em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.spoiler--hidden:hover .spoiler-image:after {
|
|
||||||
color: #fff;
|
|
||||||
background-color: rgba(0, 0, 0, 0.9);
|
|
||||||
}
|
|
||||||
|
|
||||||
.quote {
|
|
||||||
margin: 0.1em 0;
|
|
||||||
padding-left: 0.6em;
|
|
||||||
border-left: 4px solid;
|
|
||||||
border-radius: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pre {
|
|
||||||
font-family: "Consolas", "Courier New", Courier, monospace;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pre--multiline {
|
|
||||||
margin-top: 0.25em;
|
|
||||||
padding: 0.5em;
|
|
||||||
border: 2px solid;
|
|
||||||
border-radius: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pre--inline {
|
|
||||||
padding: 2px;
|
|
||||||
border-radius: 3px;
|
|
||||||
font-size: 0.85em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mention {
|
|
||||||
border-radius: 3px;
|
|
||||||
padding: 0 2px;
|
|
||||||
color: #7289da;
|
|
||||||
background: rgba(114, 137, 218, .1);
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.emoji {
|
|
||||||
width: 1.325em;
|
|
||||||
height: 1.325em;
|
|
||||||
margin: 0 0.06em;
|
|
||||||
vertical-align: -0.4em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.emoji--small {
|
|
||||||
width: 1em;
|
|
||||||
height: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.emoji--large {
|
|
||||||
width: 2.8em;
|
|
||||||
height: 2.8em;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Preamble */
|
|
||||||
|
|
||||||
.preamble {
|
|
||||||
display: grid;
|
|
||||||
margin: 0 0.3em 0.6em 0.3em;
|
|
||||||
max-width: 100%;
|
|
||||||
grid-template-columns: auto 1fr;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preamble__guild-icon-container {
|
|
||||||
grid-column: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preamble__guild-icon {
|
|
||||||
max-width: 88px;
|
|
||||||
max-height: 88px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preamble__entries-container {
|
|
||||||
grid-column: 2;
|
|
||||||
margin-left: 0.6em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preamble__entry {
|
|
||||||
font-size: 1.4em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preamble__entry--small {
|
|
||||||
font-size: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Chatlog */
|
|
||||||
|
|
||||||
.chatlog {
|
|
||||||
max-width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chatlog__message-group {
|
|
||||||
display: grid;
|
|
||||||
margin: 0 0.6em;
|
|
||||||
padding: 0.9em 0;
|
|
||||||
border-top: 1px solid;
|
|
||||||
grid-template-columns: auto 1fr;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chatlog__reference-symbol {
|
|
||||||
grid-column: 1;
|
|
||||||
border-style: solid;
|
|
||||||
border-width: 2px 0 0 2px;
|
|
||||||
border-radius: 8px 0 0 0;
|
|
||||||
margin-left: 16px;
|
|
||||||
margin-top: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chatlog__reference {
|
|
||||||
display: flex;
|
|
||||||
grid-column: 2;
|
|
||||||
margin-left: 1.2em;
|
|
||||||
margin-bottom: 0.25em;
|
|
||||||
font-size: 0.875em;
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chatlog__reference-avatar {
|
|
||||||
border-radius: 50%;
|
|
||||||
height: 16px;
|
|
||||||
width: 16px;
|
|
||||||
margin-right: 0.25em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chatlog__reference-name {
|
|
||||||
margin-right: 0.25em;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chatlog__reference-link {
|
|
||||||
flex-grow: 1;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chatlog__reference-content > * {
|
|
||||||
display: inline;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chatlog__reference-content > a:hover {
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chatlog__reference-content > .emoji {
|
|
||||||
width: 1.325em;
|
|
||||||
height: 1.325em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chatlog__reference-edited-timestamp {
|
|
||||||
margin-left: 0.25em;
|
|
||||||
font-size: 0.8em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chatlog__author-avatar-container {
|
|
||||||
grid-column: 1;
|
|
||||||
width: 40px;
|
|
||||||
height: 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chatlog__author-avatar {
|
|
||||||
border-radius: 50%;
|
|
||||||
height: 40px;
|
|
||||||
width: 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chatlog__messages {
|
|
||||||
grid-column: 2;
|
|
||||||
margin-left: 1.2em;
|
|
||||||
min-width: 50%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chatlog__author-name {
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chatlog__timestamp {
|
|
||||||
margin-left: 0.3em;
|
|
||||||
font-size: 0.75em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chatlog__message {
|
|
||||||
padding: 0.1em 0.3em;
|
|
||||||
margin: 0 -0.3em;
|
|
||||||
background-color: transparent;
|
|
||||||
transition: background-color 1s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chatlog__content {
|
|
||||||
font-size: 0.95em;
|
|
||||||
word-wrap: break-word;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chatlog__edited-timestamp {
|
|
||||||
margin-left: 0.15em;
|
|
||||||
font-size: 0.8em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chatlog__attachment {
|
|
||||||
margin-top: 0.3em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chatlog__attachment-thumbnail {
|
|
||||||
vertical-align: top;
|
|
||||||
max-width: 45vw;
|
|
||||||
max-height: 500px;
|
|
||||||
border-radius: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chatlog__attachment-container {
|
|
||||||
height: 40px;
|
|
||||||
width: 100%;
|
|
||||||
max-width: 520px;
|
|
||||||
padding: 10px;
|
|
||||||
border: 1px solid;
|
|
||||||
border-radius: 3px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chatlog__attachment-icon {
|
|
||||||
float: left;
|
|
||||||
height: 100%;
|
|
||||||
margin-right: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chatlog__attachment-icon > .a {
|
|
||||||
fill: #f4f5fb;
|
|
||||||
d: path("M50,935a25,25,0,0,1-25-25V50A25,25,0,0,1,50,25H519.6L695,201.32V910a25,25,0,0,1-25,25Z");
|
|
||||||
}
|
|
||||||
|
|
||||||
.chatlog__attachment-icon > .b {
|
|
||||||
fill: #7789c4;
|
|
||||||
d: path("M509.21,50,670,211.63V910H50V50H509.21M530,0H50A50,50,0,0,0,0,50V910a50,50,0,0,0,50,50H670a50,50,0,0,0,50-50h0V191Z");
|
|
||||||
}
|
|
||||||
|
|
||||||
.chatlog__attachment-icon > .c {
|
|
||||||
fill: #f4f5fb;
|
|
||||||
d: path("M530,215a25,25,0,0,1-25-25V50a25,25,0,0,1,16.23-23.41L693.41,198.77A25,25,0,0,1,670,215Z");
|
|
||||||
}
|
|
||||||
|
|
||||||
.chatlog__attachment-icon > .d {
|
|
||||||
fill: #7789c4;
|
|
||||||
d: path("M530,70.71,649.29,190H530V70.71M530,0a50,50,0,0,0-50,50V190a50,50,0,0,0,50,50H670a50,50,0,0,0,50-50Z");
|
|
||||||
}
|
|
||||||
|
|
||||||
.chatlog__attachment-filesize {
|
|
||||||
color: #72767d;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chatlog__attachment-filename {
|
|
||||||
overflow: hidden;
|
|
||||||
white-space: nowrap;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chatlog__embed {
|
|
||||||
display: flex;
|
|
||||||
margin-top: 0.3em;
|
|
||||||
max-width: 520px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chatlog__embed-color-pill {
|
|
||||||
flex-shrink: 0;
|
|
||||||
width: 0.25em;
|
|
||||||
border-top-left-radius: 3px;
|
|
||||||
border-bottom-left-radius: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chatlog__embed-content-container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
padding: 0.5em 0.6em;
|
|
||||||
border: 1px solid;
|
|
||||||
border-top-right-radius: 3px;
|
|
||||||
border-bottom-right-radius: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chatlog__embed-content {
|
|
||||||
display: flex;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chatlog__embed-text {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chatlog__embed-author {
|
|
||||||
display: flex;
|
|
||||||
margin-bottom: 0.3em;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chatlog__embed-author-icon {
|
|
||||||
margin-right: 0.5em;
|
|
||||||
width: 20px;
|
|
||||||
height: 20px;
|
|
||||||
border-radius: 50%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chatlog__embed-author-name {
|
|
||||||
font-size: 0.875em;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chatlog__embed-title {
|
|
||||||
margin-bottom: 0.2em;
|
|
||||||
font-size: 0.875em;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chatlog__embed-description {
|
|
||||||
font-weight: 500;
|
|
||||||
font-size: 0.85em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chatlog__embed-fields {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chatlog__embed-field {
|
|
||||||
flex: 0;
|
|
||||||
min-width: 100%;
|
|
||||||
max-width: 506px;
|
|
||||||
padding-top: 0.6em;
|
|
||||||
font-size: 0.875em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chatlog__embed-field--inline {
|
|
||||||
flex: 1;
|
|
||||||
flex-basis: auto;
|
|
||||||
min-width: 150px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chatlog__embed-field-name {
|
|
||||||
margin-bottom: 0.2em;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chatlog__embed-field-value {
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chatlog__embed-thumbnail {
|
|
||||||
flex: 0;
|
|
||||||
margin-left: 1.2em;
|
|
||||||
max-width: 80px;
|
|
||||||
max-height: 80px;
|
|
||||||
border-radius: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chatlog__embed-image-container {
|
|
||||||
margin-top: 0.6em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chatlog__embed-image {
|
|
||||||
max-width: 500px;
|
|
||||||
max-height: 400px;
|
|
||||||
border-radius: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chatlog__embed-footer {
|
|
||||||
margin-top: 0.6em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chatlog__embed-footer-icon {
|
|
||||||
margin-right: 0.2em;
|
|
||||||
width: 20px;
|
|
||||||
height: 20px;
|
|
||||||
border-radius: 50%;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chatlog__embed-footer-text {
|
|
||||||
vertical-align: middle;
|
|
||||||
font-size: 0.75em;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chatlog__embed-youtube-container {
|
|
||||||
margin-top: 0.6em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chatlog__embed-youtube {
|
|
||||||
border: 0;
|
|
||||||
border-radius: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chatlog__reactions {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chatlog__reaction {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
margin: 0.35em 0.1em 0.1em 0.1em;
|
|
||||||
padding: 0.2em 0.35em;
|
|
||||||
border-radius: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chatlog__reaction-count {
|
|
||||||
min-width: 9px;
|
|
||||||
margin-left: 0.35em;
|
|
||||||
font-size: 0.875em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chatlog__bot-tag {
|
|
||||||
position: relative;
|
|
||||||
top: -.2em;
|
|
||||||
margin-left: 0.3em;
|
|
||||||
padding: 0.05em 0.3em;
|
|
||||||
border-radius: 3px;
|
|
||||||
vertical-align: middle;
|
|
||||||
line-height: 1.3;
|
|
||||||
background: #5865F2;
|
|
||||||
color: #ffffff;
|
|
||||||
font-size: 0.625em;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Postamble */
|
|
||||||
|
|
||||||
.postamble {
|
|
||||||
margin: 1.4em 0.3em 0.6em 0.3em;
|
|
||||||
padding: 1em;
|
|
||||||
border-top: 1px solid;
|
|
||||||
}
|
|
||||||
@@ -1,147 +0,0 @@
|
|||||||
/* General */
|
|
||||||
|
|
||||||
body {
|
|
||||||
background-color: #36393e;
|
|
||||||
color: #dcddde;
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
color: #00aff4;
|
|
||||||
}
|
|
||||||
|
|
||||||
.spoiler-text {
|
|
||||||
background-color: rgba(255, 255, 255, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.spoiler--hidden .spoiler-text {
|
|
||||||
background-color: #202225;
|
|
||||||
}
|
|
||||||
|
|
||||||
.spoiler--hidden:hover .spoiler-text {
|
|
||||||
background-color: rgba(32, 34, 37, 0.8);
|
|
||||||
}
|
|
||||||
|
|
||||||
.quote {
|
|
||||||
border-color: #4f545c;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pre {
|
|
||||||
background-color: #2f3136 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pre--multiline {
|
|
||||||
border-color: #282b30 !important;
|
|
||||||
color: #b9bbbe !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Preamble */
|
|
||||||
|
|
||||||
.preamble__entry {
|
|
||||||
color: #ffffff;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Chatlog */
|
|
||||||
|
|
||||||
.chatlog__message-group {
|
|
||||||
border-color: rgba(255, 255, 255, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.chatlog__reference-symbol {
|
|
||||||
border-color: #4f545c;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chatlog__reference {
|
|
||||||
color: #b5b6b8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chatlog__reference-link {
|
|
||||||
color: #b5b6b8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chatlog__reference-link:hover {
|
|
||||||
color: #ffffff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chatlog__reference-edited-timestamp {
|
|
||||||
color: rgba(255, 255, 255, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.chatlog__author-name {
|
|
||||||
color: #ffffff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chatlog__timestamp {
|
|
||||||
color: rgba(255, 255, 255, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.chatlog__message--highlighted {
|
|
||||||
background-color: rgba(114, 137, 218, 0.2) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chatlog__message--pinned {
|
|
||||||
background-color: rgba(249, 168, 37, 0.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
.chatlog__attachment-container {
|
|
||||||
background-color: #2f3136;
|
|
||||||
border-color: #292b2f;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chatlog__edited-timestamp {
|
|
||||||
color: rgba(255, 255, 255, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.chatlog__embed-color-pill--default {
|
|
||||||
background-color: #202225;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chatlog__embed-content-container {
|
|
||||||
background-color: rgba(46, 48, 54, 0.3);
|
|
||||||
border-color: rgba(46, 48, 54, 0.6);
|
|
||||||
}
|
|
||||||
|
|
||||||
.chatlog__embed-author-name {
|
|
||||||
color: #ffffff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chatlog__embed-author-name-link {
|
|
||||||
color: #ffffff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chatlog__embed-title {
|
|
||||||
color: #ffffff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chatlog__embed-description {
|
|
||||||
color: #dcddde;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chatlog__embed-field-name {
|
|
||||||
color: #ffffff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chatlog__embed-field-value {
|
|
||||||
color: #dcddde;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chatlog__embed-footer {
|
|
||||||
color: #dcddde;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chatlog__reaction {
|
|
||||||
background-color: rgba(255, 255, 255, 0.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
.chatlog__reaction-count {
|
|
||||||
color: rgba(255, 255, 255, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Postamble */
|
|
||||||
|
|
||||||
.postamble {
|
|
||||||
border-color: rgba(255, 255, 255, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.postamble__entry {
|
|
||||||
color: #ffffff;
|
|
||||||
}
|
|
||||||
@@ -1,149 +0,0 @@
|
|||||||
/* General */
|
|
||||||
|
|
||||||
body {
|
|
||||||
background-color: #ffffff;
|
|
||||||
color: #23262a;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
color: #0068e0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.spoiler-text {
|
|
||||||
background-color: rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.spoiler--hidden .spoiler-text {
|
|
||||||
background-color: #b9bbbe;
|
|
||||||
}
|
|
||||||
|
|
||||||
.spoiler--hidden:hover .spoiler-text {
|
|
||||||
background-color: rgba(185, 187, 190, 0.8);
|
|
||||||
}
|
|
||||||
|
|
||||||
.quote {
|
|
||||||
border-color: #c7ccd1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pre {
|
|
||||||
background-color: #f9f9f9 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pre--multiline {
|
|
||||||
border-color: #f3f3f3 !important;
|
|
||||||
color: #657b83 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Preamble */
|
|
||||||
|
|
||||||
.preamble__entry {
|
|
||||||
color: #2f3136;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Chatlog */
|
|
||||||
|
|
||||||
.chatlog__message-group {
|
|
||||||
border-color: #eceeef;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chatlog__reference-symbol {
|
|
||||||
border-color: #c7ccd1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chatlog__reference {
|
|
||||||
color: #5f5f60;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chatlog__reference-link {
|
|
||||||
color: #5f5f60;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chatlog__reference-link:hover {
|
|
||||||
color: #2f3136;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chatlog__reference-edited-timestamp {
|
|
||||||
color: #747f8d;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chatlog__author-name {
|
|
||||||
font-weight: 600;
|
|
||||||
color: #2f3136;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chatlog__timestamp {
|
|
||||||
color: #747f8d;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chatlog__message--highlighted {
|
|
||||||
background-color: rgba(114, 137, 218, 0.2) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chatlog__message--pinned {
|
|
||||||
background-color: rgba(249, 168, 37, 0.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
.chatlog__attachment-container {
|
|
||||||
background-color: #f2f3f5;
|
|
||||||
border-color: #ebedef;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chatlog__edited-timestamp {
|
|
||||||
color: #747f8d;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chatlog__embed-color-pill--default {
|
|
||||||
background-color: rgba(227, 229, 232, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.chatlog__embed-content-container {
|
|
||||||
background-color: rgba(249, 249, 249, 0.3);
|
|
||||||
border-color: rgba(204, 204, 204, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.chatlog__embed-author-name {
|
|
||||||
color: #4f545c;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chatlog__embed-author-name-link {
|
|
||||||
color: #4f545c;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chatlog__embed-title {
|
|
||||||
color: #4f545c;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chatlog__embed-description {
|
|
||||||
color: #2e3338;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chatlog__embed-field-name {
|
|
||||||
color: #36393e;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chatlog__embed-field-value {
|
|
||||||
color: #2e3338;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chatlog__embed-footer {
|
|
||||||
color: #2e3338;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chatlog__reaction {
|
|
||||||
background-color: rgba(79, 84, 92, 0.06);
|
|
||||||
}
|
|
||||||
|
|
||||||
.chatlog__reaction-count {
|
|
||||||
color: #747f8d;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Postamble */
|
|
||||||
|
|
||||||
.postamble {
|
|
||||||
border-color: #eceeef;
|
|
||||||
}
|
|
||||||
|
|
||||||
.postamble__entry {
|
|
||||||
color: #2f3136;
|
|
||||||
}
|
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
@using System
|
@using System
|
||||||
@using System.Linq
|
@using System.Linq
|
||||||
@using System.Threading.Tasks
|
@using System.Threading.Tasks
|
||||||
|
@using DiscordChatExporter.Core.Exporting.Writers.Html;
|
||||||
|
|
||||||
@namespace DiscordChatExporter.Core.Exporting.Writers.Html
|
@namespace DiscordChatExporter.Core.Exporting.Writers.Html
|
||||||
@inherits MiniRazor.TemplateBase<DiscordChatExporter.Core.Exporting.Writers.Html.MessageGroupTemplateContext>
|
@inherits MiniRazor.TemplateBase<MessageGroupTemplateContext>
|
||||||
|
|
||||||
@{
|
@{
|
||||||
string FormatDate(DateTimeOffset date) => Model.ExportContext.FormatDate(date);
|
string FormatDate(DateTimeOffset date) => Model.ExportContext.FormatDate(date);
|
||||||
@@ -45,20 +47,24 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
<div class="chatlog__message-group">
|
<div class="chatlog__message-group">
|
||||||
|
@{/* Referenced message */}
|
||||||
@if (Model.MessageGroup.Reference is not null)
|
@if (Model.MessageGroup.Reference is not null)
|
||||||
{
|
{
|
||||||
<div class="chatlog__reference-symbol">
|
<div class="chatlog__reference-symbol"></div>
|
||||||
</div>
|
|
||||||
<div class="chatlog__reference">
|
<div class="chatlog__reference">
|
||||||
@if (Model.MessageGroup.ReferencedMessage is not null)
|
@if (Model.MessageGroup.ReferencedMessage is not null)
|
||||||
{
|
{
|
||||||
<img class="chatlog__reference-avatar" src="@await ResolveUrlAsync(Model.MessageGroup.ReferencedMessage.Author.AvatarUrl)" alt="Avatar">
|
<img class="chatlog__reference-avatar" src="@await ResolveUrlAsync(Model.MessageGroup.ReferencedMessage.Author.AvatarUrl)" alt="Avatar" loading="lazy">
|
||||||
<span class="chatlog__reference-name" title="@Model.MessageGroup.ReferencedMessage.Author.FullName" style="@referencedUserColorStyle">@referencedUserNick</span>
|
<span class="chatlog__reference-name" title="@Model.MessageGroup.ReferencedMessage.Author.FullName" style="@referencedUserColorStyle">@referencedUserNick</span>
|
||||||
<div class="chatlog__reference-link" onclick="scrollToMessage(event, '@Model.MessageGroup.ReferencedMessage.Id')">
|
<div class="chatlog__reference-content">
|
||||||
<span class="chatlog__reference-content">
|
<span class="chatlog__reference-link" onclick="scrollToMessage(event, '@Model.MessageGroup.ReferencedMessage.Id')">
|
||||||
@if (!string.IsNullOrWhiteSpace(Model.MessageGroup.ReferencedMessage.Content))
|
@if (!string.IsNullOrWhiteSpace(Model.MessageGroup.ReferencedMessage.Content))
|
||||||
{
|
{
|
||||||
@Raw(FormatMarkdown(Model.MessageGroup.ReferencedMessage.Content))
|
@Raw(FormatEmbedMarkdown(Model.MessageGroup.ReferencedMessage.Content))
|
||||||
|
}
|
||||||
|
else if (Model.MessageGroup.ReferencedMessage.Attachments.Any() || Model.MessageGroup.ReferencedMessage.Embeds.Any())
|
||||||
|
{
|
||||||
|
<em>Click to see attachment</em> <span>🖼️</span>
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -80,30 +86,37 @@
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@{/* Avatar */}
|
||||||
<div class="chatlog__author-avatar-container">
|
<div class="chatlog__author-avatar-container">
|
||||||
<img class="chatlog__author-avatar" src="@await ResolveUrlAsync(Model.MessageGroup.Author.AvatarUrl)" alt="Avatar">
|
<img class="chatlog__author-avatar" src="@await ResolveUrlAsync(Model.MessageGroup.Author.AvatarUrl)" alt="Avatar" loading="lazy">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="chatlog__messages">
|
<div class="chatlog__messages">
|
||||||
|
@{/* Author name */}
|
||||||
<span class="chatlog__author-name" title="@Model.MessageGroup.Author.FullName" data-user-id="@Model.MessageGroup.Author.Id" style="@userColorStyle">@userNick</span>
|
<span class="chatlog__author-name" title="@Model.MessageGroup.Author.FullName" data-user-id="@Model.MessageGroup.Author.Id" style="@userColorStyle">@userNick</span>
|
||||||
|
|
||||||
|
@{/* Bot tag */}
|
||||||
@if (Model.MessageGroup.Author.IsBot)
|
@if (Model.MessageGroup.Author.IsBot)
|
||||||
{
|
{
|
||||||
<span class="chatlog__bot-tag">BOT</span>
|
<span class="chatlog__bot-tag">BOT</span>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@{/* Message timestamp */}
|
||||||
<span class="chatlog__timestamp">@FormatDate(Model.MessageGroup.Timestamp)</span>
|
<span class="chatlog__timestamp">@FormatDate(Model.MessageGroup.Timestamp)</span>
|
||||||
|
|
||||||
|
@{/* Messages in a group */}
|
||||||
@foreach (var message in Model.MessageGroup.Messages)
|
@foreach (var message in Model.MessageGroup.Messages)
|
||||||
{
|
{
|
||||||
var isPinnedStyle = message.IsPinned ? "chatlog__message--pinned" : null;
|
<div class="chatlog__message @(message.IsPinned ? "chatlog__message--pinned" : null)" data-message-id="@message.Id" id="message-@message.Id" title="Message sent: @FormatDate(message.Timestamp)">
|
||||||
|
|
||||||
<div class="chatlog__message @isPinnedStyle" data-message-id="@message.Id" id="message-@message.Id" title="Message sent: @FormatDate(message.Timestamp)">
|
|
||||||
@if (!string.IsNullOrWhiteSpace(message.Content) || message.EditedTimestamp is not null)
|
@if (!string.IsNullOrWhiteSpace(message.Content) || message.EditedTimestamp is not null)
|
||||||
{
|
{
|
||||||
<div class="chatlog__content">
|
<div class="chatlog__content">
|
||||||
<div class="markdown">
|
<div class="markdown">
|
||||||
|
@{/* Message content */}
|
||||||
<span class="preserve-whitespace">@Raw(FormatMarkdown(message.Content))</span>
|
<span class="preserve-whitespace">@Raw(FormatMarkdown(message.Content))</span>
|
||||||
|
|
||||||
|
@{/* Edit timestamp */}
|
||||||
@if (message.EditedTimestamp is not null)
|
@if (message.EditedTimestamp is not null)
|
||||||
{
|
{
|
||||||
<span class="chatlog__edited-timestamp" title="@FormatDate(message.EditedTimestamp.Value)">(edited)</span>
|
<span class="chatlog__edited-timestamp" title="@FormatDate(message.EditedTimestamp.Value)">(edited)</span>
|
||||||
@@ -112,62 +125,64 @@
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@{/* Attachments */}
|
||||||
@foreach (var attachment in message.Attachments)
|
@foreach (var attachment in message.Attachments)
|
||||||
{
|
{
|
||||||
<div class="chatlog__attachment">
|
<div class="chatlog__attachment @(attachment.IsSpoiler ? "chatlog__attachment--hidden" : "")" onclick="@(attachment.IsSpoiler ? "showSpoiler(event, this)" : "")">
|
||||||
<div class="@(attachment.IsSpoiler ? "spoiler spoiler--hidden" : "")" onclick="@(attachment.IsSpoiler ? "showSpoiler(event, this)" : "")">
|
@{/* Spoiler caption */}
|
||||||
<div class="@(attachment.IsSpoiler ? "spoiler-image" : "")">
|
@if (attachment.IsSpoiler)
|
||||||
@if (attachment.IsImage)
|
{
|
||||||
{
|
<div class="chatlog__attachment-spoiler-caption">SPOILER</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@{/* Attachment preview */}
|
||||||
|
@if (attachment.IsImage)
|
||||||
|
{
|
||||||
|
<a href="@await ResolveUrlAsync(attachment.Url)">
|
||||||
|
<img class="chatlog__attachment-media" src="@await ResolveUrlAsync(attachment.Url)" alt="Image attachment" title="@($"Image: {attachment.FileName} ({attachment.FileSize})")" loading="lazy">
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
else if (attachment.IsVideo)
|
||||||
|
{
|
||||||
|
<video class="chatlog__attachment-media" controls>
|
||||||
|
<source src="@await ResolveUrlAsync(attachment.Url)" alt="Video attachment" title="@($"Video: {attachment.FileName} ({attachment.FileSize})")">
|
||||||
|
</video>
|
||||||
|
}
|
||||||
|
else if (attachment.IsAudio)
|
||||||
|
{
|
||||||
|
<audio class="chatlog__attachment-media" controls>
|
||||||
|
<source src="@await ResolveUrlAsync(attachment.Url)" alt="Audio attachment" title="@($"Audio: {attachment.FileName} ({attachment.FileSize})")">
|
||||||
|
</audio>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<div class="chatlog__attachment-generic">
|
||||||
|
<svg class="chatlog__attachment-generic-icon">
|
||||||
|
<use href="#icon-attachment" />
|
||||||
|
</svg>
|
||||||
|
<div class="chatlog__attachment-generic-name">
|
||||||
<a href="@await ResolveUrlAsync(attachment.Url)">
|
<a href="@await ResolveUrlAsync(attachment.Url)">
|
||||||
<img class="chatlog__attachment-thumbnail" src="@await ResolveUrlAsync(attachment.Url)" alt="Image attachment" title="@($"Image: {attachment.FileName} ({attachment.FileSize})")">
|
@attachment.FileName
|
||||||
</a>
|
</a>
|
||||||
}
|
</div>
|
||||||
else if (attachment.IsVideo)
|
<div class="chatlog__attachment-generic-size">
|
||||||
{
|
@attachment.FileSize
|
||||||
<video controls class="chatlog__attachment-thumbnail">
|
</div>
|
||||||
<source src="@await ResolveUrlAsync(attachment.Url)" alt="Video attachment" title="@($"Video: {attachment.FileName} ({attachment.FileSize})")">
|
|
||||||
</video>
|
|
||||||
}
|
|
||||||
else if (attachment.IsAudio)
|
|
||||||
{
|
|
||||||
<audio controls class="chatlog__attachment-thumbnail">
|
|
||||||
<source src="@await ResolveUrlAsync(attachment.Url)" alt="Audio attachment" title="@($"Audio: {attachment.FileName} ({attachment.FileSize})")">
|
|
||||||
</audio>
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
<div class="chatlog__attachment-container">
|
|
||||||
<svg class="chatlog__attachment-icon" viewBox="0 0 720 960">
|
|
||||||
<path class="a"/>
|
|
||||||
<path class="b"/>
|
|
||||||
<path class="c"/>
|
|
||||||
<path class="d"/>
|
|
||||||
</svg>
|
|
||||||
<div class="chatlog__attachment-filename">
|
|
||||||
<a href="@await ResolveUrlAsync(attachment.Url)">
|
|
||||||
@attachment.FileName
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div class="chatlog__attachment-filesize">
|
|
||||||
@attachment.FileSize
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@{/* Embeds */}
|
||||||
@foreach (var embed in message.Embeds)
|
@foreach (var embed in message.Embeds)
|
||||||
{
|
{
|
||||||
var youTubeVideo = embed.TryGetYouTubeVideo();
|
var youTubeVideo = embed.TryGetYouTubeVideo();
|
||||||
|
|
||||||
<div class="chatlog__embed">
|
<div class="chatlog__embed">
|
||||||
|
@{/* Color pill */}
|
||||||
@if (embed.Color is not null)
|
@if (embed.Color is not null)
|
||||||
{
|
{
|
||||||
var embedColorStyle = $"background-color: rgba({embed.Color?.R},{embed.Color?.G},{embed.Color?.B},{embed.Color?.A})";
|
<div class="chatlog__embed-color-pill" style="background-color: rgba(@embed.Color?.R,@embed.Color?.G,@embed.Color?.B,@embed.Color?.A)"></div>
|
||||||
<div class="chatlog__embed-color-pill" style="@embedColorStyle"></div>
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -177,12 +192,13 @@
|
|||||||
<div class="chatlog__embed-content-container">
|
<div class="chatlog__embed-content-container">
|
||||||
<div class="chatlog__embed-content">
|
<div class="chatlog__embed-content">
|
||||||
<div class="chatlog__embed-text">
|
<div class="chatlog__embed-text">
|
||||||
|
@{/* Embed author */}
|
||||||
@if (embed.Author is not null)
|
@if (embed.Author is not null)
|
||||||
{
|
{
|
||||||
<div class="chatlog__embed-author">
|
<div class="chatlog__embed-author">
|
||||||
@if (!string.IsNullOrWhiteSpace(embed.Author.IconUrl))
|
@if (!string.IsNullOrWhiteSpace(embed.Author.IconUrl))
|
||||||
{
|
{
|
||||||
<img class="chatlog__embed-author-icon" src="@await ResolveUrlAsync(embed.Author.IconProxyUrl ?? embed.Author.IconUrl)" alt="Author icon">
|
<img class="chatlog__embed-author-icon" src="@await ResolveUrlAsync(embed.Author.IconProxyUrl ?? embed.Author.IconUrl)" alt="Author icon" loading="lazy" onerror="this.style.visibility='hidden'">
|
||||||
}
|
}
|
||||||
|
|
||||||
@if (!string.IsNullOrWhiteSpace(embed.Author.Name))
|
@if (!string.IsNullOrWhiteSpace(embed.Author.Name))
|
||||||
@@ -201,6 +217,7 @@
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@{/* Embed title */}
|
||||||
@if (!string.IsNullOrWhiteSpace(embed.Title))
|
@if (!string.IsNullOrWhiteSpace(embed.Title))
|
||||||
{
|
{
|
||||||
<div class="chatlog__embed-title">
|
<div class="chatlog__embed-title">
|
||||||
@@ -217,6 +234,7 @@
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@{/* Embed description (with special casing for YouTube videos) */}
|
||||||
@if (youTubeVideo is not null)
|
@if (youTubeVideo is not null)
|
||||||
{
|
{
|
||||||
<div class="chatlog__embed-youtube-container">
|
<div class="chatlog__embed-youtube-container">
|
||||||
@@ -230,14 +248,13 @@
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@{/* Embed fields */}
|
||||||
@if (embed.Fields.Any())
|
@if (embed.Fields.Any())
|
||||||
{
|
{
|
||||||
<div class="chatlog__embed-fields">
|
<div class="chatlog__embed-fields">
|
||||||
@foreach (var field in embed.Fields)
|
@foreach (var field in embed.Fields)
|
||||||
{
|
{
|
||||||
var isInlineStyle = field.IsInline ? "chatlog__embed-field--inline" : null;
|
<div class="chatlog__embed-field @(field.IsInline ? "chatlog__embed-field--inline" : null)">
|
||||||
|
|
||||||
<div class="chatlog__embed-field @isInlineStyle">
|
|
||||||
@if (!string.IsNullOrWhiteSpace(field.Name))
|
@if (!string.IsNullOrWhiteSpace(field.Name))
|
||||||
{
|
{
|
||||||
<div class="chatlog__embed-field-name">
|
<div class="chatlog__embed-field-name">
|
||||||
@@ -257,34 +274,39 @@
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@{/* Embed content (not shown for YouTube videos) */}
|
||||||
@if (embed.Thumbnail is not null && !string.IsNullOrWhiteSpace(embed.Thumbnail.Url) && youTubeVideo is null)
|
@if (embed.Thumbnail is not null && !string.IsNullOrWhiteSpace(embed.Thumbnail.Url) && youTubeVideo is null)
|
||||||
{
|
{
|
||||||
<div class="chatlog__embed-thumbnail-container">
|
<div class="chatlog__embed-thumbnail-container">
|
||||||
<a class="chatlog__embed-thumbnail-link" href="@await ResolveUrlAsync(embed.Thumbnail.ProxyUrl ?? embed.Thumbnail.Url)">
|
<a class="chatlog__embed-thumbnail-link" href="@await ResolveUrlAsync(embed.Thumbnail.ProxyUrl ?? embed.Thumbnail.Url)">
|
||||||
<img class="chatlog__embed-thumbnail" src="@await ResolveUrlAsync(embed.Thumbnail.ProxyUrl ?? embed.Thumbnail.Url)" alt="Thumbnail">
|
<img class="chatlog__embed-thumbnail" src="@await ResolveUrlAsync(embed.Thumbnail.ProxyUrl ?? embed.Thumbnail.Url)" alt="Thumbnail" loading="lazy">
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@{/* Embed image */}
|
||||||
@if (embed.Image is not null && !string.IsNullOrWhiteSpace(embed.Image.Url))
|
@if (embed.Image is not null && !string.IsNullOrWhiteSpace(embed.Image.Url))
|
||||||
{
|
{
|
||||||
<div class="chatlog__embed-image-container">
|
<div class="chatlog__embed-image-container">
|
||||||
<a class="chatlog__embed-image-link" href="@await ResolveUrlAsync(embed.Image.ProxyUrl ?? embed.Image.Url)">
|
<a class="chatlog__embed-image-link" href="@await ResolveUrlAsync(embed.Image.ProxyUrl ?? embed.Image.Url)">
|
||||||
<img class="chatlog__embed-image" src="@await ResolveUrlAsync(embed.Image.ProxyUrl ?? embed.Image.Url)" alt="Image">
|
<img class="chatlog__embed-image" src="@await ResolveUrlAsync(embed.Image.ProxyUrl ?? embed.Image.Url)" alt="Image" loading="lazy">
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@{/* Embed footer & icon */}
|
||||||
@if (embed.Footer is not null || embed.Timestamp is not null)
|
@if (embed.Footer is not null || embed.Timestamp is not null)
|
||||||
{
|
{
|
||||||
<div class="chatlog__embed-footer">
|
<div class="chatlog__embed-footer">
|
||||||
|
@{/* Footer icon */}
|
||||||
@if (!string.IsNullOrWhiteSpace(embed.Footer?.IconUrl))
|
@if (!string.IsNullOrWhiteSpace(embed.Footer?.IconUrl))
|
||||||
{
|
{
|
||||||
<img class="chatlog__embed-footer-icon" src="@await ResolveUrlAsync(embed.Footer.IconProxyUrl ?? embed.Footer.IconUrl)" alt="Footer icon">
|
<img class="chatlog__embed-footer-icon" src="@await ResolveUrlAsync(embed.Footer.IconProxyUrl ?? embed.Footer.IconUrl)" alt="Footer icon" loading="lazy">
|
||||||
}
|
}
|
||||||
|
|
||||||
<span class="chatlog__embed-footer-text">
|
<span class="chatlog__embed-footer-text">
|
||||||
|
@{/* Footer text */}
|
||||||
@if (!string.IsNullOrWhiteSpace(embed.Footer?.Text))
|
@if (!string.IsNullOrWhiteSpace(embed.Footer?.Text))
|
||||||
{
|
{
|
||||||
@embed.Footer.Text
|
@embed.Footer.Text
|
||||||
@@ -295,6 +317,7 @@
|
|||||||
@(" • ")
|
@(" • ")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@{/* Embed timestamp */}
|
||||||
@if (embed.Timestamp is not null)
|
@if (embed.Timestamp is not null)
|
||||||
{
|
{
|
||||||
@FormatDate(embed.Timestamp.Value)
|
@FormatDate(embed.Timestamp.Value)
|
||||||
@@ -306,13 +329,14 @@
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@{/* Message reactions */}
|
||||||
@if (message.Reactions.Any())
|
@if (message.Reactions.Any())
|
||||||
{
|
{
|
||||||
<div class="chatlog__reactions">
|
<div class="chatlog__reactions">
|
||||||
@foreach (var reaction in message.Reactions)
|
@foreach (var reaction in message.Reactions)
|
||||||
{
|
{
|
||||||
<div class="chatlog__reaction" title="@reaction.Emoji.Code">
|
<div class="chatlog__reaction" title="@reaction.Emoji.Code">
|
||||||
<img class="emoji emoji--small" alt="@reaction.Emoji.Name" src="@await ResolveUrlAsync(reaction.Emoji.ImageUrl)">
|
<img class="emoji emoji--small" alt="@reaction.Emoji.Name" src="@await ResolveUrlAsync(reaction.Emoji.ImageUrl)" loading="lazy">
|
||||||
<span class="chatlog__reaction-count">@reaction.Count</span>
|
<span class="chatlog__reaction-count">@reaction.Count</span>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
@namespace DiscordChatExporter.Core.Exporting.Writers.Html
|
@using DiscordChatExporter.Core.Exporting.Writers.Html;
|
||||||
@inherits MiniRazor.TemplateBase<DiscordChatExporter.Core.Exporting.Writers.Html.PostambleTemplateContext>
|
|
||||||
|
|
||||||
|
@namespace DiscordChatExporter.Core.Exporting.Writers.Html
|
||||||
|
@inherits MiniRazor.TemplateBase<PostambleTemplateContext>
|
||||||
|
|
||||||
|
@{/* Close elements opened by preamble */}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="postamble">
|
<div class="postamble">
|
||||||
|
|||||||
@@ -1,15 +1,19 @@
|
|||||||
@using System
|
@using System
|
||||||
@using System.Threading.Tasks
|
@using System.Threading.Tasks
|
||||||
@using Tyrrrz.Extensions
|
@using DiscordChatExporter.Core.Exporting.Writers.Html;
|
||||||
|
|
||||||
@namespace DiscordChatExporter.Core.Exporting.Writers.Html
|
@namespace DiscordChatExporter.Core.Exporting.Writers.Html
|
||||||
@inherits MiniRazor.TemplateBase<DiscordChatExporter.Core.Exporting.Writers.Html.PreambleTemplateContext>
|
@inherits MiniRazor.TemplateBase<PreambleTemplateContext>
|
||||||
|
|
||||||
@{
|
@{
|
||||||
|
string Themed(string darkVariant, string lightVariant) =>
|
||||||
|
string.Equals(Model.ThemeName, "Dark", StringComparison.OrdinalIgnoreCase)
|
||||||
|
? darkVariant
|
||||||
|
: lightVariant;
|
||||||
|
|
||||||
string FormatDate(DateTimeOffset date) => Model.ExportContext.FormatDate(date);
|
string FormatDate(DateTimeOffset date) => Model.ExportContext.FormatDate(date);
|
||||||
|
|
||||||
ValueTask<string> ResolveUrlAsync(string url) => Model.ExportContext.ResolveMediaUrlAsync(url);
|
ValueTask<string> ResolveUrlAsync(string url) => Model.ExportContext.ResolveMediaUrlAsync(url);
|
||||||
|
|
||||||
string GetStyleSheet(string name) => Model.GetType().Assembly.GetManifestResourceString($"DiscordChatExporter.Core.Exporting.Writers.Html.{name}.css");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
@@ -20,13 +24,595 @@
|
|||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width">
|
<meta name="viewport" content="width=device-width">
|
||||||
|
|
||||||
|
@{/* Styling */}
|
||||||
<style>
|
<style>
|
||||||
@Raw(GetStyleSheet("Core"))
|
@@font-face {
|
||||||
</style>
|
font-family: Whitney;
|
||||||
<style>
|
src: url(https://cdn.jsdelivr.net/gh/Tyrrrz/DiscordFonts@master/whitney-300.woff);
|
||||||
@Raw(GetStyleSheet(Model.ThemeName))
|
font-weight: 300;
|
||||||
|
}
|
||||||
|
|
||||||
|
@@font-face {
|
||||||
|
font-family: Whitney;
|
||||||
|
src: url(https://cdn.jsdelivr.net/gh/Tyrrrz/DiscordFonts@master/whitney-400.woff);
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
@@font-face {
|
||||||
|
font-family: Whitney;
|
||||||
|
src: url(https://cdn.jsdelivr.net/gh/Tyrrrz/DiscordFonts@master/whitney-500.woff);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
@@font-face {
|
||||||
|
font-family: Whitney;
|
||||||
|
src: url(https://cdn.jsdelivr.net/gh/Tyrrrz/DiscordFonts@master/whitney-600.woff);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
@@font-face {
|
||||||
|
font-family: Whitney;
|
||||||
|
src: url(https://cdn.jsdelivr.net/gh/Tyrrrz/DiscordFonts@master/whitney-700.woff);
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-color: @Themed("#36393e", "#ffffff");
|
||||||
|
color: @Themed("#dcddde", "#23262a");
|
||||||
|
font-family: "Whitney", "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||||
|
font-size: 17px;
|
||||||
|
font-weight: @Themed("400", "500");
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: @Themed("#00aff4", "#0068e0");
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown {
|
||||||
|
max-width: 100%;
|
||||||
|
line-height: 1.3;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preserve-whitespace {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spoiler-text {
|
||||||
|
background-color: @Themed("rgba(255, 255, 255, 0.1)", "rgba(0, 0, 0, 0.1)");
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spoiler-text--hidden {
|
||||||
|
cursor: pointer;
|
||||||
|
background-color: @Themed("#202225", "#b9bbbe");
|
||||||
|
color: rgba(0, 0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.spoiler-text--hidden:hover {
|
||||||
|
background-color: @Themed("rgba(32, 34, 37, 0.8)", "rgba(185, 187, 190, 0.8)");
|
||||||
|
}
|
||||||
|
|
||||||
|
.spoiler-text--hidden::selection {
|
||||||
|
color: rgba(0, 0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.quote {
|
||||||
|
margin: 0.1em 0;
|
||||||
|
padding-left: 0.6em;
|
||||||
|
border-left: 4px solid @Themed("#4f545c", "#c7ccd1");
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pre {
|
||||||
|
background-color: @Themed("#2f3136", "#f9f9f9");
|
||||||
|
font-family: "Consolas", "Courier New", Courier, monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pre--multiline {
|
||||||
|
margin-top: 0.25em;
|
||||||
|
padding: 0.5em;
|
||||||
|
border: 2px solid @Themed("#282b30", "#f3f3f3");
|
||||||
|
border-radius: 5px;
|
||||||
|
color: @Themed("#b9bbbe", "#657b83");
|
||||||
|
}
|
||||||
|
|
||||||
|
@{/* Override Highlight.js styles with a higher specificity selector */}
|
||||||
|
.pre--multiline.hljs {
|
||||||
|
background-color: @Themed("#2f3136", "#f9f9f9");
|
||||||
|
color: @Themed("#b9bbbe", "#657b83");
|
||||||
|
}
|
||||||
|
|
||||||
|
.pre--inline {
|
||||||
|
padding: 2px;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 0.85em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mention {
|
||||||
|
border-radius: 3px;
|
||||||
|
padding: 0 2px;
|
||||||
|
color: #7289da;
|
||||||
|
background-color: rgba(114, 137, 218, .1);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timestamp {
|
||||||
|
border-radius: 3px;
|
||||||
|
padding: 0 2px;
|
||||||
|
background-color: @Themed("rgba(255, 255, 255, 0.06)", "rgba(6, 6, 7, 0.08)");
|
||||||
|
}
|
||||||
|
|
||||||
|
.emoji {
|
||||||
|
width: 1.325em;
|
||||||
|
height: 1.325em;
|
||||||
|
margin: 0 0.06em;
|
||||||
|
vertical-align: -0.4em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emoji--small {
|
||||||
|
width: 1em;
|
||||||
|
height: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emoji--large {
|
||||||
|
width: 2.8em;
|
||||||
|
height: 2.8em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preamble {
|
||||||
|
display: grid;
|
||||||
|
margin: 0 0.3em 0.6em 0.3em;
|
||||||
|
max-width: 100%;
|
||||||
|
grid-template-columns: auto 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preamble__guild-icon-container {
|
||||||
|
grid-column: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preamble__guild-icon {
|
||||||
|
max-width: 88px;
|
||||||
|
max-height: 88px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preamble__entries-container {
|
||||||
|
grid-column: 2;
|
||||||
|
margin-left: 0.6em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preamble__entry {
|
||||||
|
font-size: 1.4em;
|
||||||
|
color: @Themed("#ffffff", "#2f3136");
|
||||||
|
}
|
||||||
|
|
||||||
|
.preamble__entry--small {
|
||||||
|
font-size: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chatlog {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chatlog__message-group {
|
||||||
|
display: grid;
|
||||||
|
margin: 0 0.6em;
|
||||||
|
padding: 0.9em 0;
|
||||||
|
border-top: 1px solid @Themed("rgba(255, 255, 255, 0.1)", "#eceeef");
|
||||||
|
grid-template-columns: auto 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chatlog__reference-symbol {
|
||||||
|
grid-column: 1;
|
||||||
|
margin: 8px 4px 4px 18px;
|
||||||
|
border-left: 2px solid @Themed("#4f545c", "#c7ccd1");
|
||||||
|
border-top: 2px solid @Themed("#4f545c", "#c7ccd1");
|
||||||
|
border-radius: 8px 0 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chatlog__reference {
|
||||||
|
display: flex;
|
||||||
|
grid-column: 2;
|
||||||
|
margin-bottom: 0.25em;
|
||||||
|
font-size: 0.875em;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
align-items: center;
|
||||||
|
color: @Themed("#b5b6b8", "#5f5f60");
|
||||||
|
}
|
||||||
|
|
||||||
|
.chatlog__reference-avatar {
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
margin-right: 0.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chatlog__reference-name {
|
||||||
|
margin-right: 0.25em;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chatlog__reference-content {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chatlog__reference-content a:hover {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chatlog__reference-link {
|
||||||
|
cursor: pointer;
|
||||||
|
color: @Themed("#b5b6b8", "#5f5f60");
|
||||||
|
}
|
||||||
|
|
||||||
|
.chatlog__reference-link * {
|
||||||
|
display: inline;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chatlog__reference-link:hover {
|
||||||
|
color: @Themed("#ffffff", "#2f3136");
|
||||||
|
}
|
||||||
|
|
||||||
|
.chatlog__reference-link:hover *:not(.spoiler-text) {
|
||||||
|
color: @Themed("#ffffff", "#2f3136");
|
||||||
|
}
|
||||||
|
|
||||||
|
.chatlog__reference-edited-timestamp {
|
||||||
|
margin-left: 0.25em;
|
||||||
|
font-size: 0.8em;
|
||||||
|
color: @Themed("rgba(255, 255, 255, 0.2)", "#747f8d");
|
||||||
|
}
|
||||||
|
|
||||||
|
.chatlog__author-avatar-container {
|
||||||
|
grid-column: 1;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
margin-right: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chatlog__author-avatar {
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chatlog__messages {
|
||||||
|
grid-column: 2;
|
||||||
|
min-width: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chatlog__author-name {
|
||||||
|
font-weight: @Themed("500", "600");
|
||||||
|
color: @Themed("#ffffff", "#2f3136");
|
||||||
|
}
|
||||||
|
|
||||||
|
.chatlog__timestamp {
|
||||||
|
margin-left: 0.3em;
|
||||||
|
font-size: 0.75em;
|
||||||
|
color: @Themed("rgba(255, 255, 255, 0.2)", "#747f8d");
|
||||||
|
}
|
||||||
|
|
||||||
|
.chatlog__message {
|
||||||
|
padding: 0.1em 0.3em;
|
||||||
|
margin: 0 -0.3em;
|
||||||
|
background-color: transparent;
|
||||||
|
transition: background-color 1s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chatlog__message--highlighted {
|
||||||
|
background-color: @Themed("rgba(114, 137, 218, 0.2)", "rgba(114, 137, 218, 0.2)");
|
||||||
|
}
|
||||||
|
|
||||||
|
.chatlog__message--pinned {
|
||||||
|
background-color: @Themed("rgba(249, 168, 37, 0.05)", "rgba(249, 168, 37, 0.05)");
|
||||||
|
}
|
||||||
|
|
||||||
|
.chatlog__content {
|
||||||
|
font-size: 0.95em;
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chatlog__edited-timestamp {
|
||||||
|
margin-left: 0.15em;
|
||||||
|
font-size: 0.8em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chatlog__attachment {
|
||||||
|
display: inline-block;
|
||||||
|
position: relative;
|
||||||
|
margin-top: 0.3em;
|
||||||
|
border-radius: 3px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chatlog__attachment--hidden {
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow: 0 0 1px 1px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chatlog__attachment--hidden * {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chatlog__attachment-spoiler-caption {
|
||||||
|
display: none;
|
||||||
|
z-index: 999;
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
top: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
padding: 0.4em 0.8em;
|
||||||
|
background-color: rgba(0, 0, 0, 0.9);
|
||||||
|
border-radius: 20px;
|
||||||
|
color: #dcddde;
|
||||||
|
font-size: 0.9em;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chatlog__attachment--hidden .chatlog__attachment-spoiler-caption {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chatlog__attachment--hidden:hover .chatlog__attachment-spoiler-caption {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chatlog__attachment-media {
|
||||||
|
vertical-align: top;
|
||||||
|
max-width: 45vw;
|
||||||
|
max-height: 500px;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chatlog__attachment--hidden .chatlog__attachment-media {
|
||||||
|
filter: blur(44px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chatlog__attachment-generic {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 520px;
|
||||||
|
height: 40px;
|
||||||
|
padding: 10px;
|
||||||
|
background-color: @Themed("#2f3136", "#f2f3f5");
|
||||||
|
border: 1px solid @Themed("#292b2f", "#ebedef");
|
||||||
|
border-radius: 3px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chatlog__attachment--hidden .chatlog__attachment-generic {
|
||||||
|
filter: blur(44px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chatlog__attachment-generic-icon {
|
||||||
|
float: left;
|
||||||
|
width: 30px;
|
||||||
|
height: 100%;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chatlog__attachment-generic-size {
|
||||||
|
color: #72767d;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chatlog__attachment-generic-name {
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chatlog__edited-timestamp {
|
||||||
|
color: @Themed("rgba(255, 255, 255, 0.2)", "#747f8d");
|
||||||
|
}
|
||||||
|
|
||||||
|
.chatlog__embed {
|
||||||
|
display: flex;
|
||||||
|
margin-top: 0.3em;
|
||||||
|
max-width: 520px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chatlog__embed-color-pill {
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 0.25em;
|
||||||
|
border-top-left-radius: 3px;
|
||||||
|
border-bottom-left-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chatlog__embed-color-pill--default {
|
||||||
|
background-color: @Themed("#202225", "rgba(227, 229, 232, 1)");
|
||||||
|
}
|
||||||
|
|
||||||
|
.chatlog__embed-content-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 0.5em 0.6em;
|
||||||
|
background-color: @Themed("rgba(46, 48, 54, 0.3)", "rgba(249, 249, 249, 0.3)");
|
||||||
|
border: 1px solid @Themed("rgba(46, 48, 54, 0.6)", "rgba(204, 204, 204, 0.3)");
|
||||||
|
border-top-right-radius: 3px;
|
||||||
|
border-bottom-right-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chatlog__embed-content {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chatlog__embed-text {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chatlog__embed-author {
|
||||||
|
display: flex;
|
||||||
|
margin-bottom: 0.3em;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chatlog__embed-author-icon {
|
||||||
|
margin-right: 0.5em;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chatlog__embed-author-name {
|
||||||
|
font-size: 0.875em;
|
||||||
|
font-weight: 600;
|
||||||
|
color: @Themed("#ffffff", "#4f545c")
|
||||||
|
}
|
||||||
|
|
||||||
|
.chatlog__embed-author-name-link {
|
||||||
|
color: @Themed("#ffffff", "#4f545c");
|
||||||
|
}
|
||||||
|
|
||||||
|
.chatlog__embed-title {
|
||||||
|
margin-bottom: 0.2em;
|
||||||
|
font-size: 0.875em;
|
||||||
|
font-weight: 600;
|
||||||
|
color: @Themed("#ffffff", "#4f545c");
|
||||||
|
}
|
||||||
|
|
||||||
|
.chatlog__embed-description {
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 0.85em;
|
||||||
|
color: @Themed("#dcddde", "#2e3338");
|
||||||
|
}
|
||||||
|
|
||||||
|
.chatlog__embed-fields {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chatlog__embed-field {
|
||||||
|
flex: 0;
|
||||||
|
min-width: 100%;
|
||||||
|
max-width: 506px;
|
||||||
|
padding-top: 0.6em;
|
||||||
|
font-size: 0.875em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chatlog__embed-field--inline {
|
||||||
|
flex: 1;
|
||||||
|
flex-basis: auto;
|
||||||
|
min-width: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chatlog__embed-field-name {
|
||||||
|
margin-bottom: 0.2em;
|
||||||
|
font-weight: 600;
|
||||||
|
color: @Themed("#ffffff", "#36393e");
|
||||||
|
}
|
||||||
|
|
||||||
|
.chatlog__embed-field-value {
|
||||||
|
font-weight: 500;
|
||||||
|
color: @Themed("#dcddde", "#2e3338");
|
||||||
|
}
|
||||||
|
|
||||||
|
.chatlog__embed-thumbnail {
|
||||||
|
flex: 0;
|
||||||
|
margin-left: 1.2em;
|
||||||
|
max-width: 80px;
|
||||||
|
max-height: 80px;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chatlog__embed-image-container {
|
||||||
|
margin-top: 0.6em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chatlog__embed-image {
|
||||||
|
max-width: 500px;
|
||||||
|
max-height: 400px;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chatlog__embed-footer {
|
||||||
|
margin-top: 0.6em;
|
||||||
|
color: @Themed("#dcddde", "#2e3338");
|
||||||
|
}
|
||||||
|
|
||||||
|
.chatlog__embed-footer-icon {
|
||||||
|
margin-right: 0.2em;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border-radius: 50%;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chatlog__embed-footer-text {
|
||||||
|
vertical-align: middle;
|
||||||
|
font-size: 0.75em;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chatlog__embed-youtube-container {
|
||||||
|
margin-top: 0.6em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chatlog__embed-youtube {
|
||||||
|
border: 0;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chatlog__reactions {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chatlog__reaction {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin: 0.35em 0.1em 0.1em 0.1em;
|
||||||
|
padding: 0.2em 0.35em;
|
||||||
|
background-color: @Themed("rgba(255, 255, 255, 0.05)", "rgba(79, 84, 92, 0.06)");
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chatlog__reaction-count {
|
||||||
|
min-width: 9px;
|
||||||
|
margin-left: 0.35em;
|
||||||
|
font-size: 0.875em;
|
||||||
|
color: @Themed("rgba(255, 255, 255, 0.3)", "#747f8d");
|
||||||
|
}
|
||||||
|
|
||||||
|
.chatlog__bot-tag {
|
||||||
|
position: relative;
|
||||||
|
top: -.2em;
|
||||||
|
margin-left: 0.3em;
|
||||||
|
padding: 0.05em 0.3em;
|
||||||
|
border-radius: 3px;
|
||||||
|
vertical-align: middle;
|
||||||
|
line-height: 1.3;
|
||||||
|
background-color: #5865F2;
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 0.625em;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.postamble {
|
||||||
|
margin: 1.4em 0.3em 0.6em 0.3em;
|
||||||
|
padding: 1em;
|
||||||
|
border-top: 1px solid @Themed("rgba(255, 255, 255, 0.1)", "#eceeef");
|
||||||
|
}
|
||||||
|
|
||||||
|
.postamble__entry {
|
||||||
|
color: @Themed("#ffffff", "#2f3136");
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
@{/* Syntax highlighting */}
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.15.6/styles/solarized-@(Model.ThemeName.ToLowerInvariant()).min.css">
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.15.6/styles/solarized-@(Model.ThemeName.ToLowerInvariant()).min.css">
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.15.6/highlight.min.js"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.15.6/highlight.min.js"></script>
|
||||||
<script>
|
<script>
|
||||||
@@ -35,6 +621,7 @@
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@{/* Scripts */}
|
||||||
<script>
|
<script>
|
||||||
function scrollToMessage(event, id) {
|
function scrollToMessage(event, id) {
|
||||||
var element = document.getElementById('message-' + id);
|
var element = document.getElementById('message-' + id);
|
||||||
@@ -56,18 +643,32 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function showSpoiler(event, element) {
|
function showSpoiler(event, element) {
|
||||||
if (element && element.classList.contains('spoiler--hidden')) {
|
if (element && element.classList.contains('spoiler-text--hidden')) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
element.classList.remove('spoiler--hidden');
|
element.classList.remove('spoiler-text--hidden');
|
||||||
|
}
|
||||||
|
if (element && element.classList.contains('chatlog__attachment--hidden')) {
|
||||||
|
event.preventDefault();
|
||||||
|
element.classList.remove('chatlog__attachment--hidden');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@{/* Icons */}
|
||||||
|
<svg style="display: none">
|
||||||
|
<symbol id="icon-attachment" viewBox="0 0 720 960">
|
||||||
|
<path fill="#f4f5fb" d="M50,935a25,25,0,0,1-25-25V50A25,25,0,0,1,50,25H519.6L695,201.32V910a25,25,0,0,1-25,25Z" />
|
||||||
|
<path fill="#7789c4" d="M509.21,50,670,211.63V910H50V50H509.21M530,0H50A50,50,0,0,0,0,50V910a50,50,0,0,0,50,50H670a50,50,0,0,0,50-50h0V191Z" />
|
||||||
|
<path fill="#f4f5fb" d="M530,215a25,25,0,0,1-25-25V50a25,25,0,0,1,16.23-23.41L693.41,198.77A25,25,0,0,1,670,215Z" />
|
||||||
|
<path fill="#7789c4" d="M530,70.71,649.29,190H530V70.71M530,0a50,50,0,0,0-50,50V190a50,50,0,0,0,50,50H670a50,50,0,0,0,50-50Z" />
|
||||||
|
</symbol>
|
||||||
|
</svg>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
<div class="preamble">
|
<div class="preamble">
|
||||||
<div class="preamble__guild-icon-container">
|
<div class="preamble__guild-icon-container">
|
||||||
<img class="preamble__guild-icon" src="@await ResolveUrlAsync(Model.ExportContext.Request.Guild.IconUrl)" alt="Guild icon">
|
<img class="preamble__guild-icon" src="@await ResolveUrlAsync(Model.ExportContext.Request.Guild.IconUrl)" alt="Guild icon" loading="lazy">
|
||||||
</div>
|
</div>
|
||||||
<div class="preamble__entries-container">
|
<div class="preamble__entries-container">
|
||||||
<div class="preamble__entry">@Model.ExportContext.Request.Guild.Name</div>
|
<div class="preamble__entry">@Model.ExportContext.Request.Guild.Name</div>
|
||||||
@@ -98,4 +699,5 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@{/* Preamble cuts off at this point */}
|
||||||
<div class="chatlog">
|
<div class="chatlog">
|
||||||
+74
-63
@@ -6,7 +6,7 @@ using System.Text.RegularExpressions;
|
|||||||
using DiscordChatExporter.Core.Discord;
|
using DiscordChatExporter.Core.Discord;
|
||||||
using DiscordChatExporter.Core.Discord.Data;
|
using DiscordChatExporter.Core.Discord.Data;
|
||||||
using DiscordChatExporter.Core.Markdown;
|
using DiscordChatExporter.Core.Markdown;
|
||||||
using DiscordChatExporter.Core.Markdown.Ast;
|
using DiscordChatExporter.Core.Markdown.Parsing;
|
||||||
using DiscordChatExporter.Core.Utils.Extensions;
|
using DiscordChatExporter.Core.Utils.Extensions;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Core.Exporting.Writers.MarkdownVisitors
|
namespace DiscordChatExporter.Core.Exporting.Writers.MarkdownVisitors
|
||||||
@@ -39,7 +39,7 @@ namespace DiscordChatExporter.Core.Exporting.Writers.MarkdownVisitors
|
|||||||
TextFormatting.Underline => ("<u>", "</u>"),
|
TextFormatting.Underline => ("<u>", "</u>"),
|
||||||
TextFormatting.Strikethrough => ("<s>", "</s>"),
|
TextFormatting.Strikethrough => ("<s>", "</s>"),
|
||||||
TextFormatting.Spoiler => (
|
TextFormatting.Spoiler => (
|
||||||
"<span class=\"spoiler spoiler--hidden\" onclick=\"showSpoiler(event, this)\"><span class=\"spoiler-text\">", "</span></span>"),
|
"<span class=\"spoiler-text spoiler-text--hidden\" onclick=\"showSpoiler(event, this)\">", "</span>"),
|
||||||
TextFormatting.Quote => ("<div class=\"quote\">", "</div>"),
|
TextFormatting.Quote => ("<div class=\"quote\">", "</div>"),
|
||||||
_ => throw new ArgumentOutOfRangeException(nameof(formatted.Formatting))
|
_ => throw new ArgumentOutOfRangeException(nameof(formatted.Formatting))
|
||||||
};
|
};
|
||||||
@@ -75,67 +75,6 @@ namespace DiscordChatExporter.Core.Exporting.Writers.MarkdownVisitors
|
|||||||
return base.VisitMultiLineCodeBlock(multiLineCodeBlock);
|
return base.VisitMultiLineCodeBlock(multiLineCodeBlock);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override MarkdownNode VisitMention(MentionNode mention)
|
|
||||||
{
|
|
||||||
var mentionId = Snowflake.TryParse(mention.Id);
|
|
||||||
if (mention.Type == MentionType.Meta)
|
|
||||||
{
|
|
||||||
_buffer
|
|
||||||
.Append("<span class=\"mention\">")
|
|
||||||
.Append("@").Append(HtmlEncode(mention.Id))
|
|
||||||
.Append("</span>");
|
|
||||||
}
|
|
||||||
else if (mention.Type == MentionType.User)
|
|
||||||
{
|
|
||||||
var member = mentionId?.Pipe(_context.TryGetMember);
|
|
||||||
var fullName = member?.User.FullName ?? "Unknown";
|
|
||||||
var nick = member?.Nick ?? "Unknown";
|
|
||||||
|
|
||||||
_buffer
|
|
||||||
.Append($"<span class=\"mention\" title=\"{HtmlEncode(fullName)}\">")
|
|
||||||
.Append("@").Append(HtmlEncode(nick))
|
|
||||||
.Append("</span>");
|
|
||||||
}
|
|
||||||
else if (mention.Type == MentionType.Channel)
|
|
||||||
{
|
|
||||||
var channel = mentionId?.Pipe(_context.TryGetChannel);
|
|
||||||
var name = channel?.Name ?? "deleted-channel";
|
|
||||||
|
|
||||||
_buffer
|
|
||||||
.Append("<span class=\"mention\">")
|
|
||||||
.Append("#").Append(HtmlEncode(name))
|
|
||||||
.Append("</span>");
|
|
||||||
}
|
|
||||||
else if (mention.Type == MentionType.Role)
|
|
||||||
{
|
|
||||||
var role = mentionId?.Pipe(_context.TryGetRole);
|
|
||||||
var name = role?.Name ?? "deleted-role";
|
|
||||||
var color = role?.Color;
|
|
||||||
|
|
||||||
var style = color is not null
|
|
||||||
? $"color: rgb({color?.R}, {color?.G}, {color?.B}); background-color: rgba({color?.R}, {color?.G}, {color?.B}, 0.1);"
|
|
||||||
: "";
|
|
||||||
|
|
||||||
_buffer
|
|
||||||
.Append($"<span class=\"mention\" style=\"{style}\">")
|
|
||||||
.Append("@").Append(HtmlEncode(name))
|
|
||||||
.Append("</span>");
|
|
||||||
}
|
|
||||||
|
|
||||||
return base.VisitMention(mention);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override MarkdownNode VisitEmoji(EmojiNode emoji)
|
|
||||||
{
|
|
||||||
var emojiImageUrl = Emoji.GetImageUrl(emoji.Id, emoji.Name, emoji.IsAnimated);
|
|
||||||
var jumboClass = _isJumbo ? "emoji--large" : "";
|
|
||||||
|
|
||||||
_buffer
|
|
||||||
.Append($"<img class=\"emoji {jumboClass}\" alt=\"{emoji.Name}\" title=\"{emoji.Code}\" src=\"{emojiImageUrl}\">");
|
|
||||||
|
|
||||||
return base.VisitEmoji(emoji);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override MarkdownNode VisitLink(LinkNode link)
|
protected override MarkdownNode VisitLink(LinkNode link)
|
||||||
{
|
{
|
||||||
// Extract message ID if the link points to a Discord message
|
// Extract message ID if the link points to a Discord message
|
||||||
@@ -158,6 +97,78 @@ namespace DiscordChatExporter.Core.Exporting.Writers.MarkdownVisitors
|
|||||||
|
|
||||||
return base.VisitLink(link);
|
return base.VisitLink(link);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override MarkdownNode VisitEmoji(EmojiNode emoji)
|
||||||
|
{
|
||||||
|
var emojiImageUrl = Emoji.GetImageUrl(emoji.Id, emoji.Name, emoji.IsAnimated);
|
||||||
|
var jumboClass = _isJumbo ? "emoji--large" : "";
|
||||||
|
|
||||||
|
_buffer
|
||||||
|
.Append($"<img loading=\"lazy\" class=\"emoji {jumboClass}\" alt=\"{emoji.Name}\" title=\"{emoji.Code}\" src=\"{emojiImageUrl}\">");
|
||||||
|
|
||||||
|
return base.VisitEmoji(emoji);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override MarkdownNode VisitMention(MentionNode mention)
|
||||||
|
{
|
||||||
|
var mentionId = Snowflake.TryParse(mention.Id);
|
||||||
|
if (mention.Kind == MentionKind.Meta)
|
||||||
|
{
|
||||||
|
_buffer
|
||||||
|
.Append("<span class=\"mention\">")
|
||||||
|
.Append("@").Append(HtmlEncode(mention.Id))
|
||||||
|
.Append("</span>");
|
||||||
|
}
|
||||||
|
else if (mention.Kind == MentionKind.User)
|
||||||
|
{
|
||||||
|
var member = mentionId?.Pipe(_context.TryGetMember);
|
||||||
|
var fullName = member?.User.FullName ?? "Unknown";
|
||||||
|
var nick = member?.Nick ?? "Unknown";
|
||||||
|
|
||||||
|
_buffer
|
||||||
|
.Append($"<span class=\"mention\" title=\"{HtmlEncode(fullName)}\">")
|
||||||
|
.Append("@").Append(HtmlEncode(nick))
|
||||||
|
.Append("</span>");
|
||||||
|
}
|
||||||
|
else if (mention.Kind == MentionKind.Channel)
|
||||||
|
{
|
||||||
|
var channel = mentionId?.Pipe(_context.TryGetChannel);
|
||||||
|
var symbol = channel?.IsVoiceChannel == true ? "🔊" : "#";
|
||||||
|
var name = channel?.Name ?? "deleted-channel";
|
||||||
|
|
||||||
|
_buffer
|
||||||
|
.Append("<span class=\"mention\">")
|
||||||
|
.Append(symbol).Append(HtmlEncode(name))
|
||||||
|
.Append("</span>");
|
||||||
|
}
|
||||||
|
else if (mention.Kind == MentionKind.Role)
|
||||||
|
{
|
||||||
|
var role = mentionId?.Pipe(_context.TryGetRole);
|
||||||
|
var name = role?.Name ?? "deleted-role";
|
||||||
|
var color = role?.Color;
|
||||||
|
|
||||||
|
var style = color is not null
|
||||||
|
? $"color: rgb({color?.R}, {color?.G}, {color?.B}); background-color: rgba({color?.R}, {color?.G}, {color?.B}, 0.1);"
|
||||||
|
: "";
|
||||||
|
|
||||||
|
_buffer
|
||||||
|
.Append($"<span class=\"mention\" style=\"{style}\">")
|
||||||
|
.Append("@").Append(HtmlEncode(name))
|
||||||
|
.Append("</span>");
|
||||||
|
}
|
||||||
|
|
||||||
|
return base.VisitMention(mention);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override MarkdownNode VisitUnixTimestamp(UnixTimestampNode timestamp)
|
||||||
|
{
|
||||||
|
_buffer
|
||||||
|
.Append("<span class=\"timestamp\">")
|
||||||
|
.Append(HtmlEncode(_context.FormatDate(timestamp.Value)))
|
||||||
|
.Append("</span>");
|
||||||
|
|
||||||
|
return base.VisitUnixTimestamp(timestamp);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal partial class HtmlMarkdownVisitor
|
internal partial class HtmlMarkdownVisitor
|
||||||
|
|||||||
+23
-10
@@ -1,7 +1,7 @@
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
using DiscordChatExporter.Core.Discord;
|
using DiscordChatExporter.Core.Discord;
|
||||||
using DiscordChatExporter.Core.Markdown;
|
using DiscordChatExporter.Core.Markdown;
|
||||||
using DiscordChatExporter.Core.Markdown.Ast;
|
using DiscordChatExporter.Core.Markdown.Parsing;
|
||||||
using DiscordChatExporter.Core.Utils.Extensions;
|
using DiscordChatExporter.Core.Utils.Extensions;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Core.Exporting.Writers.MarkdownVisitors
|
namespace DiscordChatExporter.Core.Exporting.Writers.MarkdownVisitors
|
||||||
@@ -23,28 +23,43 @@ namespace DiscordChatExporter.Core.Exporting.Writers.MarkdownVisitors
|
|||||||
return base.VisitText(text);
|
return base.VisitText(text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override MarkdownNode VisitEmoji(EmojiNode emoji)
|
||||||
|
{
|
||||||
|
_buffer.Append(
|
||||||
|
emoji.IsCustomEmoji
|
||||||
|
? $":{emoji.Name}:"
|
||||||
|
: emoji.Name
|
||||||
|
);
|
||||||
|
|
||||||
|
return base.VisitEmoji(emoji);
|
||||||
|
}
|
||||||
|
|
||||||
protected override MarkdownNode VisitMention(MentionNode mention)
|
protected override MarkdownNode VisitMention(MentionNode mention)
|
||||||
{
|
{
|
||||||
var mentionId = Snowflake.TryParse(mention.Id);
|
var mentionId = Snowflake.TryParse(mention.Id);
|
||||||
if (mention.Type == MentionType.Meta)
|
if (mention.Kind == MentionKind.Meta)
|
||||||
{
|
{
|
||||||
_buffer.Append($"@{mention.Id}");
|
_buffer.Append($"@{mention.Id}");
|
||||||
}
|
}
|
||||||
else if (mention.Type == MentionType.User)
|
else if (mention.Kind == MentionKind.User)
|
||||||
{
|
{
|
||||||
var member = mentionId?.Pipe(_context.TryGetMember);
|
var member = mentionId?.Pipe(_context.TryGetMember);
|
||||||
var name = member?.User.Name ?? "Unknown";
|
var name = member?.User.Name ?? "Unknown";
|
||||||
|
|
||||||
_buffer.Append($"@{name}");
|
_buffer.Append($"@{name}");
|
||||||
}
|
}
|
||||||
else if (mention.Type == MentionType.Channel)
|
else if (mention.Kind == MentionKind.Channel)
|
||||||
{
|
{
|
||||||
var channel = mentionId?.Pipe(_context.TryGetChannel);
|
var channel = mentionId?.Pipe(_context.TryGetChannel);
|
||||||
var name = channel?.Name ?? "deleted-channel";
|
var name = channel?.Name ?? "deleted-channel";
|
||||||
|
|
||||||
_buffer.Append($"#{name}");
|
_buffer.Append($"#{name}");
|
||||||
|
|
||||||
|
// Voice channel marker
|
||||||
|
if (channel?.IsVoiceChannel == true)
|
||||||
|
_buffer.Append(" [voice]");
|
||||||
}
|
}
|
||||||
else if (mention.Type == MentionType.Role)
|
else if (mention.Kind == MentionKind.Role)
|
||||||
{
|
{
|
||||||
var role = mentionId?.Pipe(_context.TryGetRole);
|
var role = mentionId?.Pipe(_context.TryGetRole);
|
||||||
var name = role?.Name ?? "deleted-role";
|
var name = role?.Name ?? "deleted-role";
|
||||||
@@ -55,15 +70,13 @@ namespace DiscordChatExporter.Core.Exporting.Writers.MarkdownVisitors
|
|||||||
return base.VisitMention(mention);
|
return base.VisitMention(mention);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override MarkdownNode VisitEmoji(EmojiNode emoji)
|
protected override MarkdownNode VisitUnixTimestamp(UnixTimestampNode timestamp)
|
||||||
{
|
{
|
||||||
_buffer.Append(
|
_buffer.Append(
|
||||||
emoji.IsCustomEmoji
|
_context.FormatDate(timestamp.Value)
|
||||||
? $":{emoji.Name}:"
|
|
||||||
: emoji.Name
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return base.VisitEmoji(emoji);
|
return base.VisitUnixTimestamp(timestamp);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
namespace DiscordChatExporter.Core.Markdown.Ast
|
|
||||||
{
|
|
||||||
internal class MentionNode : MarkdownNode
|
|
||||||
{
|
|
||||||
public string Id { get; }
|
|
||||||
|
|
||||||
public MentionType Type { get; }
|
|
||||||
|
|
||||||
public MentionNode(string id, MentionType type)
|
|
||||||
{
|
|
||||||
Id = id;
|
|
||||||
Type = type;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override string ToString() => $"<{Type} mention> {Id}";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
namespace DiscordChatExporter.Core.Markdown.Ast
|
|
||||||
{
|
|
||||||
internal enum MentionType
|
|
||||||
{
|
|
||||||
Meta,
|
|
||||||
User,
|
|
||||||
Channel,
|
|
||||||
Role
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
using DiscordChatExporter.Core.Utils;
|
using DiscordChatExporter.Core.Utils;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Core.Markdown.Ast
|
namespace DiscordChatExporter.Core.Markdown
|
||||||
{
|
{
|
||||||
internal class EmojiNode : MarkdownNode
|
internal class EmojiNode : MarkdownNode
|
||||||
{
|
{
|
||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Core.Markdown.Ast
|
namespace DiscordChatExporter.Core.Markdown
|
||||||
{
|
{
|
||||||
internal class FormattedNode : MarkdownNode
|
internal class FormattedNode : MarkdownNode
|
||||||
{
|
{
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
namespace DiscordChatExporter.Core.Markdown.Ast
|
namespace DiscordChatExporter.Core.Markdown
|
||||||
{
|
{
|
||||||
internal class InlineCodeBlockNode : MarkdownNode
|
internal class InlineCodeBlockNode : MarkdownNode
|
||||||
{
|
{
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
namespace DiscordChatExporter.Core.Markdown.Ast
|
namespace DiscordChatExporter.Core.Markdown
|
||||||
{
|
{
|
||||||
internal class LinkNode : MarkdownNode
|
internal class LinkNode : MarkdownNode
|
||||||
{
|
{
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
namespace DiscordChatExporter.Core.Markdown.Ast
|
namespace DiscordChatExporter.Core.Markdown
|
||||||
{
|
{
|
||||||
internal abstract class MarkdownNode
|
internal abstract class MarkdownNode
|
||||||
{
|
{
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
namespace DiscordChatExporter.Core.Markdown
|
||||||
|
{
|
||||||
|
internal enum MentionKind
|
||||||
|
{
|
||||||
|
Meta,
|
||||||
|
User,
|
||||||
|
Channel,
|
||||||
|
Role
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
namespace DiscordChatExporter.Core.Markdown
|
||||||
|
{
|
||||||
|
internal class MentionNode : MarkdownNode
|
||||||
|
{
|
||||||
|
public string Id { get; }
|
||||||
|
|
||||||
|
public MentionKind Kind { get; }
|
||||||
|
|
||||||
|
public MentionNode(string id, MentionKind kind)
|
||||||
|
{
|
||||||
|
Id = id;
|
||||||
|
Kind = kind;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString() => $"<{Kind} mention> {Id}";
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
namespace DiscordChatExporter.Core.Markdown.Ast
|
namespace DiscordChatExporter.Core.Markdown
|
||||||
{
|
{
|
||||||
internal class MultiLineCodeBlockNode : MarkdownNode
|
internal class MultiLineCodeBlockNode : MarkdownNode
|
||||||
{
|
{
|
||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Core.Markdown.Matching
|
namespace DiscordChatExporter.Core.Markdown.Parsing
|
||||||
{
|
{
|
||||||
internal class AggregateMatcher<T> : IMatcher<T>
|
internal class AggregateMatcher<T> : IMatcher<T>
|
||||||
{
|
{
|
||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Core.Markdown.Matching
|
namespace DiscordChatExporter.Core.Markdown.Parsing
|
||||||
{
|
{
|
||||||
internal interface IMatcher<T>
|
internal interface IMatcher<T>
|
||||||
{
|
{
|
||||||
+46
-16
@@ -1,11 +1,11 @@
|
|||||||
using System.Collections.Generic;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using DiscordChatExporter.Core.Markdown.Ast;
|
|
||||||
using DiscordChatExporter.Core.Markdown.Matching;
|
|
||||||
using DiscordChatExporter.Core.Utils;
|
using DiscordChatExporter.Core.Utils;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Core.Markdown
|
namespace DiscordChatExporter.Core.Markdown.Parsing
|
||||||
{
|
{
|
||||||
// The following parsing logic is meant to replicate Discord's markdown grammar as close as possible
|
// The following parsing logic is meant to replicate Discord's markdown grammar as close as possible
|
||||||
internal static partial class MarkdownParser
|
internal static partial class MarkdownParser
|
||||||
@@ -120,31 +120,31 @@ namespace DiscordChatExporter.Core.Markdown
|
|||||||
// Capture @everyone
|
// Capture @everyone
|
||||||
private static readonly IMatcher<MarkdownNode> EveryoneMentionNodeMatcher = new StringMatcher<MarkdownNode>(
|
private static readonly IMatcher<MarkdownNode> EveryoneMentionNodeMatcher = new StringMatcher<MarkdownNode>(
|
||||||
"@everyone",
|
"@everyone",
|
||||||
_ => new MentionNode("everyone", MentionType.Meta)
|
_ => new MentionNode("everyone", MentionKind.Meta)
|
||||||
);
|
);
|
||||||
|
|
||||||
// Capture @here
|
// Capture @here
|
||||||
private static readonly IMatcher<MarkdownNode> HereMentionNodeMatcher = new StringMatcher<MarkdownNode>(
|
private static readonly IMatcher<MarkdownNode> HereMentionNodeMatcher = new StringMatcher<MarkdownNode>(
|
||||||
"@here",
|
"@here",
|
||||||
_ => new MentionNode("here", MentionType.Meta)
|
_ => new MentionNode("here", MentionKind.Meta)
|
||||||
);
|
);
|
||||||
|
|
||||||
// Capture <@123456> or <@!123456>
|
// Capture <@123456> or <@!123456>
|
||||||
private static readonly IMatcher<MarkdownNode> UserMentionNodeMatcher = new RegexMatcher<MarkdownNode>(
|
private static readonly IMatcher<MarkdownNode> UserMentionNodeMatcher = new RegexMatcher<MarkdownNode>(
|
||||||
new Regex("<@!?(\\d+)>", DefaultRegexOptions),
|
new Regex("<@!?(\\d+)>", DefaultRegexOptions),
|
||||||
(_, m) => new MentionNode(m.Groups[1].Value, MentionType.User)
|
(_, m) => new MentionNode(m.Groups[1].Value, MentionKind.User)
|
||||||
);
|
);
|
||||||
|
|
||||||
// Capture <#123456>
|
// Capture <#123456>
|
||||||
private static readonly IMatcher<MarkdownNode> ChannelMentionNodeMatcher = new RegexMatcher<MarkdownNode>(
|
private static readonly IMatcher<MarkdownNode> ChannelMentionNodeMatcher = new RegexMatcher<MarkdownNode>(
|
||||||
new Regex("<#(\\d+)>", DefaultRegexOptions),
|
new Regex("<#!?(\\d+)>", DefaultRegexOptions),
|
||||||
(_, m) => new MentionNode(m.Groups[1].Value, MentionType.Channel)
|
(_, m) => new MentionNode(m.Groups[1].Value, MentionKind.Channel)
|
||||||
);
|
);
|
||||||
|
|
||||||
// Capture <@&123456>
|
// Capture <@&123456>
|
||||||
private static readonly IMatcher<MarkdownNode> RoleMentionNodeMatcher = new RegexMatcher<MarkdownNode>(
|
private static readonly IMatcher<MarkdownNode> RoleMentionNodeMatcher = new RegexMatcher<MarkdownNode>(
|
||||||
new Regex("<@&(\\d+)>", DefaultRegexOptions),
|
new Regex("<@&(\\d+)>", DefaultRegexOptions),
|
||||||
(_, m) => new MentionNode(m.Groups[1].Value, MentionType.Role)
|
(_, m) => new MentionNode(m.Groups[1].Value, MentionKind.Role)
|
||||||
);
|
);
|
||||||
|
|
||||||
/* Emojis */
|
/* Emojis */
|
||||||
@@ -227,6 +227,26 @@ namespace DiscordChatExporter.Core.Markdown
|
|||||||
(_, m) => new TextNode(m.Groups[1].Value)
|
(_, m) => new TextNode(m.Groups[1].Value)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/* Misc */
|
||||||
|
|
||||||
|
// Capture <t:12345678> or <t:12345678:R>
|
||||||
|
private static readonly IMatcher<MarkdownNode> UnixTimestampNodeMatcher = new RegexMatcher<MarkdownNode>(
|
||||||
|
new Regex("<t:(\\d+)(?::\\w)?>", DefaultRegexOptions),
|
||||||
|
(_, m) =>
|
||||||
|
{
|
||||||
|
// We don't care about the 'R' parameter because we're not going to
|
||||||
|
// show relative timestamps in an export anyway.
|
||||||
|
|
||||||
|
if (!long.TryParse(m.Groups[1].Value, NumberStyles.Integer, CultureInfo.InvariantCulture,
|
||||||
|
out var offset))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new UnixTimestampNode(DateTimeOffset.UnixEpoch + TimeSpan.FromSeconds(offset));
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
// Combine all matchers into one
|
// Combine all matchers into one
|
||||||
// Matchers that have similar patterns are ordered from most specific to least specific
|
// Matchers that have similar patterns are ordered from most specific to least specific
|
||||||
private static readonly IMatcher<MarkdownNode> AggregateNodeMatcher = new AggregateMatcher<MarkdownNode>(
|
private static readonly IMatcher<MarkdownNode> AggregateNodeMatcher = new AggregateMatcher<MarkdownNode>(
|
||||||
@@ -268,7 +288,10 @@ namespace DiscordChatExporter.Core.Markdown
|
|||||||
// Emoji
|
// Emoji
|
||||||
StandardEmojiNodeMatcher,
|
StandardEmojiNodeMatcher,
|
||||||
CustomEmojiNodeMatcher,
|
CustomEmojiNodeMatcher,
|
||||||
CodedStandardEmojiNodeMatcher
|
CodedStandardEmojiNodeMatcher,
|
||||||
|
|
||||||
|
// Misc
|
||||||
|
UnixTimestampNodeMatcher
|
||||||
);
|
);
|
||||||
|
|
||||||
// Minimal set of matchers for non-multimedia formats (e.g. plain text)
|
// Minimal set of matchers for non-multimedia formats (e.g. plain text)
|
||||||
@@ -281,7 +304,10 @@ namespace DiscordChatExporter.Core.Markdown
|
|||||||
RoleMentionNodeMatcher,
|
RoleMentionNodeMatcher,
|
||||||
|
|
||||||
// Emoji
|
// Emoji
|
||||||
CustomEmojiNodeMatcher
|
CustomEmojiNodeMatcher,
|
||||||
|
|
||||||
|
// Misc
|
||||||
|
UnixTimestampNodeMatcher
|
||||||
);
|
);
|
||||||
|
|
||||||
private static IReadOnlyList<MarkdownNode> Parse(StringPart stringPart, IMatcher<MarkdownNode> matcher) =>
|
private static IReadOnlyList<MarkdownNode> Parse(StringPart stringPart, IMatcher<MarkdownNode> matcher) =>
|
||||||
@@ -293,12 +319,16 @@ namespace DiscordChatExporter.Core.Markdown
|
|||||||
|
|
||||||
internal static partial class MarkdownParser
|
internal static partial class MarkdownParser
|
||||||
{
|
{
|
||||||
private static IReadOnlyList<MarkdownNode> Parse(StringPart stringPart) => Parse(stringPart, AggregateNodeMatcher);
|
private static IReadOnlyList<MarkdownNode> Parse(StringPart stringPart) =>
|
||||||
|
Parse(stringPart, AggregateNodeMatcher);
|
||||||
|
|
||||||
private static IReadOnlyList<MarkdownNode> ParseMinimal(StringPart stringPart) => Parse(stringPart, MinimalAggregateNodeMatcher);
|
private static IReadOnlyList<MarkdownNode> ParseMinimal(StringPart stringPart) =>
|
||||||
|
Parse(stringPart, MinimalAggregateNodeMatcher);
|
||||||
|
|
||||||
public static IReadOnlyList<MarkdownNode> Parse(string input) => Parse(new StringPart(input));
|
public static IReadOnlyList<MarkdownNode> Parse(string input) =>
|
||||||
|
Parse(new StringPart(input));
|
||||||
|
|
||||||
public static IReadOnlyList<MarkdownNode> ParseMinimal(string input) => ParseMinimal(new StringPart(input));
|
public static IReadOnlyList<MarkdownNode> ParseMinimal(string input) =>
|
||||||
|
ParseMinimal(new StringPart(input));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+17
-8
@@ -1,12 +1,12 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using DiscordChatExporter.Core.Markdown.Ast;
|
|
||||||
|
|
||||||
namespace DiscordChatExporter.Core.Markdown
|
namespace DiscordChatExporter.Core.Markdown.Parsing
|
||||||
{
|
{
|
||||||
internal abstract class MarkdownVisitor
|
internal abstract class MarkdownVisitor
|
||||||
{
|
{
|
||||||
protected virtual MarkdownNode VisitText(TextNode text) => text;
|
protected virtual MarkdownNode VisitText(TextNode text) =>
|
||||||
|
text;
|
||||||
|
|
||||||
protected virtual MarkdownNode VisitFormatted(FormattedNode formatted)
|
protected virtual MarkdownNode VisitFormatted(FormattedNode formatted)
|
||||||
{
|
{
|
||||||
@@ -14,15 +14,23 @@ namespace DiscordChatExporter.Core.Markdown
|
|||||||
return formatted;
|
return formatted;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected virtual MarkdownNode VisitInlineCodeBlock(InlineCodeBlockNode inlineCodeBlock) => inlineCodeBlock;
|
protected virtual MarkdownNode VisitInlineCodeBlock(InlineCodeBlockNode inlineCodeBlock) =>
|
||||||
|
inlineCodeBlock;
|
||||||
|
|
||||||
protected virtual MarkdownNode VisitMultiLineCodeBlock(MultiLineCodeBlockNode multiLineCodeBlock) => multiLineCodeBlock;
|
protected virtual MarkdownNode VisitMultiLineCodeBlock(MultiLineCodeBlockNode multiLineCodeBlock) =>
|
||||||
|
multiLineCodeBlock;
|
||||||
|
|
||||||
protected virtual MarkdownNode VisitLink(LinkNode link) => link;
|
protected virtual MarkdownNode VisitLink(LinkNode link) =>
|
||||||
|
link;
|
||||||
|
|
||||||
protected virtual MarkdownNode VisitEmoji(EmojiNode emoji) => emoji;
|
protected virtual MarkdownNode VisitEmoji(EmojiNode emoji) =>
|
||||||
|
emoji;
|
||||||
|
|
||||||
protected virtual MarkdownNode VisitMention(MentionNode mention) => mention;
|
protected virtual MarkdownNode VisitMention(MentionNode mention) =>
|
||||||
|
mention;
|
||||||
|
|
||||||
|
protected virtual MarkdownNode VisitUnixTimestamp(UnixTimestampNode timestamp) =>
|
||||||
|
timestamp;
|
||||||
|
|
||||||
public MarkdownNode Visit(MarkdownNode node) => node switch
|
public MarkdownNode Visit(MarkdownNode node) => node switch
|
||||||
{
|
{
|
||||||
@@ -33,6 +41,7 @@ namespace DiscordChatExporter.Core.Markdown
|
|||||||
LinkNode link => VisitLink(link),
|
LinkNode link => VisitLink(link),
|
||||||
EmojiNode emoji => VisitEmoji(emoji),
|
EmojiNode emoji => VisitEmoji(emoji),
|
||||||
MentionNode mention => VisitMention(mention),
|
MentionNode mention => VisitMention(mention),
|
||||||
|
UnixTimestampNode timestamp => VisitUnixTimestamp(timestamp),
|
||||||
_ => throw new ArgumentOutOfRangeException(nameof(node))
|
_ => throw new ArgumentOutOfRangeException(nameof(node))
|
||||||
};
|
};
|
||||||
|
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
namespace DiscordChatExporter.Core.Markdown.Matching
|
namespace DiscordChatExporter.Core.Markdown.Parsing
|
||||||
{
|
{
|
||||||
internal class ParsedMatch<T>
|
internal class ParsedMatch<T>
|
||||||
{
|
{
|
||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Core.Markdown.Matching
|
namespace DiscordChatExporter.Core.Markdown.Parsing
|
||||||
{
|
{
|
||||||
internal class RegexMatcher<T> : IMatcher<T>
|
internal class RegexMatcher<T> : IMatcher<T>
|
||||||
{
|
{
|
||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Core.Markdown.Matching
|
namespace DiscordChatExporter.Core.Markdown.Parsing
|
||||||
{
|
{
|
||||||
internal class StringMatcher<T> : IMatcher<T>
|
internal class StringMatcher<T> : IMatcher<T>
|
||||||
{
|
{
|
||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Core.Markdown.Matching
|
namespace DiscordChatExporter.Core.Markdown.Parsing
|
||||||
{
|
{
|
||||||
internal readonly struct StringPart
|
internal readonly struct StringPart
|
||||||
{
|
{
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
namespace DiscordChatExporter.Core.Markdown.Ast
|
namespace DiscordChatExporter.Core.Markdown
|
||||||
{
|
{
|
||||||
internal enum TextFormatting
|
internal enum TextFormatting
|
||||||
{
|
{
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
namespace DiscordChatExporter.Core.Markdown.Ast
|
namespace DiscordChatExporter.Core.Markdown
|
||||||
{
|
{
|
||||||
internal class TextNode : MarkdownNode
|
internal class TextNode : MarkdownNode
|
||||||
{
|
{
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace DiscordChatExporter.Core.Markdown
|
||||||
|
{
|
||||||
|
internal class UnixTimestampNode : MarkdownNode
|
||||||
|
{
|
||||||
|
public DateTimeOffset Value { get; }
|
||||||
|
|
||||||
|
public UnixTimestampNode(DateTimeOffset value) => Value = value;
|
||||||
|
|
||||||
|
public override string ToString() => Value.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
using System;
|
||||||
|
using Superpower;
|
||||||
|
using Superpower.Parsers;
|
||||||
|
|
||||||
|
namespace DiscordChatExporter.Core.Utils.Extensions
|
||||||
|
{
|
||||||
|
public static class SuperpowerExtensions
|
||||||
|
{
|
||||||
|
public static TextParser<string> Text(this TextParser<char[]> parser) =>
|
||||||
|
parser.Select(chars => new string(chars));
|
||||||
|
|
||||||
|
public static TextParser<T> Token<T>(this TextParser<T> parser) =>
|
||||||
|
parser.Between(Character.WhiteSpace.IgnoreMany(), Character.WhiteSpace.IgnoreMany());
|
||||||
|
|
||||||
|
// From: https://twitter.com/nblumhardt/status/1389349059786264578
|
||||||
|
public static TextParser<T> Log<T>(this TextParser<T> parser, string description) => i =>
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Trying {description} ->");
|
||||||
|
var r = parser(i);
|
||||||
|
Console.WriteLine($"Result was {r}");
|
||||||
|
return r;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,14 +21,15 @@ namespace DiscordChatExporter.Core.Utils
|
|||||||
.OrResult<HttpResponseMessage>(m => m.StatusCode == HttpStatusCode.TooManyRequests)
|
.OrResult<HttpResponseMessage>(m => m.StatusCode == HttpStatusCode.TooManyRequests)
|
||||||
.OrResult(m => m.StatusCode == HttpStatusCode.RequestTimeout)
|
.OrResult(m => m.StatusCode == HttpStatusCode.RequestTimeout)
|
||||||
.OrResult(m => m.StatusCode >= HttpStatusCode.InternalServerError)
|
.OrResult(m => m.StatusCode >= HttpStatusCode.InternalServerError)
|
||||||
.WaitAndRetryAsync(8,
|
.WaitAndRetryAsync(
|
||||||
|
8,
|
||||||
(i, result, _) =>
|
(i, result, _) =>
|
||||||
{
|
{
|
||||||
// If rate-limited, use retry-after as a guide
|
// If rate-limited, use retry-after as a guide
|
||||||
if (result.Result?.StatusCode == HttpStatusCode.TooManyRequests)
|
if (result.Result?.StatusCode == HttpStatusCode.TooManyRequests)
|
||||||
{
|
{
|
||||||
// Only start respecting retry-after after a few attempts.
|
// Only start respecting retry-after after a few attempts, because
|
||||||
// The reason is that Discord often sends unreasonable (20+ minutes) retry-after
|
// Discord often sends unreasonable (20+ minutes) retry-after
|
||||||
// on the very first request.
|
// on the very first request.
|
||||||
if (i > 3)
|
if (i > 3)
|
||||||
{
|
{
|
||||||
@@ -40,7 +41,8 @@ namespace DiscordChatExporter.Core.Utils
|
|||||||
|
|
||||||
return TimeSpan.FromSeconds(Math.Pow(2, i) + 1);
|
return TimeSpan.FromSeconds(Math.Pow(2, i) + 1);
|
||||||
},
|
},
|
||||||
(_, _, _, _) => Task.CompletedTask);
|
(_, _, _, _) => Task.CompletedTask
|
||||||
|
);
|
||||||
|
|
||||||
private static HttpStatusCode? TryGetStatusCodeFromException(HttpRequestException ex) =>
|
private static HttpStatusCode? TryGetStatusCodeFromException(HttpRequestException ex) =>
|
||||||
// This is extremely frail, but there's no other way
|
// This is extremely frail, but there's no other way
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
// ReSharper disable CheckNamespace
|
||||||
|
// TODO: remove after moving to .NET 5
|
||||||
|
|
||||||
|
namespace System.Runtime.CompilerServices
|
||||||
|
{
|
||||||
|
internal static class IsExternalInit
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -25,6 +25,8 @@ namespace DiscordChatExporter.Gui.Services
|
|||||||
|
|
||||||
public string? LastPartitionLimitValue { get; set; }
|
public string? LastPartitionLimitValue { get; set; }
|
||||||
|
|
||||||
|
public string? LastMessageFilterValue { get; set; }
|
||||||
|
|
||||||
public bool LastShouldDownloadMedia { get; set; }
|
public bool LastShouldDownloadMedia { get; set; }
|
||||||
|
|
||||||
public SettingsService()
|
public SettingsService()
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ using System.Linq;
|
|||||||
using DiscordChatExporter.Core.Discord;
|
using DiscordChatExporter.Core.Discord;
|
||||||
using DiscordChatExporter.Core.Discord.Data;
|
using DiscordChatExporter.Core.Discord.Data;
|
||||||
using DiscordChatExporter.Core.Exporting;
|
using DiscordChatExporter.Core.Exporting;
|
||||||
|
using DiscordChatExporter.Core.Exporting.Filtering;
|
||||||
using DiscordChatExporter.Core.Exporting.Partitioning;
|
using DiscordChatExporter.Core.Exporting.Partitioning;
|
||||||
using DiscordChatExporter.Core.Utils.Extensions;
|
using DiscordChatExporter.Core.Utils.Extensions;
|
||||||
using DiscordChatExporter.Gui.Services;
|
using DiscordChatExporter.Gui.Services;
|
||||||
@@ -51,7 +52,13 @@ namespace DiscordChatExporter.Gui.ViewModels.Dialogs
|
|||||||
|
|
||||||
public PartitionLimit PartitionLimit => !string.IsNullOrWhiteSpace(PartitionLimitValue)
|
public PartitionLimit PartitionLimit => !string.IsNullOrWhiteSpace(PartitionLimitValue)
|
||||||
? PartitionLimit.Parse(PartitionLimitValue)
|
? PartitionLimit.Parse(PartitionLimitValue)
|
||||||
: NullPartitionLimit.Instance;
|
: PartitionLimit.Null;
|
||||||
|
|
||||||
|
public string? MessageFilterValue { get; set; }
|
||||||
|
|
||||||
|
public MessageFilter MessageFilter => !string.IsNullOrWhiteSpace(MessageFilterValue)
|
||||||
|
? MessageFilter.Parse(MessageFilterValue)
|
||||||
|
: MessageFilter.Null;
|
||||||
|
|
||||||
public bool ShouldDownloadMedia { get; set; }
|
public bool ShouldDownloadMedia { get; set; }
|
||||||
|
|
||||||
@@ -61,6 +68,7 @@ namespace DiscordChatExporter.Gui.ViewModels.Dialogs
|
|||||||
After != default ||
|
After != default ||
|
||||||
Before != default ||
|
Before != default ||
|
||||||
!string.IsNullOrWhiteSpace(PartitionLimitValue) ||
|
!string.IsNullOrWhiteSpace(PartitionLimitValue) ||
|
||||||
|
!string.IsNullOrWhiteSpace(MessageFilterValue) ||
|
||||||
ShouldDownloadMedia != default;
|
ShouldDownloadMedia != default;
|
||||||
|
|
||||||
public ExportSetupViewModel(DialogManager dialogManager, SettingsService settingsService)
|
public ExportSetupViewModel(DialogManager dialogManager, SettingsService settingsService)
|
||||||
@@ -71,6 +79,7 @@ namespace DiscordChatExporter.Gui.ViewModels.Dialogs
|
|||||||
// Persist preferences
|
// Persist preferences
|
||||||
SelectedFormat = _settingsService.LastExportFormat;
|
SelectedFormat = _settingsService.LastExportFormat;
|
||||||
PartitionLimitValue = _settingsService.LastPartitionLimitValue;
|
PartitionLimitValue = _settingsService.LastPartitionLimitValue;
|
||||||
|
MessageFilterValue = _settingsService.LastMessageFilterValue;
|
||||||
ShouldDownloadMedia = _settingsService.LastShouldDownloadMedia;
|
ShouldDownloadMedia = _settingsService.LastShouldDownloadMedia;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,6 +88,7 @@ namespace DiscordChatExporter.Gui.ViewModels.Dialogs
|
|||||||
// Persist preferences
|
// Persist preferences
|
||||||
_settingsService.LastExportFormat = SelectedFormat;
|
_settingsService.LastExportFormat = SelectedFormat;
|
||||||
_settingsService.LastPartitionLimitValue = PartitionLimitValue;
|
_settingsService.LastPartitionLimitValue = PartitionLimitValue;
|
||||||
|
_settingsService.LastMessageFilterValue = MessageFilterValue;
|
||||||
_settingsService.LastShouldDownloadMedia = ShouldDownloadMedia;
|
_settingsService.LastShouldDownloadMedia = ShouldDownloadMedia;
|
||||||
|
|
||||||
// If single channel - prompt file path
|
// If single channel - prompt file path
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
using DiscordChatExporter.Gui.ViewModels.Framework;
|
||||||
|
|
||||||
|
namespace DiscordChatExporter.Gui.ViewModels.Dialogs
|
||||||
|
{
|
||||||
|
public class MessageBoxViewModel : DialogScreen
|
||||||
|
{
|
||||||
|
public string? Title { get; set; }
|
||||||
|
|
||||||
|
public string? Message { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class MessageBoxViewModelExtensions
|
||||||
|
{
|
||||||
|
public static MessageBoxViewModel CreateMessageBoxViewModel(
|
||||||
|
this IViewModelFactory factory,
|
||||||
|
string title,
|
||||||
|
string message)
|
||||||
|
{
|
||||||
|
var viewModel = factory.CreateMessageBoxViewModel();
|
||||||
|
|
||||||
|
viewModel.Title = title;
|
||||||
|
viewModel.Message = message;
|
||||||
|
|
||||||
|
return viewModel;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,6 +7,8 @@ namespace DiscordChatExporter.Gui.ViewModels.Framework
|
|||||||
{
|
{
|
||||||
ExportSetupViewModel CreateExportSetupViewModel();
|
ExportSetupViewModel CreateExportSetupViewModel();
|
||||||
|
|
||||||
|
MessageBoxViewModel CreateMessageBoxViewModel();
|
||||||
|
|
||||||
SettingsViewModel CreateSettingsViewModel();
|
SettingsViewModel CreateSettingsViewModel();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -168,7 +168,8 @@ namespace DiscordChatExporter.Gui.ViewModels
|
|||||||
var guildChannelMap = new Dictionary<Guild, IReadOnlyList<Channel>>();
|
var guildChannelMap = new Dictionary<Guild, IReadOnlyList<Channel>>();
|
||||||
await foreach (var guild in discord.GetUserGuildsAsync())
|
await foreach (var guild in discord.GetUserGuildsAsync())
|
||||||
{
|
{
|
||||||
guildChannelMap[guild] = await discord.GetGuildChannelsAsync(guild.Id);
|
var channels = await discord.GetGuildChannelsAsync(guild.Id);
|
||||||
|
guildChannelMap[guild] = channels.Where(c => c.IsTextChannel).ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
GuildChannelMap = guildChannelMap;
|
GuildChannelMap = guildChannelMap;
|
||||||
@@ -178,6 +179,15 @@ namespace DiscordChatExporter.Gui.ViewModels
|
|||||||
{
|
{
|
||||||
Notifications.Enqueue(ex.Message.TrimEnd('.'));
|
Notifications.Enqueue(ex.Message.TrimEnd('.'));
|
||||||
}
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
var dialog = _viewModelFactory.CreateMessageBoxViewModel(
|
||||||
|
"Error pulling guilds and channels",
|
||||||
|
ex.ToString()
|
||||||
|
);
|
||||||
|
|
||||||
|
await _dialogManager.ShowDialogAsync(dialog);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool CanExportChannels =>
|
public bool CanExportChannels =>
|
||||||
@@ -185,55 +195,68 @@ namespace DiscordChatExporter.Gui.ViewModels
|
|||||||
|
|
||||||
public async void ExportChannels()
|
public async void ExportChannels()
|
||||||
{
|
{
|
||||||
var token = _settingsService.LastToken;
|
try
|
||||||
if (token is null || SelectedGuild is null || SelectedChannels is null || !SelectedChannels.Any())
|
|
||||||
return;
|
|
||||||
|
|
||||||
var dialog = _viewModelFactory.CreateExportSetupViewModel(SelectedGuild, SelectedChannels);
|
|
||||||
if (await _dialogManager.ShowDialogAsync(dialog) != true)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var exporter = new ChannelExporter(token);
|
|
||||||
|
|
||||||
var operations = ProgressManager.CreateOperations(dialog.Channels!.Count);
|
|
||||||
var successfulExportCount = 0;
|
|
||||||
|
|
||||||
await dialog.Channels.Zip(operations).ParallelForEachAsync(async tuple =>
|
|
||||||
{
|
{
|
||||||
var (channel, operation) = tuple;
|
var token = _settingsService.LastToken;
|
||||||
|
if (token is null || SelectedGuild is null || SelectedChannels is null || !SelectedChannels.Any())
|
||||||
|
return;
|
||||||
|
|
||||||
try
|
var dialog = _viewModelFactory.CreateExportSetupViewModel(SelectedGuild, SelectedChannels);
|
||||||
|
if (await _dialogManager.ShowDialogAsync(dialog) != true)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var exporter = new ChannelExporter(token);
|
||||||
|
|
||||||
|
var operations = ProgressManager.CreateOperations(dialog.Channels!.Count);
|
||||||
|
var successfulExportCount = 0;
|
||||||
|
|
||||||
|
await dialog.Channels.Zip(operations).ParallelForEachAsync(async tuple =>
|
||||||
{
|
{
|
||||||
var request = new ExportRequest(
|
var (channel, operation) = tuple;
|
||||||
dialog.Guild!,
|
|
||||||
channel!,
|
|
||||||
dialog.OutputPath!,
|
|
||||||
dialog.SelectedFormat,
|
|
||||||
dialog.After?.Pipe(Snowflake.FromDate),
|
|
||||||
dialog.Before?.Pipe(Snowflake.FromDate),
|
|
||||||
dialog.PartitionLimit,
|
|
||||||
dialog.ShouldDownloadMedia,
|
|
||||||
_settingsService.ShouldReuseMedia,
|
|
||||||
_settingsService.DateFormat
|
|
||||||
);
|
|
||||||
|
|
||||||
await exporter.ExportChannelAsync(request, operation);
|
try
|
||||||
|
{
|
||||||
|
var request = new ExportRequest(
|
||||||
|
dialog.Guild!,
|
||||||
|
channel!,
|
||||||
|
dialog.OutputPath!,
|
||||||
|
dialog.SelectedFormat,
|
||||||
|
dialog.After?.Pipe(Snowflake.FromDate),
|
||||||
|
dialog.Before?.Pipe(Snowflake.FromDate),
|
||||||
|
dialog.PartitionLimit,
|
||||||
|
dialog.MessageFilter,
|
||||||
|
dialog.ShouldDownloadMedia,
|
||||||
|
_settingsService.ShouldReuseMedia,
|
||||||
|
_settingsService.DateFormat
|
||||||
|
);
|
||||||
|
|
||||||
Interlocked.Increment(ref successfulExportCount);
|
await exporter.ExportChannelAsync(request, operation);
|
||||||
}
|
|
||||||
catch (DiscordChatExporterException ex) when (!ex.IsCritical)
|
|
||||||
{
|
|
||||||
Notifications.Enqueue(ex.Message.TrimEnd('.'));
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
operation.Dispose();
|
|
||||||
}
|
|
||||||
}, _settingsService.ParallelLimit.ClampMin(1));
|
|
||||||
|
|
||||||
// Notify of overall completion
|
Interlocked.Increment(ref successfulExportCount);
|
||||||
if (successfulExportCount > 0)
|
}
|
||||||
Notifications.Enqueue($"Successfully exported {successfulExportCount} channel(s)");
|
catch (DiscordChatExporterException ex) when (!ex.IsCritical)
|
||||||
|
{
|
||||||
|
Notifications.Enqueue(ex.Message.TrimEnd('.'));
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
operation.Dispose();
|
||||||
|
}
|
||||||
|
}, _settingsService.ParallelLimit.ClampMin(1));
|
||||||
|
|
||||||
|
// Notify of overall completion
|
||||||
|
if (successfulExportCount > 0)
|
||||||
|
Notifications.Enqueue($"Successfully exported {successfulExportCount} channel(s)");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
var dialog = _viewModelFactory.CreateMessageBoxViewModel(
|
||||||
|
"Error exporting channel(s)",
|
||||||
|
ex.ToString()
|
||||||
|
);
|
||||||
|
|
||||||
|
await _dialogManager.ShowDialogAsync(dialog);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -131,7 +131,15 @@
|
|||||||
materialDesign:HintAssist.Hint="Partition limit"
|
materialDesign:HintAssist.Hint="Partition limit"
|
||||||
materialDesign:HintAssist.IsFloating="True"
|
materialDesign:HintAssist.IsFloating="True"
|
||||||
Text="{Binding PartitionLimitValue}"
|
Text="{Binding PartitionLimitValue}"
|
||||||
ToolTip="Split output into partitions, each limited to this number of messages (e.g. 100) or file size (e.g. 10mb)" />
|
ToolTip="Split output into partitions, each limited to this number of messages (e.g. '100') or file size (e.g. '10mb')" />
|
||||||
|
|
||||||
|
<!-- Filtering -->
|
||||||
|
<TextBox
|
||||||
|
Margin="16,8"
|
||||||
|
materialDesign:HintAssist.Hint="Message filter"
|
||||||
|
materialDesign:HintAssist.IsFloating="True"
|
||||||
|
Text="{Binding MessageFilterValue}"
|
||||||
|
ToolTip="Only include messages that satisfy this filter (e.g. 'from:foo#1234' or 'has:image')." />
|
||||||
|
|
||||||
<!-- Download media -->
|
<!-- Download media -->
|
||||||
<Grid Margin="16,16" ToolTip="Download referenced media content (user avatars, attached files, embedded images, etc)">
|
<Grid Margin="16,16" ToolTip="Download referenced media content (user avatars, attached files, embedded images, etc)">
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
<UserControl
|
||||||
|
x:Class="DiscordChatExporter.Gui.Views.Dialogs.MessageBoxView"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:dialogs="clr-namespace:DiscordChatExporter.Gui.ViewModels.Dialogs"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
xmlns:s="https://github.com/canton7/Stylet"
|
||||||
|
MinWidth="500"
|
||||||
|
d:DataContext="{d:DesignInstance Type=dialogs:MessageBoxViewModel}"
|
||||||
|
d:DesignHeight="450"
|
||||||
|
d:DesignWidth="800"
|
||||||
|
Style="{DynamicResource MaterialDesignRoot}"
|
||||||
|
mc:Ignorable="d">
|
||||||
|
<Grid>
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="*" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<!-- Title -->
|
||||||
|
<TextBlock
|
||||||
|
Grid.Row="0"
|
||||||
|
Margin="16"
|
||||||
|
FontSize="17"
|
||||||
|
Text="{Binding Title}"
|
||||||
|
TextTrimming="CharacterEllipsis"
|
||||||
|
ToolTip="{Binding Title}" />
|
||||||
|
|
||||||
|
<!-- Message -->
|
||||||
|
<TextBlock
|
||||||
|
Grid.Row="1"
|
||||||
|
Margin="16,0,16,16"
|
||||||
|
Text="{Binding Message}"
|
||||||
|
TextWrapping="Wrap" />
|
||||||
|
|
||||||
|
<!-- Close -->
|
||||||
|
<Button
|
||||||
|
Grid.Row="2"
|
||||||
|
Margin="8"
|
||||||
|
HorizontalAlignment="Right"
|
||||||
|
Command="{s:Action Close}"
|
||||||
|
Content="CLOSE"
|
||||||
|
IsCancel="True"
|
||||||
|
IsDefault="True"
|
||||||
|
Style="{DynamicResource MaterialDesignFlatButton}" />
|
||||||
|
</Grid>
|
||||||
|
</UserControl>
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
namespace DiscordChatExporter.Gui.Views.Dialogs
|
||||||
|
{
|
||||||
|
public partial class MessageBoxView
|
||||||
|
{
|
||||||
|
public MessageBoxView()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -163,23 +163,32 @@
|
|||||||
<Run FontWeight="SemiBold" Text="Ctrl+Shift+I" />
|
<Run FontWeight="SemiBold" Text="Ctrl+Shift+I" />
|
||||||
<Run Text="to show developer tools" />
|
<Run Text="to show developer tools" />
|
||||||
<LineBreak />
|
<LineBreak />
|
||||||
<Run Text="3. Navigate to the" />
|
<Run Text="3. Press" />
|
||||||
|
<Run FontWeight="SemiBold" Text="Ctrl+Shift+M" />
|
||||||
|
<Run Text="to toggle device toolbar" />
|
||||||
|
<LineBreak />
|
||||||
|
<Run Text="4. Navigate to the" />
|
||||||
<Run FontWeight="SemiBold" Text="Application" />
|
<Run FontWeight="SemiBold" Text="Application" />
|
||||||
<Run Text="tab" />
|
<Run Text="tab" />
|
||||||
<LineBreak />
|
<LineBreak />
|
||||||
<Run Text="4. Select" />
|
<Run Text="5. On the left, expand" />
|
||||||
<Run FontWeight="SemiBold" Text="Local Storage" />
|
<Run FontWeight="SemiBold" Text="Local Storage" />
|
||||||
<Run Text=">" />
|
<Run Text="and select" />
|
||||||
<Run FontWeight="SemiBold" Text="https://discord.com" />
|
<Run FontWeight="SemiBold" Text="https://discord.com" />
|
||||||
<Run Text="on the left" />
|
|
||||||
<LineBreak />
|
<LineBreak />
|
||||||
<Run Text="5. Press" />
|
<Run Text="6. Type" />
|
||||||
|
<Run FontWeight="SemiBold" Text="token" />
|
||||||
|
<Run Text="into the" />
|
||||||
|
<Run FontWeight="SemiBold" Text="Filter" />
|
||||||
|
<Run Text="box" />
|
||||||
|
<LineBreak />
|
||||||
|
<Run Text="7. If the token key does not appear, press" />
|
||||||
<Run FontWeight="SemiBold" Text="Ctrl+R" />
|
<Run FontWeight="SemiBold" Text="Ctrl+R" />
|
||||||
<Run Text="to reload" />
|
<Run Text="to reload" />
|
||||||
<LineBreak />
|
<LineBreak />
|
||||||
<Run Text="6. Find" />
|
<Run Text="8. Copy the value of the" />
|
||||||
<Run FontWeight="SemiBold" Text="token" />
|
<Run FontWeight="SemiBold" Text="token" />
|
||||||
<Run Text="at the bottom and copy the value" />
|
<Run Text="key" />
|
||||||
</TextBlock>
|
</TextBlock>
|
||||||
<TextBlock Margin="0,24,0,0" FontSize="14">
|
<TextBlock Margin="0,24,0,0" FontSize="14">
|
||||||
<Run Text="Automating user accounts is technically against TOS, use at your own risk." />
|
<Run Text="Automating user accounts is technically against TOS, use at your own risk." />
|
||||||
@@ -384,4 +393,4 @@
|
|||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
</materialDesign:DialogHost>
|
</materialDesign:DialogHost>
|
||||||
</Window>
|
</Window>
|
||||||
|
|||||||
@@ -61,12 +61,12 @@ The following table lists all available download options:
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
- 🦄 - Community-maintained resource
|
|
||||||
- ⚙️ - Requires .NET runtime to be installed manually:
|
- ⚙️ - Requires .NET runtime to be installed manually:
|
||||||
- [.NET Core v3.1 Runtime for **Windows x64**](https://dotnet.microsoft.com/download/dotnet-core/thank-you/runtime-desktop-3.1.0-windows-x64-installer)
|
- [.NET Core v3.1 Runtime for **Windows x64**](https://dotnet.microsoft.com/download/dotnet-core/thank-you/runtime-desktop-3.1.0-windows-x64-installer)
|
||||||
- [.NET Core v3.1 Runtime for **Windows x86**](https://dotnet.microsoft.com/download/dotnet-core/thank-you/runtime-desktop-3.1.0-windows-x86-installer)
|
- [.NET Core v3.1 Runtime for **Windows x86**](https://dotnet.microsoft.com/download/dotnet-core/thank-you/runtime-desktop-3.1.0-windows-x86-installer)
|
||||||
- [.NET Core v3.1 Runtime for **macOS x64**](https://dotnet.microsoft.com/download/dotnet-core/thank-you/runtime-3.1.0-macos-x64-installer)
|
- [.NET Core v3.1 Runtime for **macOS x64**](https://dotnet.microsoft.com/download/dotnet-core/thank-you/runtime-3.1.0-macos-x64-installer)
|
||||||
- [.NET Core v3.1 Runtime for **Linux**](https://docs.microsoft.com/en-us/dotnet/core/install/linux) (find your distro)
|
- [.NET Core v3.1 Runtime for **Linux**](https://docs.microsoft.com/en-us/dotnet/core/install/linux) (find your distro)
|
||||||
|
- 🦄 - Community-maintained resource
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
@@ -74,6 +74,7 @@ The following table lists all available download options:
|
|||||||
- Command line interface (Windows, Linux, macOS)
|
- Command line interface (Windows, Linux, macOS)
|
||||||
- Support for both user and bot tokens
|
- Support for both user and bot tokens
|
||||||
- Support for Discord's dialect of markdown
|
- Support for Discord's dialect of markdown
|
||||||
|
- Support for Discord's message filter syntax
|
||||||
- Support for attachments, embeds, emojis, and other rich media features
|
- Support for attachments, embeds, emojis, and other rich media features
|
||||||
- Multiple output formats: HTML (dark/light), TXT, CSV, JSON
|
- Multiple output formats: HTML (dark/light), TXT, CSV, JSON
|
||||||
- File partitioning, date ranges, and other export options
|
- File partitioning, date ranges, and other export options
|
||||||
@@ -82,4 +83,33 @@ The following table lists all available download options:
|
|||||||
## Screenshots
|
## Screenshots
|
||||||
|
|
||||||

|

|
||||||

|

|
||||||
|
|
||||||
|
## Building the project locally
|
||||||
|
|
||||||
|
Prerequisites:
|
||||||
|
|
||||||
|
- [.NET 5.0 SDK](https://dotnet.microsoft.com/download/dotnet/5.0)
|
||||||
|
- [.NET 3.1 SDK](https://dotnet.microsoft.com/download/dotnet/3.1) (temporarily as the app still targets .NET 3.1, but requires .NET 5.0 to build)
|
||||||
|
- _(Recommended)_ C#/.NET IDE, such as [JetBrains Rider](https://www.jetbrains.com/rider), [VS Code](https://code.visualstudio.com/docs/languages/csharp), or [Visual Studio](https://visualstudio.microsoft.com).
|
||||||
|
|
||||||
|
To build the entire solution run the following command in the root of the repository:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
> dotnet build
|
||||||
|
```
|
||||||
|
|
||||||
|
This will generate runtime artifacts for each project:
|
||||||
|
|
||||||
|
```plaintext
|
||||||
|
./DiscordChatExporter.Gui/bin/[Debug|Release]/[runtime]/*
|
||||||
|
./DiscordChatExporter.Cli/bin/[Debug|Release]/[runtime]/*
|
||||||
|
```
|
||||||
|
|
||||||
|
You can also build and run a specific project directly.
|
||||||
|
To do that, navigate to its directory and use `dotnet run`:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
> cd DiscordChatExporter.Gui
|
||||||
|
> dotnet run
|
||||||
|
```
|
||||||
Reference in New Issue
Block a user