[
  {
    "path": ".docs/Docker.md",
    "content": "# Docker usage instructions\n\nDocker distribution of DiscordChatExporter provides a way to run the app in a virtualized and isolated environment. Due to the nature of Docker, you also don't need to install any prerequisites otherwise required by DCE.\n\n> **Note**:\n> Only the CLI flavor of DiscordChatExporter is available for use with Docker.\n\n## Pulling\n\nThis will download the [Docker image from the registry](https://hub.docker.com/r/tyrrrz/discordchatexporter) to your computer. You can run this command again to update when a new version is released.\n\n```console\n$ docker pull tyrrrz/discordchatexporter:stable\n```\n\nNote the `:stable` tag. DiscordChatExporter images are tagged according to the following patterns:\n\n- `stable` — latest stable version release. This tag is updated with each release of a new project version. Recommended for personal use.\n- `x.y.z` (e.g. `2.30.1`) — specific stable version release. This tag is pushed when the corresponding version is released and never updated thereafter. Recommended for use in automation scenarios.\n- `latest` — latest (potentially unstable) build. This tag is updated with each new commit to the `prime` branch. Not recommended, unless you want to test a new feature that has not been released in a stable version yet.\n\nYou can see all available tags [here](https://hub.docker.com/r/tyrrrz/discordchatexporter/tags?ordering=name).\n\n## Usage\n\nTo run the CLI in Docker and render help text:\n\n```console\n$ docker run --rm tyrrrz/discordchatexporter:stable\n```\n\nTo export a channel:\n\n```console\n$ docker run --rm -v /path/on/machine:/out tyrrrz/discordchatexporter:stable export -t TOKEN -c CHANNELID\n```\n\nIf you want colored output and real-time progress reporting, pass the `-it` (interactive + pseudo-terminal) option:\n\n```console\n$ docker run --rm -it -v /path/on/machine:/out tyrrrz/discordchatexporter:stable export -t TOKEN -c CHANNELID\n```\n\nThe `-v /path/on/machine:/out` option instructs Docker to bind the `/out` directory inside the container to a path on your host machine. Replace `/path/on/machine` with the directory you want the files to be saved at.\n\n> **Note**:\n> If you are running SELinux, you will need to add the `:z` option after `/out`, e.g.:\n>\n> ```console\n> $ docker run --rm -v /path/on/machine:/out:z tyrrrz/discordchatexporter:stable export -t TOKEN -c CHANNELID\n> ```\n>\n> For more information, refer to the [Docker docs SELinux labels for bind mounts page](https://docs.docker.com/storage/bind-mounts/#configure-the-selinux-label).\n\nYou can also use the current working directory as the output directory by specifying:\n\n- `-v $PWD:/out` in Bash\n- `-v $pwd.Path:/out` in PowerShell\n\nFor more information, please refer to the [Dockerfile](https://github.com/Tyrrrz/DiscordChatExporter/blob/prime/DiscordChatExporter.Cli.dockerfile) and [Docker documentation](https://docs.docker.com/engine/reference/run).\n\nTo get your Token and Channel IDs, please refer to [this page](Token-and-IDs.md).\n\n## Unix permissions issues\n\nThis image was designed with a user running as uid:gid of 1000:1000.\n\nIf your current user has different IDs, and you want to generate files directly editable for your user, you might want to run the container like this:\n\n```console\n$ mkdir data # or chown -R $(id -u):$(id -g) data\n$ docker run -it --rm -v $PWD/data:/out --user $(id -u):$(id -g) tyrrrz/discordchatexporter:stable export -t TOKEN -g CHANNELID\n```\n\n## Environment variables\n\nDiscordChatExpoter CLI accepts the `DISCORD_TOKEN` environment variable as a fallback for the `--token` option. You can set this variable either with the `--env` Docker option or with a combination of the `--env-file` Docker option and a `.env` file.\n\nPlease refer to the [Docker documentation](https://docs.docker.com/engine/reference/commandline/run/#set-environment-variables--e---env---env-file) for more information.\n"
  },
  {
    "path": ".docs/Getting-started.md",
    "content": "# Getting started\n\nWelcome to the getting started page!\nHere you'll learn how to use every **DiscordChatExporter** (DCE for short) feature.\nFor other things you can do with DCE, check out the [Guides](Readme.md#guides) section.\n\nIf you still have unanswered questions after reading this page or if you have encountered a problem, please visit our [FAQ & Troubleshooting](Troubleshooting.md) section.\n\nThe information presented on this page is valid for **all** platforms.\n\n## GUI or CLI?\n\n![GUI vs CLI](https://i.imgur.com/j9OTxRB.png)\n\n**DCE** has two different versions:\n\n- **Graphical User Interface** (**GUI**) - it's the preferred version for newcomers as it is easy to use.\n  You can get it by [downloading](https://github.com/Tyrrrz/DiscordChatExporter/releases/latest) the `DiscordChatExporter.*.zip` file.\n- **Command-line Interface** (**CLI**) - offers greater flexibility and more features for advanced users, such as export scheduling, ID lists, and more specific date ranges.\n  You can get it by [downloading](https://github.com/Tyrrrz/DiscordChatExporter/releases/latest) the `DiscordChatExporter.Cli.*.zip` file.\n\nThere are dedicated guides for each version:\n\n- [Using the GUI](Using-the-GUI.md)\n- [Using the CLI](Using-the-CLI.md)\n\n## File formats\n\n### HTML\n\n![](https://i.imgur.com/S7lBTkV.png)\nThe HTML format replicates Discord's interface, making it the most user-friendly option.\nIt's the best format for attachment preview and sharing.\nYou can open `.html` files with a web browser, such as Google Chrome.\n\n> [!WARNING]\n> If a picture is deleted, or if a user changes its avatar, the respective images will no longer be displayed.\n> Export using the \"Download assets\" (`--media`) option to avoid this.\n\n### Plain Text\n\n<img src=\"https://i.imgur.com/PbUyRXD.png\" height=\"400\"/>\n\nThe Plain Text format formats messages as plain text, and has the smallest size.\nYou can open `.txt` files with a text editor, such as Notepad.\n\n### JSON\n\n<img src=\"https://i.imgur.com/FAeSA4O.png\" height=\"400\"/>\n\nThe JSON format contains more technical information and is easily parsable.\nYou can open `.json` files with a text editor, such as Notepad.\n\n### CSV\n\n![](https://i.imgur.com/VEVUsKs.png)\n![](https://i.imgur.com/1vPmQqQ.png)\n\nThe CSV format allows for easy parsing of the chat log. Depending on your needs, the JSON format might be better.\nYou can open `.csv` files with a text editor, such as Notepad, or a spreadsheet app, like Microsoft Excel and Google Sheets.\n"
  },
  {
    "path": ".docs/Message-filters.md",
    "content": "# Message filters\n\nYou can use a special notation to filter messages that you want to have included in an export. The notation syntax is designed to mimic Discord's search query syntax, but with additional capabilities.\n\nTo configure a filter, specify it in advanced export parameters when using the GUI or by passing the `--filter` option when using the CLI. For the CLI version, see also [caveats](#cli-caveats).\n\n## Examples\n\n- Filter by user\n\n```console\nfrom:Tyrrrz\n```\n\n- Filter by user (with discriminator)\n\n```console\nfrom:Tyrrrz#1234\n```\n\n- Filter by message content (allowed values: `link`, `embed`, `file`, `video`, `image`, `sound`)\n\n```console\nhas:image\n```\n\n- Filter by mentioned user (same rules apply as with `from:` filter)\n\n```console\nmentions:Tyrrrz#1234\n```\n\n- Filter by contained text (has word \"hello\" and word \"world\" somewhere in the message text):\n\n```console\nhello world\n```\n\n- Filter by contained text (has the string \"hello world\" somewhere in the message text):\n\n```console\n\"hello world\"\n```\n\n- Combine multiple filters ('and'):\n\n```console\nfrom:Tyrrrz has:image\n```\n\n- Same thing but with an explicit operator:\n\n```console\nfrom:Tyrrrz & has:image\n```\n\n- Combine multiple filters ('or'):\n\n```console\nfrom:Tyrrrz | from:\"96-LB\"\n```\n\n- Combine multiple filters using groups:\n\n```console\n(from:Tyrrrz | from:\"96-LB\") has:image\n```\n\n- Negate a filter:\n\n```console\n-from:Tyrrrz | -has:image\n```\n\n- Negate a grouped filter:\n\n```console\n-(from:Tyrrrz has:image)\n```\n\n- Escape special characters (`-` is escaped below, so it's not parsed as negation operator):\n\n```console\nfrom:96\\-LB\n```\n\n## CLI Caveats\n\nIn most cases, you will need to enclose your filter in quotes (`\"`) to escape characters that may have special meaning in your shell:\n\n```console\n$ ./DiscordChatExporter.Cli export [...] --filter \"from:Tyrrrz has:image\"\n```\n\nIf you need to include quotes inside the filter itself as well, use single quotes (`'`) for those instead:\n\n```console\n$ ./DiscordChatExporter.Cli export [...] --filter \"from:Tyrrrz 'hello world'\"\n```\n\nAdditionally, negated filters (those that start with `-`) may cause parsing issues even when enclosed in quotes. To avoid this, use the tilde (`~`) character instead of the dash (`-`):\n\n```console\n$ ./DiscordChatExporter.Cli export [...] --filter ~from:Tyrrrz\n```\n"
  },
  {
    "path": ".docs/Readme.md",
    "content": "# Home\n\n## Installation & Usage\n\n- Getting started:\n  - [Using the GUI](Using-the-GUI.md)\n  - [Using the CLI](Using-the-CLI.md)\n  - [File formats](Getting-started.md#file-formats)\n\n## Guides\n\n- [How to get Token and Channel IDs](Token-and-IDs.md)\n- [How to use message filters](Message-filters.md)\n- Export scheduling with CLI:\n  - [Windows](Scheduling-Windows.md)\n  - [macOS](Scheduling-MacOS.md)\n  - [Linux](Scheduling-Linux.md)\n\n## Video tutorial\n\n- Video by [NoIntro Tutorials](https://youtube.com/channel/UCFezKSxdNKJe77-hYiuXu3Q) (using DiscordChatExporter GUI)\n\n[![Video tutorial](https://i.ytimg.com/vi/jjtu0VQXV7I/hqdefault.jpg)](https://youtube.com/watch?v=jjtu0VQXV7I)\n\n## FAQ & Troubleshooting\n\n- [General questions](Troubleshooting.md#general)\n- [First steps help](Troubleshooting.md#first-steps)\n- [It's crashing/failing](Troubleshooting.md#DCE-is-crashingfailing)\n- [Errors](Troubleshooting.md#errors)\n- [**More help**](Troubleshooting.md)\n"
  },
  {
    "path": ".docs/Scheduling-Linux.md",
    "content": "# Scheduling exports with Cron\n\n## Creating the script\n\n1. Open Terminal and create a new text file with `nano /path/to/DiscordChatExporter/cron.sh`\n\n> **Note**:\n> You can't use your mouse in nano, use the arrow keys to control the cursor (caret).\n\n2. Paste the following into the text file:\n\n```bash\n#!/bin/bash\n# Info: https://github.com/Tyrrrz/DiscordChatExporter/blob/prime/.docs\n\nTOKEN=tokenhere\nCHANNELID=channelhere\nDLLFOLDER=dceFOLDERpathhere\nFILENAME=filenamehere\nEXPORTDIRECTORY=dirhere\nEXPORTFORMAT=formathere\n# Available export formats: plaintext, htmldark, htmllight, json, csv\n# /\\ CaSe-SeNsItIvE /\\\n# You can edit the export command on line 40 if you'd like to include more options like date ranges and date format. You can't use partitioning (-p) with this script.\n\n# This will verify if EXPORTFORMAT is valid and will set the final file extension according to it. If the format is invalid, the script will display a message and exit.\nif [[ \"$EXPORTFORMAT\" == \"plaintext\" ]]; then\nFORMATEXT=.txt\nelif [[ \"$EXPORTFORMAT\" == \"htmldark\" ]] || [[ \"$EXPORTFORMAT\" == \"htmllight\" ]]; then\nFORMATEXT=.html\nelif [[ \"$EXPORTFORMAT\" == \"json\" ]]; then\nFORMATEXT=.json\nelif [[ \"$EXPORTFORMAT\" == \"csv\" ]]; then\nFORMATEXT=.csv\nelse\necho \"$EXPORTFORMAT - Unknown export format\"\necho \"Available export formats: plaintext, htmldark, htmllight, csv, json\"\necho \"/\\ CaSe-SeNsItIvE /\\\\\"\nexit 1\nfi\n\n# This will change the script's directory to DLLPATH, if unable to do so, the script will exit.\ncd $DLLFOLDER || exit 1\n\n# This will export your chat\n./DiscordChatExporter.Cli export -t $TOKEN -c $CHANNELID -f $EXPORTFORMAT -o $FILENAME.tmp\n\n# This sets the current time to a variable\nCURRENTTIME=$(date +\"%Y-%m-%d-%H-%M-%S\")\n\n# This will move the .tmp file to the desired export location, if unable to do so, it will attempt to delete the .tmp file.\nif ! mv \"$FILENAME.tmp\" \"${EXPORTDIRECTORY//\\\"}/$FILENAME-$CURRENTTIME$FORMATEXT\" ; then\necho \"Unable to move $FILENAME.tmp to $EXPORTDIRECTORY/$FILENAME-$CURRENTTIME$FORMATEXT.\"\necho \"Cleaning up...\"\n  if ! rm -Rf \"$FILENAME.tmp\" ; then\n  echo \"Unable to remove $FILENAME.tmp.\"\n  fi\nexit 1\nfi\nexit 0\n```\n\n3. Replace:\n\n- `tokenhere` with your [Token](Token-and-IDs.md).\n- `channelhere` with a [Channel ID](Token-and-IDs.md).\n- `dceFOLDERpathhere` with DCE's **directory path** (e.g. `/path/to/folder`, NOT `/path/to/folder/DiscordChatExporter.dll`).\n- `filenamehere` with the exported channel's filename, without spaces.\n- `dirhere` with the export directory (e.g. /home/user/Documents/Discord\\ Exports).\n- `formathere` with one of the available export formats.\n\n> **Note**:\n> Remember to escape spaces (add `\\` before them) or to quote (\") the paths (`\"/home/my user\"`)!\n\n> **Note**:\n> To save, hold down CTRL and then press O, if asked for a filename, type it and press ENTER. Hit CTRL+X to exit the text editor.\n> [Check out this page](https://wiki.gentoo.org/wiki/Nano/Basics_Guide) if you want to know more about nano.\n\n4. Make your script executable with `chmod +x /path/to/DiscordChatExporter/cron.sh`\n\n5. Let's edit the cron file. If you want to run the script with your user privileges, edit it by running `crontab -e`. If you want to run the script as root, edit it with `sudo crontab -e`. If this is your first time running this command, you might be asked to select a text editor. Nano is easier for beginners.\n\n6. Add the following to the end of the file `* * * * * /path/to/DiscordChatExporter/cron.sh >/tmp/discordchatexporter.log 2>/tmp/discordchatexportererror.log`. Don't forget to replace the `/path/to/DiscordChatExporter/cron.sh`!\n\n> **Note**:\n> If you don't want logs to be created, replace both `/tmp/discordchatexporter.log` with `/dev/null`.\n\nThen replace the \\*s according to:\n\n![](https://i.imgur.com/RY7USM6.png)\n\n---\n\n**Examples**:\n\n- If you want to execute the script at minute 15 of every hour: `15 * * * *`\n- Every 30 minutes `*/30 * * * *`\n- Every day at midnight `0 0 * * *`\n- Every day at noon `0 12 * * *`\n- Every day at 3, 4 and 6 PM `0 15,16,18 * * *`\n- Every Wednesday at 9 AM `0 9 * * 3`\n\nVerify your cron time [here](https://crontab.guru).\n\n---\n\n**Additional information**\n\nThe week starts on Sunday. 0 = SUN, 1 = MON ... 7 = SUN.\n\nBe aware that if you set the day to '31', the script will only run on months that have the 31st day.\n\n> [Learn more about running a cron job on the last day of the month here](https://stackoverflow.com/questions/6139189/cron-job-to-run-on-the-last-day-of-the-month) (expert).\n\nThe default filename for the exported channel is `YYYY-MM-DD-hh-mm-ss-yourfilename`. You can change it if you'd like.\n\nDon't forget to update your token in the script after it has been reset!\n\n---\n\nSpecial thanks to [@Yudi](https://github.com/Yudi)\n"
  },
  {
    "path": ".docs/Scheduling-MacOS.md",
    "content": "# Scheduling exports on macOS\n\n## Creating the script\n\n1. Open TextEdit.app and create a new file\n\n2. Convert the file to a plain text one in 'Format > Make Plain Text' (⇧⌘T)\n\n![](https://i.imgur.com/WXrTtXM.png)\n\n3. Paste the following into the text editor:\n\n```bash\n#!/bin/bash\n# Info: https://github.com/Tyrrrz/DiscordChatExporter/blob/prime/.docs\n\nTOKEN=tokenhere\nCHANNELID=channelhere\nDLLFOLDER=dceFOLDERpathhere\nFILENAME=filenamehere\nEXPORTDIRECTORY=dirhere\nEXPORTFORMAT=formathere\n# Available export formats: plaintext, htmldark, htmllight, json, csv\n# /\\ CaSe-SeNsItIvE /\\\n# You can edit the export command on line 43 if you'd like to include more options like date ranges and date format. You can't use partitioning (-p) with this script.\n\n# This variable specifies in which directories the executable programs are located. Don't change it.\nPATH=/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/share/dotnet\n\n# This will verify if EXPORTFORMAT is valid and will set the final file extension according to it. If the format is invalid, the script will display a message and exit.\nif [[ \"$EXPORTFORMAT\" == \"plaintext\" ]]; then\nFORMATEXT=.txt\nelif [[ \"$EXPORTFORMAT\" == \"htmldark\" ]] || [[ \"$EXPORTFORMAT\" == \"htmllight\" ]]; then\nFORMATEXT=.html\nelif [[ \"$EXPORTFORMAT\" == \"json\" ]]; then\nFORMATEXT=.json\nelif [[ \"$EXPORTFORMAT\" == \"csv\" ]]; then\nFORMATEXT=.csv\nelse\necho \"$EXPORTFORMAT - Unknown export format\"\necho \"Available export formats: plaintext, htmldark, htmllight, csv, json\"\necho \"/\\ CaSe-SeNsItIvE /\\\\\"\nexit 1\nfi\n\n# This will change the script's directory to DLLPATH, if unable to do so, the script will exit.\ncd $DLLFOLDER || exit 1\n\n# This will export your chat\n./DiscordChatExporter.Cli export -t $TOKEN -c $CHANNELID -f $EXPORTFORMAT -o $FILENAME.tmp\n\n# This sets the current time to a variable\nCURRENTTIME=$(date +\"%Y-%m-%d-%H-%M-%S\")\n\n# This will move the .tmp file to the desired export location. If unable to do so, it will attempt to delete the .tmp file.\nif ! mv \"$FILENAME.tmp\" \"${EXPORTDIRECTORY//\\\"}/$FILENAME-$CURRENTTIME$FORMATEXT\" ; then\necho \"Unable to move $FILENAME.tmp to $EXPORTDIRECTORY/$FILENAME-$CURRENTTIME$FORMATEXT.\"\necho \"Cleaning up...\"\n  if ! rm -Rf \"$FILENAME.tmp\" ; then\n  echo \"Unable to remove $FILENAME.tmp.\"\n  fi\nexit 1\nfi\nexit 0\n```\n\n4. Replace:\n\n- `tokenhere` with your [Token](Token-and-IDs.md)\n- `channelhere` with a [Channel ID](Token-and-IDs.md)\n- `dceFOLDERpathhere` with DCE's **directory's path** (e.g. `/Users/user/Desktop/DiscordChatExporterFolder`, NOT `/Users/user/Desktop/DiscordChatExporterFolder/DiscordChatExporter.DLL`)\n- `filenamehere` with the exported channel's filename, without spaces\n- `dirhere` with the directory you want the files to be saved at (e.g. `/Users/user/Documents/Discord\\ Exports`)\n- `formathere` with one of the available export formats\n\nTo quickly get file or folder paths, select the file/folder, then hit Command+I (⌘I) and copy what's after `Where:`.\nAfter copying and pasting, make sure the file/folder name is not missing. If a folder has spaces in its name, add `\\` before the spaces, like in the example below:\n\n- `Discord\\ Exports` - Wrong ✗\n- `/Users/user/Documents` - Wrong ✗\n- `/Users/user/Documents/Discord Exports` - Wrong ✗\n- `/Users/user/Documents/Discord\\ Exports/DCE.Cli.dll` - Wrong ✗\n- `/Users/user/Documents/Discord \\Exports` - Wrong ✗\n- `/Users/user/Documents/Discord\\ Exports` - Correct ✓\n- `/Users/user/Desktop/DiscordChatExporter` - Correct ✓\n\n![Screenshot of mac info window](https://i.imgur.com/29u6Nyx.png)\n\n5. Save the file as `filename.sh`, not `.txt`\n6. Open Terminal.app, type `chmod +x`, press the SPACE key, then drag & drop the `filename.sh` into the Terminal window and hit RETURN. You may be prompted for your password, and you won't be able to see it as you type.\n\n## Creating the .plist file\n\nOpen TextEdit, make a Plain Text (⇧⌘T) and then paste the following into it:\n\n```xml\n<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n  <dict>\n    <key>Label</key>\n    <string>local.discordchatexporter</string>\n    <key>Program</key>\n    <string>/path/to/filename.sh</string>\n    REPLACEME\n  </dict>\n</plist>\n```\n\n- The `Label` string is the name of the export job, it must be something unique. Replace the `local.discordchatexporter` between the `<string>` with another name if you'd like to run more than one script.\n- The `Program` string is the path to the script. Replace `/path/to/filename.sh` between the `<string>` with the path of the previously created script.\n- Replace the `REPLACEME` with the content presented in the following sections according to <u>when</u> you want to export.\n\nWhen you're done, save the file with the same name as the `Label` and with the `.plist` extension (not `.txt`), like `local.discordchatexporter.plist`.\n\n### Exporting on System Boot/User Login\n\n```xml\n<key>RunAtLoad</key>\n<true/>\n```\n\n### Export every _n_ seconds\n\nThe following example is to export every 3600 seconds (1 hour), replace the integer value with your desired time:\n\n```xml\n<key>StartInterval</key>\n<integer>3600</integer>\n```\n\n### Export at a specific time and date\n\n```xml\n<key>StartCalendarInterval</key>\n<dict>\n  <key>Weekday</key>\n  <integer>0</integer>\n  <key>Month</key>\n  <integer>0</integer>\n  <key>Day</key>\n  <integer>0</integer>\n  <key>Hour</key>\n  <integer>0</integer>\n  <key>Minute</key>\n  <integer>0</integer>\n</dict>\n```\n\n| Key         | Integer           |\n| ----------- | ----------------- |\n| **Month**   | 1-12              |\n| **Day**     | 1-31              |\n| **Weekday** | 0-6 (0 is Sunday) |\n| **Hour**    | 0-23              |\n| **Minute**  | 0-59              |\n\n**Sunday** - 0; **Monday** - 1; **Tuesday** - 2; **Wednesday** - 3; **Thursday** - 4; **Friday** - 5; **Saturday** - 6\n\nReplace the template's `0`s according to the desired times.\n\nYou can delete the `<key>`s you don't need, don't forget to remove the `<integer>0</integer>` under it.\nOmitted keys are interpreted as wildcards, for example, if you delete the Minute key, the script will run at every minute, delete the Weekday key and it'll run at every weekday, and so on.\n\nBe aware that if you set the day to '31', the script will only run on months that have the 31st day.\n\n**Check the examples below ([or skip to step 3 (loading the file)](#3-loading-the-plist-into-launchctl)):**\n\nExport everyday at 5:15 PM:\n\n```xml\n<key>StartCalendarInterval</key>\n<dict>\n  <key>Hour</key>\n  <integer>17</integer>\n  <key>Minute</key>\n  <integer>15</integer>\n</dict>\n\n```\n\nEvery 15 minutes of an hour (xx:15):\n\n```xml\n<key>StartCalendarInterval</key>\n<dict>\n  <key>Minute</key>\n  <integer>15</integer>\n</dict>\n\n```\n\nEvery Sunday at midnight and every Wednesday full hour (xx:00). Notice the inclusion of `<array>` and `</array>` to allow multiple values:\n\n```xml\n<key>StartCalendarInterval</key>\n<array>\n  <dict>\n    <key>Weekday</key>\n    <integer>0</integer>\n    <key>Hour</key>\n    <integer>00</integer>\n    <key>Minute</key>\n    <integer>00</integer>\n  </dict>\n  <dict>\n    <key>Weekday</key>\n    <integer>3</integer>\n    <key>Minute</key>\n    <integer>00</integer>\n  </dict>\n</array>\n```\n\n## Loading the .plist into launchctl\n\n1. Copy your `filename.plist` file to one of these folders according to how you want it to run:\n\n- `~/Library/LaunchAgents` runs as the current logged-in user.\n\n- `/Library/LaunchDaemons` runs as the system \"_administrator_\" (root).\n\n- If macOS has a single user:\n  - If you want to export only when the user is logged in, choose the first one.\n  - If you want the script to always run on System Startup, choose the second one.\n- If macOS has multiple users:\n  - If you want the script to run only when a certain user is logged in, choose the first one.\n  - If you want the script to always run on System Startup, choose the second one.\n\nTo quickly go to these directories, open Finder and press Command+Shift+G (⌘⇧G), then paste the path into the text box.\n\n2. To load the job into launchctl, in Terminal, type `launchctl load`, press SPACE, drag and drop the `.plist` into the Terminal window, then hit RETURN. It won't output anything if it was successfully loaded.\n\n### Extra launchctl commands\n\n**Unloading a job**\n\n```\nlaunchctl unload /path/to/Library/LaunchAgents/local.discordchatexporter.plist\n```\n\n**List every loaded job**\n\n```\nlaunchctl list\n```\n\n**Check if a specific job is enabled**\nYou can also see error codes (2nd number) by running this command.\n\n```\nlaunchctl list | grep local.discordchatexporter\n```\n\n---\n\nFurther reading: [Script management with launchd in Terminal on Mac](https://support.apple.com/guide/terminal/script-management-with-launchd-apdc6c1077b-5d5d-4d35-9c19-60f2397b2369/mac) and [launchd.info](https://launchd.info/).\nSpecial thanks to [@Yudi](https://github.com/Yudi)\n"
  },
  {
    "path": ".docs/Scheduling-Windows.md",
    "content": "# Scheduling exports on Windows\n\n## Creating the script\n\n1. Open a text editor such as Notepad and paste:\n\n```console\n# Info: https://github.com/Tyrrrz/DiscordChatExporter/blob/prime/.docs\n\n$TOKEN = \"tokenhere\"\n$CHANNEL = \"channelhere\"\n$EXEPATH = \"exefolderhere\"\n$FILENAME = \"filenamehere\"\n$EXPORTDIRECTORY = \"dirhere\"\n$EXPORTFORMAT = \"formathere\"\n# Available export formats: PlainText, HtmlDark, HtmlLight, Json, Csv\n\ncd $EXEPATH\n\n./DiscordChatExporter.Cli export -t $TOKEN -c $CHANNEL -f $EXPORTFORMAT -o \"$FILENAME.tmp\"\n\n$Date = Get-Date -Format \"yyyy-MM-dd-HH-mm\"\n\nIf($EXPORTFORMAT -match \"PlainText\"){mv \"$FILENAME.tmp\" -Destination \"$EXPORTDIRECTORY\\$FILENAME-$Date.txt\"}\nElseIf($EXPORTFORMAT -match \"HtmlDark\"){mv \"$FILENAME.tmp\" -Destination \"$EXPORTDIRECTORY\\$FILENAME-$Date.html\"}\nElseIf($EXPORTFORMAT -match \"HtmlLight\"){mv \"$FILENAME.tmp\" -Destination \"$EXPORTDIRECTORY\\$FILENAME-$Date.html\"}\nElseIf($EXPORTFORMAT -match \"Json\"){mv \"$FILENAME.tmp\" -Destination \"$EXPORTDIRECTORY\\$FILENAME-$Date.json\"}\nElseIf($EXPORTFORMAT -match \"Csv\"){mv \"$FILENAME.tmp\" -Destination \"$EXPORTDIRECTORY\\$FILENAME-$Date.csv\"}\nexit\n```\n\n2. Replace:\n\n- `tokenhere` with your [Token](Token-and-IDs.md)\n- `channelhere` with a [Channel ID](Token-and-IDs.md)\n- `exefolderhere` with the .exe **directory's path** (e.g. C:\\Users\\User\\Desktop\\DiscordChatExporter)\n- `filenamehere` with a filename without spaces\n- `dirhere` with the export directory (e.g. C:\\Users\\User\\Documents\\Exports)\n- `formathere` with one of the available export formats\n\nMake sure not to delete the quotes (\")\n\n3. Save the file as `filename.ps1`, not as `.txt`\n\n> **Note**: You can also modify the script to use other options, such as `include-threads` or switch to a different command, e. g. `exportguild`.\n\n## Export at Startup\n\n1. Press Windows + R, type `shell:startup` and press ENTER\n2. Paste `filename.ps1` or a shortcut into this folder\n\n## Scheduling with Task Scheduler\n\nPlease note that your computer must be turned on for the export to happen.\n\n1. Press Windows + R, type `taskschd.msc` and press ENTER\n2. Select `Task Scheduler Library`, create a Basic Task, and follow the instructions on-screen\n\n<img src=\"https://i.imgur.com/MHRVGDi.png\" height=\"500\"/>\n\n![Screenshot from Task Scheduler](https://i.imgur.com/m2DKhA8.png)\n\n3. At 'Start a Program', write `powershell -file -ExecutionPolicy ByPass -WindowStyle Hidden \"C:\\path\\to\\filename.ps1\"` in the Program/script text box\n\n![](https://i.imgur.com/FGtWRod.png)\n\n4. Click 'Yes'\n\n![](https://i.imgur.com/DuaRBt3.png)\n\n5. Click 'Finish'\n\n![](https://i.imgur.com/LHgXp9Q.png)\n\n---\n\nSpecial thanks to [@Yudi](https://github.com/Yudi)\n"
  },
  {
    "path": ".docs/Token-and-IDs.md",
    "content": "# Obtaining Token and Channel IDs\n\n> [!WARNING]\n> **Do not share your token!** A token gives full access to an account.  \n> To reset a user token, change your account password.  \n> To reset a bot token, click on [Reset Token](#how-to-export-with-a-bot-token) in the bot settings.\n\n## How to get a user token\n\n**Caution:** [Automating user accounts violates Discord's terms of service](https://support.discord.com/hc/en-us/articles/115002192352-Automated-user-accounts-self-bots-) and may result in account termination. Use at your own risk.\n\n### Through your web browser\n\nPrerequisite step: Navigate to [discord.com](https://discord.com) and login.\n\n#### In Chrome\n\n##### Using the console\n\n1. <img width=\"500\" align=\"right\" src=\"https://i.imgur.com/zdDwIT5.jpg\" />Press <kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>I</kbd> (<kbd>⌥</kbd>+<kbd>⌘</kbd>+<kbd>I</kbd> on macOS). Chrome's [DevTools](https://developer.chrome.com/docs/devtools/overview) tools will display.\n\n<br clear=\"right\" />\n<br />\n\n2. Click the `Console` tab. The [console](https://developer.chrome.com/docs/devtools/console/) will open.\n\n3. Type\n\n    ```js\n    let m;webpackChunkdiscord_app.push([[Math.random()],{},e=>{for(let i in e.c){let x=e.c[i];if(x?.exports?.getToken){m=x;break}}}]);m&&console.log(\"Token:\",m.exports.getToken());\n    ```\n\n    into the console and press <kbd>Enter</kbd>. The console will display your user token.\n\n##### Using the network monitor\n\n1. <img width=\"500\" align=\"right\" src=\"https://i.imgur.com/zdDwIT5.jpg\" />Press <kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>I</kbd> (<kbd>⌥</kbd>+<kbd>⌘</kbd>+<kbd>I</kbd> on macOS). Chrome's [DevTools](https://developer.chrome.com/docs/devtools/overview) tools will display.\n\n<br clear=\"right\" />\n<br />\n\n2. <img width=\"500\" align=\"right\" src=\"https://i.imgur.com/BDeG0zg.png\" />Click the `Network` tab. The [network panel](https://developer.chrome.com/docs/devtools/overview/#network) will open\n\n<br clear=\"right\" />\n<br />\n\n3. <img width=\"500\" align=\"right\" src=\"https://i.imgur.com/0Lgj0vk.png\" />Press <kbd>F5</kbd>. The page will reload, and the network log (the lower half of the network panel) will display several entries.\n\n<br clear=\"right\" />\n<br />\n\n4. <img width=\"500\" align=\"right\" src=\"https://i.imgur.com/rnZG8Id.png\" />Click the text box labelled `Filter` and type `messages`. The entries will filter down to a single request named `messages`. If the request doesn't appear, switch to any other Discord channel to trigger it.\n\n<br clear=\"right\" />\n<br />\n\n5. <img width=\"500\" align=\"right\" src=\"https://i.imgur.com/29dE3fR.png\" />Click the entry named `messages`. A panel will open to the right and display details about the entry. Click the `Headers` tab if it isn't already active.\n\n<br clear=\"right\" />\n<br />\n\n6. <img width=\"500\" align=\"right\" src=\"https://i.imgur.com/u7CxXAt.png\" />Scroll through the contents of the `Headers` tab until you find an entry beginning with `authorization:`.\n\n<br clear=\"right\" />\n<br />\n\n7. <img width=\"500\" align=\"right\" src=\"https://i.imgur.com/dXcXzma.png\" />Right-click the entry and click `copy value`.\n\n<br clear=\"right\" />\n<br />\n\n##### Using the storage inspector\n\n1. <img width=\"500\" align=\"right\" src=\"https://i.imgur.com/zdDwIT5.jpg\" />Press <kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>I</kbd> (<kbd>⌥</kbd>+<kbd>⌘</kbd>+<kbd>I</kbd> on macOS). Chrome's [DevTools](https://developer.chrome.com/docs/devtools/overview/) will display.\n\n<br clear=\"right\" />\n<br />\n\n2. <img width=\"500\" align=\"right\" src=\"https://i.imgur.com/biAUIop.png\" />Press <kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>M</kbd> (<kbd>⌘</kbd>+<kbd>Shift</kbd>+<kbd>M</kbd>). Chrome will enter [Device Mode](https://developer.chrome.com/docs/devtools/device-mode/), and the webpage will display as if on a mobile device.\n\n<br clear=\"right\" />\n<br />\n\n3. <img width=\"500\" align=\"right\" src=\"https://i.imgur.com/oUDRZoy.png\" />If necessary, click the `»` at the right end of the tab bar, and click `Application`. The [application panel](https://developer.chrome.com/docs/devtools/overview/#application) will display.\n\n<br clear=\"right\" />\n<br />\n\n4. <img width=\"500\" align=\"right\" src=\"https://i.imgur.com/sydNPia.png\" />In the menu to the right, under `Storage`, expand `Local Storage` if necessary, then click `https://discord.com`. The pane to the right will display a list of key-value pairs.\n\n<br clear=\"right\" />\n<br />\n\n5. <img width=\"500\" align=\"right\" src=\"https://i.imgur.com/qKo0ny9.png\" />In the text box marked `Filter`, type `token`. The entries will filter down to those containing the string `token`.\n\n<br clear=\"right\" />\n<br />\n\n6. <img width=\"500\" align=\"right\" src=\"https://i.imgur.com/caj3lQq.png\" />Click the `token` entry. (Note: if the token doesn't display, try refreshing by pressing <kbd>F5</kbd> or <kbd>⌘</kbd>+<kbd>R</kbd> on macOS)\n\n<br clear=\"right\" />\n<br />\n\n7. <img width=\"500\" align=\"right\" src=\"https://i.imgur.com/SwWFIH4.png\" />Click the text box at the bottom, press <kbd>Ctrl</kbd>+<kbd>A</kbd> (<kbd>⌘</kbd>+<kbd>A</kbd> on macOS) then <kbd>Ctrl</kbd>+<kbd>C</kbd> (<kbd>⌘</kbd>+<kbd>C</kbd> on macOS) to copy the value to your clipboard.\n\n<br clear=\"right\" />\n<br />\n\n#### In Firefox\n\n##### Using the console\n\n1. <img width=\"500\" align=\"right\" src=\"https://i.imgur.com/O34nwdG.png\" />Press <kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>K</kbd> (<kbd>⌥</kbd>+<kbd>⌘</kbd>+<kbd>K</kbd> on macOS). Firefox’s [web developer tools](https://firefox-source-docs.mozilla.org/devtools-user/) will display at the bottom of the window, and the [web console](https://firefox-source-docs.mozilla.org/devtools-user/console/index.html) will display.\n\n<br clear=\"right\" />\n<br />\n\n2. Click the `Console` tab. The [console](https://firefox-source-docs.mozilla.org/devtools-user/console/index.html) will open.\n\n1. Type\n\n    ```js\n    let m;webpackChunkdiscord_app.push([[Math.random()],{},e=>{for(let i in e.c){let x=e.c[i];if(x?.exports?.getToken){m=x;break}}}]);m&&console.log(\"Token:\",m.exports.getToken());\n    ```\n\n    into the console and press <kbd>Enter</kbd>. The console will display your user token.\n\n##### Using the network monitor\n\n1. <img width=\"500\" align=\"right\" src=\"https://i.imgur.com/O34nwdG.png\" />Press <kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>E</kbd> (<kbd>⌥</kbd>+<kbd>⌘</kbd>+<kbd>E</kbd> on macOS). Firefox’s [web developer tools](https://firefox-source-docs.mozilla.org/devtools-user/) will display at the bottom of the window, and the [network monitor](https://firefox-source-docs.mozilla.org/devtools-user/network_monitor/) will display.\n\n<br clear=\"right\" />\n<br />\n\n2. <img width=\"500\" align=\"right\" src=\"https://i.imgur.com/j00QzhU.png\" />Press <kbd>F5</kbd>. The page will reload, and the [network request list](https://firefox-source-docs.mozilla.org/devtools-user/network_monitor/request_list/index.html) will populate with entries.\n\n<br clear=\"right\" />\n<br />\n\n3. <img width=\"500\" align=\"right\" src=\"https://i.imgur.com/efUCfBO.png\" />Type `messages` into the filter. The network request list will filter out any entries not containing the string `messages`. If the request doesn't appear, switch to any other Discord channel to trigger it.\n\n<br clear=\"right\" />\n<br />\n\n4. <img width=\"500\" align=\"right\" src=\"https://i.imgur.com/cdJZ7Q1.png\" />Click `messages`. The [network request details pane](https://firefox-source-docs.mozilla.org/devtools-user/network_monitor/request_details/index.html) will display. The [headers tab](https://firefox-source-docs.mozilla.org/devtools-user/network_monitor/request_details/index.html#network-monitor-request-details-headers-tab) should be active by default. If it isn’t, click it.\n\n<br clear=\"right\" />\n<br />\n\n5. <img width=\"500\" align=\"right\" src=\"https://i.imgur.com/zBmq1JW.png\" />Type `authorization` into the text box labelled `Filter Headers`.\n\n<br clear=\"right\" />\n<br />\n\n6. <img width=\"500\" align=\"right\" src=\"https://i.imgur.com/O3blcIS.png\" />Scroll down until you see an entry labeled [authorization](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Authorization) under `Request Headers`.\n\n<br clear=\"right\" />\n<br />\n\n7. <img width=\"500\" align=\"right\" src=\"https://i.imgur.com/zHYEYoZ.png\" />Right-click the entry labeled `authorization` and select `copy value`.\n\n<br clear=\"right\" />\n<br />\n\n##### Using the storage inspector\n\n1. <img width=\"500\" align=\"right\" src=\"https://i.imgur.com/A8jqpkm.png\" />Press <kbd>Shift</kbd>+<kbd>F9</kbd>. Firefox’s [web developer tools](https://firefox-source-docs.mozilla.org/devtools-user/) will display at the bottom of the window, and the [storage](https://firefox-source-docs.mozilla.org/devtools-user/storage_inspector/index.html) panel will be selected.\n\n<br clear=\"right\" />\n<br />\n\n2. <img width=\"500\" align=\"right\" src=\"https://i.imgur.com/TGcbB7f.png\" />Press <kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>M</kbd> (<kbd>⌥</kbd>+<kbd>⌘</kbd>+<kbd>M</kbd> on macOS). Firefox will toggle [responsive design mode](https://firefox-source-docs.mozilla.org/devtools-user/responsive_design_mode/), and the web page will display as if on a mobile device. (Note: Discord may steal focus and respond to the command by toggling mute. If this happens, return focus to Firefox’s web developer tools by clicking somewhere in it, then try the command again.)\n\n<br clear=\"right\" />\n<br />\n\n3. <img width=\"500\" align=\"right\" src=\"https://i.imgur.com/2xWkep9.png\" />In the [storage tree](https://firefox-source-docs.mozilla.org/devtools-user/storage_inspector/index.html#storage-inspector-storage-tree) (the list on the left side of the web developer tools panel), click [Local Storage](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage). The entry will expand, and the entry `https://discord.com` will display beneath it.\n\n<br clear=\"right\" />\n<br />\n\n4. <img width=\"500\" align=\"right\" src=\"https://i.imgur.com/tGlGuOL.png\" />In the storage tree, click `https://discord.com`. The [table widget](https://firefox-source-docs.mozilla.org/devtools-user/storage_inspector/index.html#storage-inspector-table-widget) to the right of the storage tree will display several key-value pairs.\n\n<br clear=\"right\" />\n<br />\n\n5. <img width=\"500\" align=\"right\" src=\"https://i.imgur.com/hDNsnZ5.png\" />In the text box labelled `Filter items` at the top of the table widget, enter `token`. The table will now only display entries containing the string `token`.\n\n<br clear=\"right\" />\n<br />\n\n6. <img width=\"500\" align=\"right\" src=\"https://i.imgur.com/8fKId1W.png\" />Click the entry `token`. The [sidebar](https://firefox-source-docs.mozilla.org/devtools-user/storage_inspector/index.html#storage-inspector-sidebar) will display. (Note: If the token doesn’t display, try refreshing by pressing <kbd>F5</kbd>.)\n\n<br clear=\"right\" />\n<br />\n\n7. <img width=\"500\" align=\"right\" src=\"https://i.imgur.com/yD1ZuR9.png\" />Right-click the single entry in the sidebar and select `copy`.\n\n<br clear=\"right\" />\n<br />\n\n### Through the desktop app / enabling web developer tools\n\n#### By editing the settings file\n\n1. If Discord is running, exit the application by right-clicking the icon in your taskbar tray and clicking `Quit Discord`.\n\n2. Open Discord's settings file in your preferred text editor. See the following table for help finding it:\n\n   | OS      | Stable                                                | Canary                                                      | Public Test Build (PTB)                                  |\n   | ------- | ----------------------------------------------------- | ----------------------------------------------------------- | -------------------------------------------------------- |\n   | Windows | `%APPDATA%\\discord\\settings.json`                     | `%APPDATA%\\discordcanary\\settings.json`                     | `%APPDATA%\\discordptb\\settings.json`                     |\n   | macOS   | `~/Library/Application Support/discord/settings.json` | `~/Library/Application Support/discordcanary/settings.json` | `~/Library/Application Support/discordptb/settings.json` |\n   | Linux   | `~/.config/discord/settings.json`                     | `~/.config/discordcanary/settings.json`                     | `~/.config/discordptb/settings.json`                     |\n\n   If you use BetterDiscord, use the following table instead:\n\n   | OS      | Stable                                                                  | Canary                                                                  | Public Test Build (PTB)                                              |\n   | ------- | ----------------------------------------------------------------------- | ----------------------------------------------------------------------- | -------------------------------------------------------------------- |\n   | Windows | `%APPDATA%\\BetterDiscord\\data\\stable\\settings.json`                     | `%APPDATA%\\BetterDiscord\\data\\canary\\settings.json`                     | `%APPDATA%\\BetterDiscord\\data\\ptb\\settings.json`                     |\n   | macOS   | `~/Library/Application Support/BetterDiscord/data/stable/settings.json` | `~/Library/Application Support/BetterDiscord/data/canary/settings.json` | `~/Library/Application Support/BetterDiscord/data/ptb/settings.json` |\n   | Linux   | `~/.config/BetterDiscord/data/stable/settings.json`                     | `~/.config/BetterDiscord/data/canary/settings.json`                     | `~/.config/BetterDiscord/data/ptb/settings.json`                     |\n\n3. Insert a blank line after the first curly bracket (`{`), add the text `\"DANGEROUS_ENABLE_DEVTOOLS_ONLY_ENABLE_IF_YOU_KNOW_WHAT_YOURE_DOING\": true,` to it, and save the file. Your file should resemble the following:\n\n```json\n{\n  \"DANGEROUS_ENABLE_DEVTOOLS_ONLY_ENABLE_IF_YOU_KNOW_WHAT_YOURE_DOING\": true,\n  \"BACKGROUND_COLOR\": \"#202225\",\n  \"IS_MAXIMIZED\": true\n}\n```\n\n4. Launch Discord.\n\n5. To find your user token, continue [here](#in-chrome).\n\n#### Via settings menu (BetterDiscord only)\n\n1. <img width=\"500\" align=\"right\" src=\"https://i.imgur.com/mu1g4OF.png\" />Click the User Settings button (the gear icon to the right of your username). Discord’s settings page will open.\n\n<br clear=\"right\" />\n<br />\n\n2. <img width=\"500\" align=\"right\" src=\"https://i.imgur.com/qFrIKON.png\" />In the sidebar to the left, click `Settings` under the `BetterDiscord` group. BetterDiscord’s settings page will display.\n\n<br clear=\"right\" />\n<br />\n\n3. <img width=\"500\" align=\"right\" src=\"https://i.imgur.com/48Re5kj.png\" />In the main panel to the right, expand the `Developer Settings` group if necessary, and toggle `DevTools` to enabled.\n\n<br clear=\"right\" />\n<br />\n\n4. Press <kbd>Esc</kbd>. The settings page will close.\n5. To find your user token, continue [here](#in-chrome).\n\n## How to export with a bot token\n\n### Step 1 - Create an application\n\nYou can create a new application or use an existing one. If you want to create a new one:\n\n1. Go to [Discord developer portal](https://discord.com/developers/applications)\n2. Click on **New Application** in the top right corner\n3. Enter a name for your application and click **Create**\n\n### Step 2 - Invite the bot to your server\n\nThe bot needs to be invited to the server you'd like to export from.\n\n1. Go to [Discord developer portal](https://discord.com/developers/applications)\n2. Navigate to **General Information** on the left\n3. Copy the **Application ID**\n4. Open the following URL in your browser, replacing `YOUR_APP_ID` with the copied Client ID:\n\n<!-- Permission code 66560 corresponds to \"View Channels\" and \"Read Message History\" permissions.\n      User can uncheck these when adding the bot to their server. -->\n\n```\nhttps://discord.com/oauth2/authorize?scope=bot&permissions=66560&client_id=YOUR_APP_ID\n```\n\n### Step 3 - Ensure message content intent is enabled\n\nIf this option is not enabled, the exported files will be empty.\n\n1. Go to [Discord developer portal](https://discord.com/developers/applications)\n2. Open your Application's settings\n3. Navigate to the **Bot** section on the left\n4. Scroll down to the **Privileged Gateway Intents** section\n5. Enable **Message Content Intent** by toggling the switch\n\n<img width=\"500\" align=\"right\" src=\"https://i.imgur.com/PPm2KKn.png\" />\n\n### Step 4 - Copy the bot token\n\nIf you don't have a bot token yet or if you've lost it, follow these steps to reset it:\n\n1. Go to [Discord developer portal](https://discord.com/developers/applications)\n2. Open your Application's settings\n3. Navigate to the **Bot** section on the left\n4. Under **Token** click **Reset Token**\n5. Click **Yes, do it!** and authenticate to confirm\n\n> **Tip**:\n> As the token is only shown once, make sure to store it in a safe place. If you lose the token, you will have to reset it again.\n\n> [!WARNING]\n> Resetting the token will invalidate the old one. Any integrations relying on the old token will cease to function until they are updated.\n\n![https://discord.com/developers/applications/](https://i.imgur.com/soiB8Qc.png)\n\n---\n\n## How to get a Server ID or a Channel ID\n\n1. Open Discord Settings\n2. Go to the **Advanced** section\n3. Enable **Developer Mode**\n4. Right-click on the desired server or channel and click **Copy Server ID** or **Copy Channel ID**\n"
  },
  {
    "path": ".docs/Troubleshooting.md",
    "content": "# Troubleshooting\n\nWelcome to the Frequently Asked Questions (FAQ) and Troubleshooting page!\nHere you'll find the answers to most of the questions related to **DiscordChatExporter** (DCE for short) and its core features.\n\n- ❓ If you still have unanswered questions _after_ reading this page, feel free to [create a new discussion](https://github.com/Tyrrrz/DiscordChatExporter/discussions/new).\n- 🐞 If you have encountered a problem that's not described here, has not [been discussed before](https://github.com/Tyrrrz/DiscordChatExporter/discussions), and is not a [known issue](https://github.com/Tyrrrz/DiscordChatExporter/issues?q=is%3Aissue), please [create a new discussion](https://github.com/Tyrrrz/DiscordChatExporter/discussions/new) or [open a bug report](https://github.com/Tyrrrz/DiscordChatExporter/issues/new). Don't forget to include your platform (Windows, Mac, Linux, etc.) and a detailed description of your question/problem.\n\n## General questions\n\n### Token stealer?\n\nNo. That's why this kind of software needs to be open-source, so the code can be audited by anyone.\nYour token is only used to connect to Discord's API, it's not sent anywhere else.\nIf you're using the GUI, be aware that your token will be saved to a plain text file unless you disable it in the settings menu.\n\n### Why should I be worried about the safety of my token?\n\nA token can be used to log into your account, so treat it like a password and never share it.\n\n### How can I reset my token?\n\nFollow the [instructions here](Token-and-IDs.md).\n\n### Will I get banned if I use this?\n\nAutomating user accounts is technically against [TOS](https://discord.com/terms), use at your discretion. [Bot accounts](https://discord.com/developers/docs/topics/oauth2#bot-users) don't have this restriction.\n\n### Will the messages disappear from the exported file if I delete a message, delete my account or block a person?\n\nText messages will not be removed from the exported file, but if media, such as images and user avatars, is changed or deleted, it will no longer be displayed. To avoid this, export using the \"Download media\" (`--media`) option.\n\n### Can DCE export messages that have already been deleted?\n\nNo, DCE cannot access them since they have been permanently deleted from Discord's servers.\n\n### Can DCE export private chats?\n\nYes, if your account has access to them.\n\n### Can DCE download images?\n\nYes, and other media too. Export using the \"Download media\" (`--media`) option.\n\n### Can the exported chats be shared?\n\nYes.\n\n### Can DCE export multiple formats at once?\n\nNo, you can only export one format at a time.\n\n### Can DCE recreate the exported chats in Discord?\n\nNo, DCE is an exporter.\n\n### Can DCE reupload exported messages to another channel?\n\nNo, DCE is an exporter.\n\n### Can DCE add new messages to an existing export?\n\nNo.\n\n## First steps\n\n### How can I find my token?\n\nCheck the following page: [Obtaining token](Token-and-IDs.md)\n\n### When I open DCE a black window pops up quickly or nothing shows up\n\nYou might have downloaded the CLI flavor of the app, which is meant to be run in a terminal. Try [downloading the GUI](Getting-started.md#gui-or-cli) instead if that's what you want.\n\n### How can I set DCE to export automatically at certain times?\n\nCheck the following pages to learn how to schedule **DiscordChatExporter.CLI** runs (advanced):\n\n- [Windows scheduling](Scheduling-Windows.md)\n- [macOS scheduling](Scheduling-MacOS.md)\n- [Linux scheduling](Scheduling-Linux.md)\n\n### The exported file is too large, I can't open it\n\nTry opening it with a different program, try partitioning or use a different file format, like `PlainText`.\n\n### I see messages in the export, but they have no content\n\nYour bot is missing the 'Message Content Intent'. Go to the [Discord Developer Portal](https://discord.com/developers/applications), navigate to the 'Bot' section and enable it.\n\n## CLI\n\n### How do I use the CLI?\n\nCheck the following page:\n\n- [Using the CLI](Using-the-CLI.md)\n\nIf you're using **Docker**, please refer to the [Docker Usage Instructions](Docker.md) instead.\n\n### Where can I find the 'Channel IDs'?\n\nCheck the following page:\n\n- [Obtaining Channel IDs](Token-and-IDs.md)\n\n### I can't find Docker exported chats\n\nCheck the following page:\n\n- [Docker usage instructions](Docker.md)\n\n### I can't export Direct Messages\n\nMake sure you're [copying the DM Channel ID](Token-and-IDs.md#how-to-get-a-direct-message-channel-id), not the person's user ID.\n\n## Errors\n\n```yml\nDiscordChatExporter.Domain.Exceptions.DiscordChatExporterException: Authentication token is invalid.\n```\n\n↳ Make sure the provided token is correct.\n\n```yml\nDiscordChatExporter.Domain.Exceptions.DiscordChatExporterException: Requested resource does not exist.\n```\n\n↳ Check your channel ID, it might be invalid. [Read this if you need help](Token-and-IDs.md).\n\n```yml\nDiscordChatExporter.Domain.Exceptions.DiscordChatExporterException: Access is forbidden.\n```\n\n↳ This means you don't have access to the channel.\n\n```yml\nSystem.Net.WebException: Error: TrustFailure ... Invalid certificate received from server.\n```\n\n↳ Try running cert-sync.\n\nDebian/Ubuntu: `cert-sync /etc/ssl/certs/ca-certificates.crt`\n\nRed Hat: `cert-sync --user /etc/pki/tls/certs/ca-bundle.crt`\n\nIf it still doesn't work, try mozroots: `mozroots --import --ask-remove`\n\n## macOS-specific\n\n### DiscordChatExporter is damaged and can’t be opened. You should move it to the Trash.\n\nCheck the [Using the GUI page](Using-the-GUI.md#step-1) for instructions on how to run the app.\n\n---\n\n> ❓ If you still have unanswered questions, feel free to [create a new discussion](https://github.com/Tyrrrz/DiscordChatExporter/discussions/new).\n>\n> 🐞 If you have encountered a problem that's not described here, has not [been discussed before](https://github.com/Tyrrrz/DiscordChatExporter/discussions), and is not a [known issue](https://github.com/Tyrrrz/DiscordChatExporter/issues?q=is%3Aissue), please [create a new discussion](https://github.com/Tyrrrz/DiscordChatExporter/discussions/new) or [open a bug report](https://github.com/Tyrrrz/DiscordChatExporter/issues/new).\n"
  },
  {
    "path": ".docs/Using-the-CLI.md",
    "content": "# Using the CLI\n\n## Step 1\n\nAfter extracting the `.zip` archive, open your preferred terminal.\n\n## Step 2\n\nChange the current directory to DCE's folder with `cd C:\\path\\to\\DiscordChatExporter` (`cd /path/to/DiscordChatExporter` on **MacOS** and **Linux**), then press ENTER to run the command.\n\n**Windows** users can quickly get the folder's path by clicking the address bar while inside the folder.\n![Copy path from Explorer](https://i.imgur.com/XncnhC2.gif)\n\n**macOS** users can press Command+Option+C (⌘⌥C) while inside the folder (or selecting it) to copy its path to the clipboard.\n\nYou can also drag and drop the folder on **every platform**.\n![Drag and drop folder](https://i.imgur.com/sOpZQAb.gif)\n\n## Step 3\n\nNow we're ready to run the commands.\n\nType the following command in your terminal of choice, then press ENTER to run it. This will list all available subcommands and options.\n\n```console\n./DiscordChatExporter.Cli\n```\n\n> **Note**:\n> On Windows, if you're using the default Command Prompt (`cmd`), omit the leading `./` at the start of the command.\n\n> **Docker** users, please refer to the [Docker usage instructions](Docker.md).\n\n## CLI commands\n\n| Command     | Description                                          |\n| ----------- | ---------------------------------------------------- |\n| export      | Exports a channel                                    |\n| exportdm    | Exports all direct message channels                  |\n| exportguild | Exports all channels within the specified server     |\n| exportall   | Exports all accessible channels                      |\n| channels    | Outputs the list of channels in the given server     |\n| dm          | Outputs the list of direct message channels          |\n| guilds      | Outputs the list of accessible servers               |\n| guide       | Explains how to obtain token, server, and channel ID |\n\nTo use the commands, you'll need a token. For the instructions on how to get a token, please refer to [this page](Token-and-IDs.md), or run `./DiscordChatExporter.Cli guide`.\n\nTo get help with a specific command, run:\n\n```console\n./DiscordChatExporter.Cli command --help\n```\n\nFor example, to figure out how to use the `export` command, run:\n\n```console\n./DiscordChatExporter.Cli export --help\n```\n\n## Export a specific channel\n\nYou can quickly export with DCE's default settings by using just `-t token` and `-c channelid`.\n\n```console\n./DiscordChatExporter.Cli export -t \"mfa.Ifrn\" -c 53555\n```\n\n#### Changing the format\n\nYou can change the export format to `HtmlDark`, `HtmlLight`, `PlainText` `Json` or `Csv` with `-f format`. The default\nformat is `HtmlDark`.\n\n```console\n./DiscordChatExporter.Cli export -t \"mfa.Ifrn\" -c 53555 -f Json\n```\n\n#### Changing the output filename\n\nYou can change the filename by using `-o name.ext`. e.g. for the `HTML` format:\n\n```console\n./DiscordChatExporter.Cli export -t \"mfa.Ifrn\" -c 53555 -o myserver.html\n```\n\n#### Changing the output directory\n\nYou can change the export directory by using `-o` and providing a path that ends with a slash or does not have a file\nextension.\nIf any of the folders in the path have a space in its name, escape them with quotes (\").\n\n```console\n./DiscordChatExporter.Cli export -t \"mfa.Ifrn\" -c 53555 -o \"C:\\Discord Exports\"\n```\n\n#### Changing the filename and output directory\n\nYou can change both the filename and export directory by using `-o directory\\name.ext`.\nNote that the filename must have an extension, otherwise it will be considered a directory name.\nIf any of the folders in the path have a space in its name, escape them with quotes (\").\n\n```console\n./DiscordChatExporter.Cli export -t \"mfa.Ifrn\" -c 53555 -o \"C:\\Discord Exports\\myserver.html\"\n```\n\n#### Generating the filename and output directory dynamically\n\nYou can use template tokens to generate the output file path based on the server and channel metadata.\n\n```console\n./DiscordChatExporter.Cli export -t \"mfa.Ifrn\" -c 53555 -o \"C:\\Discord Exports\\%G\\%T\\%C.html\"\n```\n\nAssuming you are exporting a channel named `\"my-channel\"` in the `\"Text channels\"` category from a server\ncalled `\"My server\"`, you will get the following output file\npath: `C:\\Discord Exports\\My server\\Text channels\\my-channel.html`\n\nHere is the full list of supported template tokens:\n\n- `%g` - server ID\n- `%G` - server name\n- `%t` - category ID\n- `%T` - category name\n- `%c` - channel ID\n- `%C` - channel name\n- `%p` - channel position\n- `%P` - category position\n- `%a` - the \"after\" date\n- `%b` - the \"before\" date\n- `%d` - the current date\n- `%%` - escapes `%`\n\n#### Partitioning\n\nYou can use partitioning to split files after a given number of messages or file size.\nFor example, a channel with 36 messages set to be partitioned every 10 messages will output 4 files.\n\n```console\n./DiscordChatExporter.Cli export -t \"mfa.Ifrn\" -c 53555 -p 10\n```\n\nA 45 MB channel set to be partitioned every 20 MB will output 3 files.\n\n```console\n./DiscordChatExporter.Cli export -t \"mfa.Ifrn\" -c 53555 -p 20mb\n```\n\n#### Downloading assets\n\nIf this option is set, the export will include additional files such as user avatars, attached files, images, etc.\nOnly files that are referenced by the export are downloaded, which means that, for example, user avatars will not be\ndownloaded when using the plain text (TXT) export format.\nA folder containing the assets will be created along with the exported chat. They must be kept together.\n\n```console\n./DiscordChatExporter.Cli export -t \"mfa.Ifrn\" -c 53555 --media\n```\n\n#### Reusing assets\n\nPreviously downloaded assets can be reused to skip redundant downloads as long as the chat is always exported to the\nsame folder. Using this option can speed up future exports. This option requires the `--media` option.\n\n```console\n./DiscordChatExporter.Cli export -t \"mfa.Ifrn\" -c 53555 --media --reuse-media\n```\n\n#### Changing the media directory\n\nBy default, the media directory is created alongside the exported chat. You can change this by using `--media-dir` and\nproviding a path that ends with a slash. All of the exported media will be stored in this directory.\n\n```console\n./DiscordChatExporter.Cli export -t \"mfa.Ifrn\" -c 53555 --media --media-dir \"C:\\Discord Media\"\n```\n\n#### Changing the date format\n\nYou can customize how dates are formatted in the exported files by using `--locale` and inserting one of Discord's\nlocales. The default locale is `en-US`.\n\n```console\n./DiscordChatExporter.Cli export -t \"mfa.Ifrn\" -c 53555 --locale \"de-DE\"\n```\n\n#### Date ranges\n\n**Messages sent before a date**\nUse `--before` to export messages sent before the provided date. E.g. messages sent before September 18th, 2019:\n\n```console\n./DiscordChatExporter.Cli export -t \"mfa.Ifrn\" -c 53555 --before 2019-09-18\n```\n\n**Messages sent after a date**\nUse `--after` to export messages sent after the provided date. E.g. messages sent after September 17th, 2019 11:34 PM:\n\n```console\n./DiscordChatExporter.Cli export -t \"mfa.Ifrn\" -c 53555 --after \"2019-09-17 23:34\"\n```\n\n**Messages sent in a date range**\nUse `--before` and `--after` to export messages sent during the provided date range. E.g. messages sent between\nSeptember 17th, 2019 11:34 PM and September 18th:\n\n```console\n./DiscordChatExporter.Cli export -t \"mfa.Ifrn\" -c 53555 --after \"2019-09-17 23:34\" --before \"2019-09-18\"\n```\n\nYou can try different formats like `17-SEP-2019 11:34 PM` or even refine your ranges down to\nmilliseconds `17-SEP-2019 23:45:30.6170`!\nDon't forget to quote (\") the date if it has spaces!\nMore info about .NET date\nformats [here](https://docs.microsoft.com/en-us/dotnet/standard/base-types/custom-date-and-time-format-strings).\n\n#### Filtering messages\n\nUse `--filter` to filter what messages are included in the export.\n\n```console\n./DiscordChatExporter.Cli export -t \"mfa.Ifrn\" -c 53555 --filter \"from:Tyrrrz has:image\"\n```\n\nDocumentation on message filter syntax can be found [here](https://github.com/Tyrrrz/DiscordChatExporter/blob/prime/.docs/Message-filters.md).\n\n### Export channels from a specific server\n\nTo export all channels in a specific server, use the `exportguild` command and provide the server ID through the `-g|--guild` option:\n\n```console\n./DiscordChatExporter.Cli exportguild -t \"mfa.Ifrn\" -g 21814\n```\n\n#### Including threads\n\nBy default, threads are not included in the export. You can change this behavior by using `--include-threads` and\nspecifying which threads should be included. It has possible values of `none`, `active`, or `all`, indicating which\nthreads should be included. To include both active and archived threads, use `--include-threads all`.\n\n```console\n./DiscordChatExporter.Cli exportguild -t \"mfa.Ifrn\" -g 21814 --include-threads all\n```\n\n#### Including voice channels\n\nBy default, voice channels are included in the export. You can change this behavior by using `--include-vc` and\nspecifying whether to include voice channels in the export. It has possible values of `true` or `false`, to exclude\nvoice channels, use `--include-vc false`.\n\n```console\n./DiscordChatExporter.Cli exportguild -t \"mfa.Ifrn\" -g 21814 --include-vc false\n```\n\n### Export all channels\n\nTo export all accessible channels, use the `exportall` command:\n\n```console\n./DiscordChatExporter.Cli exportall -t \"mfa.Ifrn\"\n```\n\n#### Excluding DMs\n\nTo exclude DMs, add the `--include-dm false` option.\n\n```console\n./DiscordChatExporter.Cli exportall -t \"mfa.Ifrn\" --include-dm false\n```\n\n### List channels in a server\n\nTo list the channels available in a specific server, use the `channels` command and provide the server ID through the `-g|--guild` option:\n\n```console\n./DiscordChatExporter.Cli channels -t \"mfa.Ifrn\" -g 21814\n```\n\n### List direct message channels\n\nTo list all DM channels accessible to the current account, use the `dm` command:\n\n```console\n./DiscordChatExporter.Cli dm -t \"mfa.Ifrn\"\n```\n\n### List servers\n\nTo list all servers accessible by the current account, use the `guilds` command:\n\n```console\n./DiscordChatExporter.Cli guilds -t \"mfa.Ifrn\" > C:\\path\\to\\output.txt\n```\n"
  },
  {
    "path": ".docs/Using-the-GUI.md",
    "content": "# Using the GUI\n\n## Video tutorial\n\n[![Video tutorial](https://i.ytimg.com/vi/jjtu0VQXV7I/hqdefault.jpg)](https://youtube.com/watch?v=jjtu0VQXV7I)\n\n> Video by [NoIntro Tutorials](https://youtube.com/channel/UCFezKSxdNKJe77-hYiuXu3Q).\n\n## Guide\n\n### Step 1\n\nAfter extracting the `.zip`, run `DiscordChatExporter.exe` **(Windows)**, or `DiscordChatExporter` **(Linux)**.\n\nIf you're using **macOS**, you'll need to manually grant permission for the app to run.  \nIf you skip these steps, the \"DiscordChatExporter is damaged and can’t be opened\" error will be shown.\n\n1. Open Terminal.app. You can search for it in Spotlight (press <kbd>⌘</kbd> + <kbd>Space</kbd> and type \"Terminal\").\n2. Paste the following into the terminal window:\n   ```bash\n   xattr -rd com.apple.quarantine\n   ```\n3. Hit <kbd>Space</kbd> once to add a space after the command\n4. Drag and drop DiscordChatExporter.app into the terminal window\n5. Press <kbd>Return</kbd> to run the command\n6. Open DiscordChatExporter.app normally\n\n> Apple requires apps to be notarized and signed in order to run on macOS without warnings, which in turn requires an Apple Developer membership ($99/year). This open-source project is distributed for free and without commercial intent.\n\n### Step 2\n\nPlease refer to the on-screen instructions to get your token, then paste your token in the upper text box and hit ENTER or click the arrow (→).\n\n> [!WARNING]\n> **Never share your token!**\n> A token gives full access to an account, treat it like a password.\n\n<img src=\"https://i.imgur.com/SuLQ5tZ.png\" height=\"400\"/>\n\n### Step 3\n\nDCE will display your Direct Messages and a sidebar with your server list. Select the channel you would like to export, then click the ![Screenshot](https://i.imgur.com/dnTOlDa.png) button to continue.\n\n> **Note**:\n> You can export multiple channels at once by holding `CTRL` or `SHIFT` while selecting.\n> You can also double-click a channel to export it without clicking the ![Screenshot](https://i.imgur.com/dnTOlDa.png) button.\n\n<img src=\"https://i.imgur.com/JHMFRh2.png\" height=\"400\"/>\n\n### Step 4\n\nIn this screen you can customize the following:\n\n- **Output path** - The folder where the exported chat(s) will be saved.\n\n- **Export format** - HTML (Dark), HTML (Light), TXT, CSV and JSON\n\n- **Date range (after/before)** (Optional) - If set, only messages sent in the provided date range will be exported. Only one value (either after or before) is required if you want to use this option.\n  > **Note**:\n  > Please note that the time defaults to **12:00 AM** (midnight/00:00). This means that if you choose to export between Sep 17th and Sep 18th, messages from Sep 18th won't be exported.\n\n- **Partition limit** (Optional) - Split output into partitions, each limited to this number of messages (e.g. 100) or file size (e.g. 10mb). For example, a channel with 36 messages set to be partitioned every 10 messages will output 4 files.\n\n- **Message Filter** (Optional) - Special notation for filtering the messages that get included in the export. See [Message filters](Message-filters.md) for more info.\n\n- **Format markdown** (Optional) - Disable markdown processing when exporting. You can use this to produce JSON or plain text exports without unwrapping mentions, custom emoji, and certain other special tokens.\n\n- **Download assets** (Optional) - If this option is set, the export will include additional files such as user avatars, attached files, images, etc. Only files that are referenced by the export are downloaded, which means that, for example, user avatars will not be downloaded when using the plain text (TXT) export format. A folder containing the assets will be created along with the exported chat. They must be kept together.\n\n- **Reuse assets** (Optional) - If this option is set, the export will reuse already downloaded assets to skip redundant requests. This option is only available when **Download assets** is enabled.\n\n- **Assets directory path** (Optional) - If this option is set, the export will use the specified directory to store assets from all exported channels in the same place.\n\n> **Note**:\n> You need to scroll down to see all available options.\n\n## Settings\n\n- **Auto-update** - Perform automatic updates on every launch.\nDefault: Enabled\n\n  > **Note**:\n  > Keep this option enabled to receive the latest features and bug fixes!\n\n- **Dark mode** - Use darker colors in the UI (User Interface).\nDefault: Disabled\n\n- **Persist token** - Persist last used token between sessions.\nDefault: Enabled\n\n- **Show threads** - Controls whether threads are shown in the channel list.\nDefault: none\n\n- **Locale** - Customize how dates are formatted in the exported files.\n\n- **Date format** - Customize how dates are formatted in the exported files in the settings menu ().\n\n- **Parallel limit** - The number of channels that will be exported at the same time.\nDefault: 1\n\n  > **Note**:\n  > Try to keep this number low so that your account doesn't get flagged.\n\n- **Normalize to UTC** - Convert all dates to UTC before exporting.\n\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug-report.yml",
    "content": "name: 🐛 Bug report\ndescription: Report broken functionality.\nlabels: [bug]\n\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        - Avoid generic or vague titles such as \"Something's not working\" or \"A couple of problems\" — be as descriptive as possible.\n        - Keep your issue focused on one single problem. If you have multiple bug reports, please create a separate issue for each of them.\n        - Issues should represent **complete and actionable** work items. If you are unsure about something or have a question, please start a [discussion](https://github.com/Tyrrrz/DiscordChatExporter/discussions/new) instead.\n        - Remember that **DiscordChatExporter** is an open-source project funded by the community. If you find it useful, **please consider [donating](https://tyrrrz.me/donate) to support its development**.\n\n        ___\n\n  - type: input\n    attributes:\n      label: Version\n      description: Which version of the application does this bug affect? Make sure you're not using an outdated version.\n      placeholder: v1.0.0\n    validations:\n      required: true\n\n  - type: dropdown\n    attributes:\n      label: Flavor\n      description: Which flavor(s) of the application does this bug affect?\n      multiple: true\n      options:\n        - GUI (Graphical User Interface)\n        - CLI (Command-Line Interface)\n    validations:\n      required: true\n\n  - type: input\n    attributes:\n      label: Platform\n      description: Which platform do you experience this bug on?\n      placeholder: Docker / Windows 11\n    validations:\n      required: true\n\n  - type: dropdown\n    attributes:\n      label: Export format\n      description: Which export format(s) do you experience this bug with, if applicable?\n      multiple: true\n      options:\n        - HTML\n        - TXT\n        - JSON\n        - CSV\n\n  - type: textarea\n    attributes:\n      label: Steps to reproduce\n      description: >\n        Minimum steps required to reproduce the bug, including prerequisites, export settings, or other relevant items.\n        The information provided in this field must be readily actionable, meaning that anyone should be able to reproduce the bug by following these steps.\n        If the bug depends on external factors (such as a specific server, channel, or message), then this field must include the server invite and the corresponding link.\n      placeholder: |\n        Server invite: https://discord.gg/...\n        Channel or message link: https://discord.com/channels/.../...\n\n        Export settings:\n        - ...\n\n        Application settings:\n        - ...\n\n        Steps:\n        - Step 1\n        - Step 2\n        - Step 3\n    validations:\n      required: true\n\n  - type: textarea\n    attributes:\n      label: Details\n      description: Clear and thorough explanation of the bug, including any additional information you may find relevant.\n      placeholder: |\n        - Expected behavior: ...\n        - Actual behavior: ...\n    validations:\n      required: true\n\n  - type: checkboxes\n    attributes:\n      label: Checklist\n      description: Quick list of checks to ensure that everything is in order.\n      options:\n        - label: I have looked through existing issues to make sure that this bug has not been reported before\n          required: true\n        - label: I have provided a descriptive title for this issue\n          required: true\n        - label: I have made sure that this bug is reproducible on the latest version of the application\n          required: true\n        - label: I have provided all the information needed to reproduce this bug as efficiently as possible\n          required: true\n        - label: I have sponsored this project\n          required: false\n        - label: I have not read any of the above and just checked all the boxes to submit the issue\n          required: false\n\n  - type: markdown\n    attributes:\n      value: |\n        If you are struggling to provide actionable reproduction steps, or if something else is preventing you from creating a complete bug report, please start a [discussion](https://github.com/Tyrrrz/DiscordChatExporter/discussions/new) instead.\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "content": "blank_issues_enabled: false\ncontact_links:\n  - name: ⚠ Feature request\n    url: https://github.com/Tyrrrz/.github/blob/prime/docs/project-status.md\n    about: Sorry, but this project is in maintenance mode and no longer accepts new feature requests.\n  - name: 📖 Documentation\n    url: https://github.com/Tyrrrz/DiscordChatExporter/blob/prime/.docs\n    about: Find usage guides and frequently asked questions.\n  - name: 🗨 Discussions\n    url: https://github.com/Tyrrrz/DiscordChatExporter/discussions/new\n    about: Ask and answer questions.\n  - name: 💬 Discord server\n    url: https://discord.gg/2SUWKFnHSm\n    about: Chat with the project community.\n"
  },
  {
    "path": ".github/dependabot.yml",
    "content": "version: 2\nupdates:\n  - package-ecosystem: github-actions\n    directory: \"/\"\n    schedule:\n      interval: monthly\n    labels:\n      - enhancement\n    groups:\n      actions:\n        patterns:\n          - \"*\"\n  - package-ecosystem: docker\n    directory: \"/\"\n    schedule:\n      interval: monthly\n    labels:\n      - enhancement\n    groups:\n      docker:\n        patterns:\n          - \"*\"\n  - package-ecosystem: nuget\n    directory: \"/\"\n    schedule:\n      interval: monthly\n    labels:\n      - enhancement\n    groups:\n      nuget:\n        patterns:\n          - \"*\"\n"
  },
  {
    "path": ".github/workflows/docker.yml",
    "content": "name: docker\n\non:\n  workflow_dispatch:\n  push:\n    branches:\n      - prime\n    tags:\n      - \"*\"\n  pull_request:\n    branches:\n      - prime\n\njobs:\n  # Outputs from this job aren't really used, but it's here to verify that the Dockerfile builds correctly\n  pack:\n    runs-on: ubuntu-latest\n    timeout-minutes: 10\n\n    permissions:\n      actions: write\n      contents: read\n\n    steps:\n      - name: Checkout\n        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n\n      - name: Install Docker Buildx\n        uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0\n\n      - name: Build image\n        run: >\n          docker buildx build .\n          --file DiscordChatExporter.Cli.dockerfile\n          --platform linux/amd64,linux/arm64\n          --build-arg VERSION=${{ github.ref_type == 'tag' && github.ref_name || format('999.9.9-ci-{0}', github.sha) }}\n          --output type=tar,dest=DiscordChatExporter.Cli.Docker.tar\n\n      - name: Upload image\n        uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0\n        with:\n          name: DiscordChatExporter.Cli.Docker\n          path: DiscordChatExporter.Cli.Docker.tar\n          if-no-files-found: error\n\n  deploy:\n    # Deploy to DockerHub only on tag push or prime branch push\n    if: ${{ github.ref_type == 'tag' || github.ref_type == 'branch' && github.ref_name == 'prime' }}\n\n    runs-on: ubuntu-latest\n    timeout-minutes: 10\n\n    permissions:\n      contents: read\n\n    steps:\n      - name: Checkout\n        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n\n      - name: Install Docker Buildx\n        uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0\n\n      - name: Login to DockerHub\n        run: >\n          echo ${{ secrets.DOCKER_TOKEN }} |\n          docker login --username tyrrrz --password-stdin\n\n      - name: Build & push image\n        run: >\n          docker buildx build .\n          --file DiscordChatExporter.Cli.dockerfile\n          --platform linux/amd64,linux/arm64\n          --build-arg VERSION=${{ github.ref_type == 'tag' && github.ref_name || format('999.9.9-ci-{0}', github.sha) }}\n          --push\n          --tag tyrrrz/discordchatexporter:latest\n          ${{ github.ref_type == 'tag' && '--tag tyrrrz/discordchatexporter:$GITHUB_REF_NAME' || '' }}\n          ${{ github.ref_type == 'tag' && '--tag tyrrrz/discordchatexporter:stable' || '' }}\n"
  },
  {
    "path": ".github/workflows/main.yml",
    "content": "name: main\n\non:\n  workflow_dispatch:\n  push:\n    branches:\n      - prime\n    tags:\n      - \"*\"\n  pull_request:\n    branches:\n      - prime\n\nenv:\n  DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true\n  DOTNET_NOLOGO: true\n  DOTNET_CLI_TELEMETRY_OPTOUT: true\n\njobs:\n  format:\n    runs-on: ubuntu-latest\n    timeout-minutes: 10\n\n    permissions:\n      contents: read\n\n    steps:\n      - name: Checkout\n        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n\n      - name: Install .NET\n        uses: actions/setup-dotnet@baa11fbfe1d6520db94683bd5c7a3818018e4309 # v5.1.0\n\n      # Build the project separately to discern between build and format errors\n      - name: Build\n        run: >\n          dotnet build\n          -p:CSharpier_Bypass=true\n          --configuration Release\n\n      - name: Verify formatting\n        id: verify\n        run: >\n          dotnet build\n          -t:CSharpierFormat\n          --configuration Release\n          --no-restore\n\n      - name: Report issues\n        if: ${{ failure() && steps.verify.outcome == 'failure' }}\n        run: echo \"::error title=Bad formatting::Formatting issues detected. Please build the solution locally to fix them.\"\n\n  test:\n    # Tests need access to secrets, so we can't run them against PRs because of limited trust\n    if: ${{ github.event_name != 'pull_request' }}\n\n    runs-on: ubuntu-latest\n    timeout-minutes: 10\n\n    permissions:\n      contents: read\n\n    steps:\n      - name: Checkout\n        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n\n      - name: Install .NET\n        uses: actions/setup-dotnet@baa11fbfe1d6520db94683bd5c7a3818018e4309 # v5.1.0\n\n      - name: Run tests\n        env:\n          DISCORD_TOKEN: ${{ secrets.DISCORD_TOKEN }}\n        run: >\n          dotnet test\n          -p:CSharpier_Bypass=true\n          --configuration Release\n          --logger \"GitHubActions;summary.includePassedTests=true;summary.includeSkippedTests=true\"\n          --collect:\"XPlat Code Coverage\"\n          --\n          RunConfiguration.CollectSourceInformation=true\n          DataCollectionRunSettings.DataCollectors.DataCollector.Configuration.Format=opencover\n\n      - name: Upload coverage\n        uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2\n        with:\n          token: ${{ secrets.CODECOV_TOKEN }}\n\n  pack:\n    strategy:\n      matrix:\n        app:\n          - DiscordChatExporter.Cli\n          - DiscordChatExporter.Gui\n        rid:\n          - win-arm64\n          - win-x86\n          - win-x64\n          - linux-arm\n          - linux-arm64\n          - linux-musl-x64\n          - linux-x64\n          - osx-arm64\n          - osx-x64\n        include:\n          - app: DiscordChatExporter.Cli\n            asset: DiscordChatExporter.Cli\n          - app: DiscordChatExporter.Gui\n            # GUI assets aren't suffixed, unlike the CLI assets\n            asset: DiscordChatExporter\n\n    runs-on: ${{ startsWith(matrix.rid, 'win-') && 'windows-latest' || startsWith(matrix.rid, 'osx-') && 'macos-latest' || 'ubuntu-latest' }}\n    timeout-minutes: 10\n\n    permissions:\n      actions: write\n      contents: read\n\n    steps:\n      - name: Checkout\n        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n\n      - name: Install .NET\n        uses: actions/setup-dotnet@baa11fbfe1d6520db94683bd5c7a3818018e4309 # v5.1.0\n\n      - name: Publish app\n        run: >\n          dotnet publish ${{ matrix.app }}\n          -p:Version=${{ github.ref_type == 'tag' && github.ref_name || format('999.9.9-ci-{0}', github.sha) }}\n          -p:CSharpier_Bypass=true\n          -p:EncryptionSalt=${{ secrets.ENCRYPTION_SALT || 'HimalayanPinkSalt' }}\n          -p:PublishMacOSBundle=${{ startsWith(matrix.rid, 'osx-') }}\n          --output ${{ matrix.app }}/bin/publish/\n          --configuration Release\n          --runtime ${{ matrix.rid }}\n          --self-contained\n\n      - name: Upload app binaries\n        uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0\n        with:\n          name: ${{ matrix.asset }}.${{ matrix.rid }}\n          path: ${{ matrix.app }}/bin/publish/\n          if-no-files-found: error\n\n  release:\n    if: ${{ github.ref_type == 'tag' }}\n\n    needs:\n      - format\n      - test\n      - pack\n\n    runs-on: ubuntu-latest\n    timeout-minutes: 10\n\n    permissions:\n      contents: write\n\n    steps:\n      - name: Create release\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n        run: >\n          gh release create ${{ github.ref_name }}\n          --repo ${{ github.event.repository.full_name }}\n          --title ${{ github.ref_name }}\n          --generate-notes\n          --verify-tag\n\n  deploy:\n    needs: release\n\n    strategy:\n      matrix:\n        app:\n          - DiscordChatExporter.Cli\n          - DiscordChatExporter.Gui\n        rid:\n          - win-arm64\n          - win-x86\n          - win-x64\n          - linux-arm\n          - linux-arm64\n          - linux-musl-x64\n          - linux-x64\n          - osx-arm64\n          - osx-x64\n        include:\n          - app: DiscordChatExporter.Cli\n            asset: DiscordChatExporter.Cli\n          - app: DiscordChatExporter.Gui\n            # GUI assets aren't suffixed, unlike the CLI assets\n            asset: DiscordChatExporter\n\n    runs-on: ubuntu-latest\n    timeout-minutes: 10\n\n    permissions:\n      actions: read\n      contents: write\n\n    steps:\n      - name: Download app binaries\n        uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0\n        with:\n          name: ${{ matrix.asset }}.${{ matrix.rid }}\n          path: ${{ matrix.app }}/\n\n      - name: Set permissions\n        if: ${{ !startsWith(matrix.rid, 'win-') }}\n        run: |\n          [ -f ${{ matrix.app }}/${{ matrix.asset }} ] && chmod +x ${{ matrix.app }}/${{ matrix.asset }} || true\n\n          # macOS bundle\n          [ -f ${{ matrix.app }}/${{ matrix.asset }}.app/Contents/MacOS/${{ matrix.asset }} ] && chmod +x ${{ matrix.app }}/${{ matrix.asset }}.app/Contents/MacOS/${{ matrix.asset }} || true\n\n      - name: Create package\n        # Change into the artifacts directory to avoid including the directory itself in the zip archive\n        working-directory: ${{ matrix.app }}/\n        run: zip -r ../${{ matrix.asset }}.${{ matrix.rid }}.zip .\n\n      - name: Upload release asset\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n        run: >\n          gh release upload ${{ github.ref_name }}\n          ${{ matrix.asset }}.${{ matrix.rid }}.zip\n          --repo ${{ github.event.repository.full_name }}\n\n  notify:\n    needs: deploy\n    runs-on: ubuntu-latest\n    timeout-minutes: 10\n\n    permissions:\n      contents: read\n\n    steps:\n      - name: Notify Discord\n        uses: tyrrrz/action-http-request@1dd7ad841a34b9299f3741f7c7399f9feefdfb08 # 1.1.3\n        with:\n          url: ${{ secrets.DISCORD_WEBHOOK }}\n          method: POST\n          headers: |\n            Content-Type: application/json; charset=UTF-8\n          body: |\n            {\n              \"avatar_url\": \"https://raw.githubusercontent.com/${{ github.event.repository.full_name }}/${{ github.ref_name }}/favicon.png\",\n              \"content\": \"[**${{ github.event.repository.name }}**](<${{ github.event.repository.html_url }}>) v${{ github.ref_name }} has been released!\"\n            }\n          retry-count: 5\n"
  },
  {
    "path": ".gitignore",
    "content": "# User-specific files\n.vs/\n.idea/\n*.suo\n*.user\n\n# Build results\nbin/\nobj/\n\n# Test results\nTestResults/\n"
  },
  {
    "path": "Directory.Build.props",
    "content": "<Project>\n\n  <PropertyGroup>\n    <TargetFramework>net10.0</TargetFramework>\n    <Version>999.9.9-dev</Version>\n    <Company>Tyrrrz</Company>\n    <Copyright>Copyright (c) Oleksii Holub</Copyright>\n    <LangVersion>preview</LangVersion>\n    <Nullable>enable</Nullable>\n    <TreatWarningsAsErrors>true</TreatWarningsAsErrors>\n    <ILLinkTreatWarningsAsErrors>false</ILLinkTreatWarningsAsErrors>\n  </PropertyGroup>\n\n</Project>"
  },
  {
    "path": "Directory.Packages.props",
    "content": "<Project>\n  <PropertyGroup>\n    <ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <PackageVersion Include=\"AngleSharp\" Version=\"1.4.0\" />\n    <PackageVersion Include=\"AsyncImageLoader.Avalonia\" Version=\"3.6.0\" />\n    <PackageVersion Include=\"AsyncKeyedLock\" Version=\"8.0.2\" />\n    <PackageVersion Include=\"Avalonia\" Version=\"11.3.12\" />\n    <PackageVersion Include=\"Avalonia.Desktop\" Version=\"11.3.12\" />\n    <PackageVersion Include=\"Avalonia.Diagnostics\" Version=\"11.3.12\" />\n    <PackageVersion Include=\"CliFx\" Version=\"2.3.6\" />\n    <PackageVersion Include=\"Cogwheel\" Version=\"2.1.0\" />\n    <PackageVersion Include=\"CommunityToolkit.Mvvm\" Version=\"8.4.0\" />\n    <PackageVersion Include=\"coverlet.collector\" Version=\"8.0.0\" />\n    <PackageVersion Include=\"CSharpier.MsBuild\" Version=\"1.2.6\" />\n    <PackageVersion Include=\"Deorcify\" Version=\"1.1.0\" />\n    <PackageVersion Include=\"ThisAssembly.Project\" Version=\"2.1.2\" />\n    <PackageVersion Include=\"DialogHost.Avalonia\" Version=\"0.10.4\" />\n    <PackageVersion Include=\"FluentAssertions\" Version=\"8.8.0\" />\n    <PackageVersion Include=\"GitHubActionsTestLogger\" Version=\"3.0.1\" />\n    <PackageVersion Include=\"Gress\" Version=\"2.1.1\" />\n    <PackageVersion Include=\"JsonExtensions\" Version=\"1.2.0\" />\n    <PackageVersion Include=\"Markdig\" Version=\"1.0.1\" />\n    <PackageVersion Include=\"Material.Avalonia\" Version=\"3.9.2\" />\n    <PackageVersion Include=\"Material.Icons.Avalonia\" Version=\"2.2.0\" />\n    <PackageVersion Include=\"Microsoft.Extensions.Configuration\" Version=\"10.0.3\" />\n    <PackageVersion Include=\"Microsoft.Extensions.Configuration.EnvironmentVariables\" Version=\"10.0.3\" />\n    <PackageVersion Include=\"Microsoft.Extensions.Configuration.UserSecrets\" Version=\"10.0.3\" />\n    <PackageVersion Include=\"Microsoft.Extensions.DependencyInjection\" Version=\"10.0.3\" />\n    <PackageVersion Include=\"Microsoft.NET.Test.Sdk\" Version=\"18.3.0\" />\n    <PackageVersion Include=\"Onova\" Version=\"2.6.13\" />\n    <PackageVersion Include=\"Polly\" Version=\"8.6.5\" />\n    <PackageVersion Include=\"RazorBlade\" Version=\"0.11.0\" />\n    <PackageVersion Include=\"Spectre.Console\" Version=\"0.54.0\" />\n    <PackageVersion Include=\"Superpower\" Version=\"3.1.0\" />\n    <PackageVersion Include=\"WebMarkupMin.Core\" Version=\"2.20.2\" />\n    <PackageVersion Include=\"xunit\" Version=\"2.9.3\" />\n    <PackageVersion Include=\"xunit.runner.visualstudio\" Version=\"3.1.5\" />\n    <PackageVersion Include=\"YoutubeExplode\" Version=\"6.5.7\" />\n  </ItemGroup>\n</Project>\n"
  },
  {
    "path": "DiscordChatExporter.Cli/Commands/Base/DiscordCommandBase.cs",
    "content": "﻿using System;\nusing System.Diagnostics.CodeAnalysis;\nusing System.Threading.Tasks;\nusing CliFx;\nusing CliFx.Attributes;\nusing CliFx.Infrastructure;\nusing DiscordChatExporter.Core.Discord;\nusing DiscordChatExporter.Core.Utils;\n\nnamespace DiscordChatExporter.Cli.Commands.Base;\n\npublic abstract class DiscordCommandBase : ICommand\n{\n    [CommandOption(\n        \"token\",\n        't',\n        EnvironmentVariable = \"DISCORD_TOKEN\",\n        Description = \"Authentication token.\"\n    )]\n    public required string Token { get; init; }\n\n    [Obsolete(\"This option doesn't do anything. Kept for backwards compatibility.\")]\n    [CommandOption(\n        \"bot\",\n        'b',\n        EnvironmentVariable = \"DISCORD_TOKEN_BOT\",\n        Description = \"This option doesn't do anything. Kept for backwards compatibility.\"\n    )]\n    public bool IsBotToken { get; init; } = false;\n\n    [CommandOption(\n        \"respect-rate-limits\",\n        Description = \"Whether to respect advisory rate limits. \"\n            + \"If disabled, only hard rate limits (i.e. 429 responses) will be respected.\"\n    )]\n    public bool ShouldRespectRateLimits { get; init; } = true;\n\n    [field: AllowNull, MaybeNull]\n    protected DiscordClient Discord =>\n        field ??= new DiscordClient(\n            Token,\n            ShouldRespectRateLimits ? RateLimitPreference.RespectAll : RateLimitPreference.IgnoreAll\n        );\n\n    public virtual ValueTask ExecuteAsync(IConsole console)\n    {\n#pragma warning disable CS0618\n        // Warn if the bot option is used\n        if (IsBotToken)\n        {\n            using (console.WithForegroundColor(ConsoleColor.DarkYellow))\n            {\n                console.Error.WriteLine(\n                    \"Warning: The --bot option is deprecated and should not be used. \"\n                        + \"The token type is now inferred automatically. \"\n                        + \"Please update your workflows as this option may be completely removed in a future version.\"\n                );\n            }\n        }\n#pragma warning restore CS0618\n\n        // Note about interactivity for Docker\n        if (console.IsOutputRedirected && Docker.IsRunningInContainer)\n        {\n            console.Error.WriteLine(\n                \"Note: Output streams are redirected, rich console interactions are disabled. \"\n                    + \"If you are running this command in Docker, consider allocating a pseudo-terminal for better user experience (docker run -it ...).\"\n            );\n        }\n\n        return default;\n    }\n}\n"
  },
  {
    "path": "DiscordChatExporter.Cli/Commands/Base/ExportCommandBase.cs",
    "content": "﻿using System;\nusing System.Collections.Concurrent;\nusing System.Collections.Generic;\nusing System.Diagnostics.CodeAnalysis;\nusing System.IO;\nusing System.Linq;\nusing System.Threading.Tasks;\nusing CliFx.Attributes;\nusing CliFx.Exceptions;\nusing CliFx.Infrastructure;\nusing DiscordChatExporter.Cli.Commands.Converters;\nusing DiscordChatExporter.Cli.Commands.Shared;\nusing DiscordChatExporter.Cli.Utils.Extensions;\nusing DiscordChatExporter.Core.Discord;\nusing DiscordChatExporter.Core.Discord.Data;\nusing DiscordChatExporter.Core.Exceptions;\nusing DiscordChatExporter.Core.Exporting;\nusing DiscordChatExporter.Core.Exporting.Filtering;\nusing DiscordChatExporter.Core.Exporting.Partitioning;\nusing Gress;\nusing Spectre.Console;\n\nnamespace DiscordChatExporter.Cli.Commands.Base;\n\npublic abstract class ExportCommandBase : DiscordCommandBase\n{\n    [CommandOption(\n        \"output\",\n        'o',\n        Description = \"Output file or directory path. \"\n            + \"If a directory is specified, file names will be generated automatically based on the channel names and export parameters. \"\n            + \"Directory paths must end with a slash to avoid ambiguity. \"\n            + \"Supports template tokens, see the documentation for more info.\"\n    )]\n    public string OutputPath\n    {\n        get;\n        // Handle ~/ in paths on Unix systems\n        // https://github.com/Tyrrrz/DiscordChatExporter/pull/903\n        init => field = Path.GetFullPath(value);\n    } = Directory.GetCurrentDirectory();\n\n    [CommandOption(\"format\", 'f', Description = \"Export format.\")]\n    public ExportFormat ExportFormat { get; init; } = ExportFormat.HtmlDark;\n\n    [CommandOption(\n        \"after\",\n        Description = \"Only include messages sent after this date or message ID.\"\n    )]\n    public Snowflake? After { get; init; }\n\n    [CommandOption(\n        \"before\",\n        Description = \"Only include messages sent before this date or message ID.\"\n    )]\n    public Snowflake? Before { get; init; }\n\n    [CommandOption(\n        \"partition\",\n        'p',\n        Description = \"Split the output into partitions, each limited to the specified \"\n            + \"number of messages (e.g. '100') or file size (e.g. '10mb').\"\n    )]\n    public PartitionLimit PartitionLimit { get; init; } = PartitionLimit.Null;\n\n    [CommandOption(\n        \"include-threads\",\n        Description = \"Which types of threads should be included.\",\n        Converter = typeof(ThreadInclusionModeBindingConverter)\n    )]\n    public ThreadInclusionMode ThreadInclusionMode { get; init; } = ThreadInclusionMode.None;\n\n    [CommandOption(\n        \"filter\",\n        Description = \"Only include messages that satisfy this filter. \"\n            + \"See the documentation for more info.\"\n    )]\n    public MessageFilter MessageFilter { get; init; } = MessageFilter.Null;\n\n    [CommandOption(\n        \"parallel\",\n        Description = \"Limits how many channels can be exported in parallel.\"\n    )]\n    public int ParallelLimit { get; init; } = 1;\n\n    [CommandOption(\n        \"reverse\",\n        Description = \"Export messages in reverse chronological order (newest first).\"\n    )]\n    public bool IsReverseMessageOrder { get; init; }\n\n    [CommandOption(\n        \"markdown\",\n        Description = \"Process markdown, mentions, and other special tokens.\"\n    )]\n    public bool ShouldFormatMarkdown { get; init; } = true;\n\n    [CommandOption(\n        \"media\",\n        Description = \"Download assets referenced by the export (user avatars, attached files, embedded images, etc.).\"\n    )]\n    public bool ShouldDownloadAssets { get; init; }\n\n    [CommandOption(\n        \"reuse-media\",\n        Description = \"Reuse previously downloaded assets to avoid redundant requests.\"\n    )]\n    public bool ShouldReuseAssets { get; init; } = false;\n\n    [CommandOption(\n        \"media-dir\",\n        Description = \"Download assets to this directory. \"\n            + \"If not specified, the asset directory path will be derived from the output path.\"\n    )]\n    public string? AssetsDirPath\n    {\n        get;\n        // Handle ~/ in paths on Unix systems\n        // https://github.com/Tyrrrz/DiscordChatExporter/pull/903\n        init => field = value is not null ? Path.GetFullPath(value) : null;\n    }\n\n    [Obsolete(\"This option doesn't do anything. Kept for backwards compatibility.\")]\n    [CommandOption(\n        \"dateformat\",\n        Description = \"This option doesn't do anything. Kept for backwards compatibility.\"\n    )]\n    public string DateFormat { get; init; } = \"MM/dd/yyyy h:mm tt\";\n\n    [CommandOption(\n        \"locale\",\n        Description = \"Locale to use when formatting dates and numbers. \"\n            + \"If not specified, the default system locale will be used.\"\n    )]\n    public string? Locale { get; init; }\n\n    [CommandOption(\"utc\", Description = \"Normalize all timestamps to UTC+0.\")]\n    public bool IsUtcNormalizationEnabled { get; init; } = false;\n\n    [CommandOption(\n        \"fuck-russia\",\n        EnvironmentVariable = \"FUCK_RUSSIA\",\n        Description = \"Don't print the Support Ukraine message to the console.\",\n        // Use a converter to accept '1' as 'true' to reuse the existing environment variable\n        Converter = typeof(TruthyBooleanBindingConverter)\n    )]\n    public bool IsUkraineSupportMessageDisabled { get; init; } = false;\n\n    [field: AllowNull, MaybeNull]\n    protected ChannelExporter Exporter => field ??= new ChannelExporter(Discord);\n\n    protected async ValueTask ExportAsync(IConsole console, IReadOnlyList<Channel> channels)\n    {\n        var cancellationToken = console.RegisterCancellationHandler();\n\n        // Asset reuse can only be enabled if the download assets option is set\n        // https://github.com/Tyrrrz/DiscordChatExporter/issues/425\n        if (ShouldReuseAssets && !ShouldDownloadAssets)\n        {\n            throw new CommandException(\"Option --reuse-media cannot be used without --media.\");\n        }\n\n        // Assets directory can only be specified if the download assets option is set\n        if (!string.IsNullOrWhiteSpace(AssetsDirPath) && !ShouldDownloadAssets)\n        {\n            throw new CommandException(\"Option --media-dir cannot be used without --media.\");\n        }\n\n        var unwrappedChannels = new List<Channel>(channels);\n\n        // Unwrap threads\n        if (ThreadInclusionMode != ThreadInclusionMode.None)\n        {\n            await console.Output.WriteLineAsync(\"Fetching threads...\");\n\n            var fetchedThreadsCount = 0;\n            await console\n                .CreateStatusTicker()\n                .StartAsync(\n                    \"...\",\n                    async ctx =>\n                    {\n                        await foreach (\n                            var thread in Discord.GetChannelThreadsAsync(\n                                channels,\n                                ThreadInclusionMode == ThreadInclusionMode.All,\n                                Before,\n                                After,\n                                cancellationToken\n                            )\n                        )\n                        {\n                            unwrappedChannels.Add(thread);\n\n                            ctx.Status(Markup.Escape($\"Fetched '{thread.GetHierarchicalName()}'.\"));\n\n                            fetchedThreadsCount++;\n                        }\n                    }\n                );\n\n            // Remove forums, as they cannot be exported directly and their constituent threads\n            // have already been fetched.\n            unwrappedChannels.RemoveAll(channel => channel.Kind == ChannelKind.GuildForum);\n\n            await console.Output.WriteLineAsync($\"Fetched {fetchedThreadsCount} thread(s).\");\n        }\n\n        // Make sure the user does not try to export multiple channels into one file.\n        // Output path must either be a directory or contain template tokens for this to work.\n        // https://github.com/Tyrrrz/DiscordChatExporter/issues/799\n        // https://github.com/Tyrrrz/DiscordChatExporter/issues/917\n        var isValidOutputPath =\n            // Anything is valid when exporting a single channel\n            unwrappedChannels.Count <= 1\n            // When using template tokens, assume the user knows what they're doing\n            || OutputPath.Contains('%')\n            // Otherwise, require an existing directory or an unambiguous directory path\n            || Directory.Exists(OutputPath)\n            || Path.EndsInDirectorySeparator(OutputPath);\n\n        if (!isValidOutputPath)\n        {\n            throw new CommandException(\n                \"Attempted to export multiple channels, but the output path is neither a directory nor a template. \"\n                    + \"If the provided output path is meant to be treated as a directory, make sure it ends with a slash. \"\n                    + $\"Provided output path: '{OutputPath}'.\"\n            );\n        }\n\n        // Export\n        var errorsByChannel = new ConcurrentDictionary<Channel, string>();\n        var warningsByChannel = new ConcurrentDictionary<Channel, string>();\n\n        await console.Output.WriteLineAsync($\"Exporting {unwrappedChannels.Count} channel(s)...\");\n        await console\n            .CreateProgressTicker()\n            .HideCompleted(\n                // When exporting multiple channels in parallel, hide the completed tasks\n                // because it gets hard to visually parse them as they complete out of order.\n                // https://github.com/Tyrrrz/DiscordChatExporter/issues/1124\n                ParallelLimit > 1\n            )\n            .StartAsync(async ctx =>\n            {\n                await Parallel.ForEachAsync(\n                    unwrappedChannels,\n                    new ParallelOptions\n                    {\n                        MaxDegreeOfParallelism = Math.Max(1, ParallelLimit),\n                        CancellationToken = cancellationToken,\n                    },\n                    async (channel, innerCancellationToken) =>\n                    {\n                        try\n                        {\n                            await ctx.StartTaskAsync(\n                                Markup.Escape(channel.GetHierarchicalName()),\n                                async progress =>\n                                {\n                                    var guild = await Discord.GetGuildAsync(\n                                        channel.GuildId,\n                                        innerCancellationToken\n                                    );\n\n                                    var request = new ExportRequest(\n                                        guild,\n                                        channel,\n                                        OutputPath,\n                                        AssetsDirPath,\n                                        ExportFormat,\n                                        After,\n                                        Before,\n                                        PartitionLimit,\n                                        MessageFilter,\n                                        IsReverseMessageOrder,\n                                        ShouldFormatMarkdown,\n                                        ShouldDownloadAssets,\n                                        ShouldReuseAssets,\n                                        Locale,\n                                        IsUtcNormalizationEnabled\n                                    );\n\n                                    await Exporter.ExportChannelAsync(\n                                        request,\n                                        progress.ToPercentageBased(),\n                                        innerCancellationToken\n                                    );\n                                }\n                            );\n                        }\n                        catch (ChannelEmptyException ex)\n                        {\n                            warningsByChannel[channel] = ex.Message;\n                        }\n                        catch (DiscordChatExporterException ex) when (!ex.IsFatal)\n                        {\n                            errorsByChannel[channel] = ex.Message;\n                        }\n                    }\n                );\n            });\n\n        // Print the result\n        using (console.WithForegroundColor(ConsoleColor.White))\n        {\n            await console.Output.WriteLineAsync(\n                $\"Successfully exported {unwrappedChannels.Count - errorsByChannel.Count} channel(s).\"\n            );\n        }\n\n        // Print warnings\n        if (warningsByChannel.Any())\n        {\n            await console.Output.WriteLineAsync();\n\n            using (console.WithForegroundColor(ConsoleColor.Yellow))\n            {\n                await console.Error.WriteLineAsync(\n                    \"Warnings reported for the following channel(s):\"\n                );\n            }\n\n            foreach (var (channel, message) in warningsByChannel)\n            {\n                await console.Error.WriteAsync($\"{channel.GetHierarchicalName()}: \");\n                using (console.WithForegroundColor(ConsoleColor.Yellow))\n                    await console.Error.WriteLineAsync(message);\n            }\n\n            await console.Error.WriteLineAsync();\n        }\n\n        // Print errors\n        if (errorsByChannel.Any())\n        {\n            await console.Output.WriteLineAsync();\n\n            using (console.WithForegroundColor(ConsoleColor.Red))\n            {\n                await console.Error.WriteLineAsync(\"Failed to export the following channel(s):\");\n            }\n\n            foreach (var (channel, message) in errorsByChannel)\n            {\n                await console.Error.WriteAsync($\"{channel.GetHierarchicalName()}: \");\n                using (console.WithForegroundColor(ConsoleColor.Red))\n                    await console.Error.WriteLineAsync(message);\n            }\n\n            await console.Error.WriteLineAsync();\n        }\n\n        // Fail the command only if ALL channels failed to export.\n        // If only some channels failed to export, it's okay.\n        if (errorsByChannel.Count >= unwrappedChannels.Count)\n            throw new CommandException(\"Export failed.\");\n    }\n\n    public override async ValueTask ExecuteAsync(IConsole console)\n    {\n        // Support Ukraine callout\n        if (!IsUkraineSupportMessageDisabled)\n        {\n            console.Output.WriteLine(\n                \"┌────────────────────────────────────────────────────────────────────┐\"\n            );\n            console.Output.WriteLine(\n                \"│   Thank you for supporting Ukraine <3                              │\"\n            );\n            console.Output.WriteLine(\n                \"│                                                                    │\"\n            );\n            console.Output.WriteLine(\n                \"│   As Russia wages a genocidal war against my country,              │\"\n            );\n            console.Output.WriteLine(\n                \"│   I'm grateful to everyone who continues to                        │\"\n            );\n            console.Output.WriteLine(\n                \"│   stand with Ukraine in our fight for freedom.                     │\"\n            );\n            console.Output.WriteLine(\n                \"│                                                                    │\"\n            );\n            console.Output.WriteLine(\n                \"│   Learn more: https://tyrrrz.me/ukraine                            │\"\n            );\n            console.Output.WriteLine(\n                \"└────────────────────────────────────────────────────────────────────┘\"\n            );\n            console.Output.WriteLine(\"\");\n        }\n\n        await base.ExecuteAsync(console);\n    }\n}\n"
  },
  {
    "path": "DiscordChatExporter.Cli/Commands/Converters/ThreadInclusionModeBindingConverter.cs",
    "content": "﻿using System;\nusing CliFx.Extensibility;\nusing DiscordChatExporter.Cli.Commands.Shared;\n\nnamespace DiscordChatExporter.Cli.Commands.Converters;\n\ninternal class ThreadInclusionModeBindingConverter : BindingConverter<ThreadInclusionMode>\n{\n    public override ThreadInclusionMode Convert(string? rawValue)\n    {\n        // Empty or unset value is treated as 'active' to match the previous behavior\n        if (string.IsNullOrWhiteSpace(rawValue))\n            return ThreadInclusionMode.Active;\n\n        // Boolean 'true' is treated as 'active', boolean 'false' is treated as 'none'\n        if (bool.TryParse(rawValue, out var boolValue))\n            return boolValue ? ThreadInclusionMode.Active : ThreadInclusionMode.None;\n\n        // Otherwise, fall back to regular enum parsing\n        return Enum.Parse<ThreadInclusionMode>(rawValue, true);\n    }\n}\n"
  },
  {
    "path": "DiscordChatExporter.Cli/Commands/Converters/TruthyBooleanBindingConverter.cs",
    "content": "﻿using System.Globalization;\nusing CliFx.Extensibility;\n\nnamespace DiscordChatExporter.Cli.Commands.Converters;\n\ninternal class TruthyBooleanBindingConverter : BindingConverter<bool>\n{\n    public override bool Convert(string? rawValue)\n    {\n        // Empty or unset value is treated as 'true', to match the regular boolean behavior\n        if (string.IsNullOrWhiteSpace(rawValue))\n            return true;\n\n        // Number '1' is treated as 'true', other numbers are treated as 'false'\n        if (int.TryParse(rawValue, CultureInfo.InvariantCulture, out var intValue))\n            return intValue == 1;\n\n        // Otherwise, fall back to regular boolean parsing\n        return bool.Parse(rawValue);\n    }\n}\n"
  },
  {
    "path": "DiscordChatExporter.Cli/Commands/ExportAllCommand.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Threading.Tasks;\nusing CliFx.Attributes;\nusing CliFx.Infrastructure;\nusing DiscordChatExporter.Cli.Commands.Base;\nusing DiscordChatExporter.Cli.Utils.Extensions;\nusing DiscordChatExporter.Core.Discord.Data;\nusing DiscordChatExporter.Core.Discord.Dump;\nusing DiscordChatExporter.Core.Exceptions;\nusing Spectre.Console;\n\nnamespace DiscordChatExporter.Cli.Commands;\n\n[Command(\"exportall\", Description = \"Exports all accessible channels.\")]\npublic class ExportAllCommand : ExportCommandBase\n{\n    [CommandOption(\"include-dm\", Description = \"Include direct message channels.\")]\n    public bool IncludeDirectChannels { get; init; } = true;\n\n    [CommandOption(\"include-guilds\", Description = \"Include server channels.\")]\n    public bool IncludeGuildChannels { get; init; } = true;\n\n    [CommandOption(\"include-vc\", Description = \"Include voice channels.\")]\n    public bool IncludeVoiceChannels { get; init; } = true;\n\n    [CommandOption(\n        \"data-package\",\n        Description = \"Path to the personal data package (ZIP file) requested from Discord. \"\n            + \"If provided, only channels referenced in the dump will be exported.\"\n    )]\n    public string? DataPackageFilePath { get; init; }\n\n    public override async ValueTask ExecuteAsync(IConsole console)\n    {\n        await base.ExecuteAsync(console);\n\n        var cancellationToken = console.RegisterCancellationHandler();\n        var channels = new List<Channel>();\n\n        // Pull from the API\n        if (string.IsNullOrWhiteSpace(DataPackageFilePath))\n        {\n            await foreach (var guild in Discord.GetUserGuildsAsync(cancellationToken))\n            {\n                // Regular channels\n                await console.Output.WriteLineAsync(\n                    $\"Fetching channels for server '{guild.Name}'...\"\n                );\n\n                var fetchedChannelsCount = 0;\n                await console\n                    .CreateStatusTicker()\n                    .StartAsync(\n                        \"...\",\n                        async ctx =>\n                        {\n                            await foreach (\n                                var channel in Discord.GetGuildChannelsAsync(\n                                    guild.Id,\n                                    cancellationToken\n                                )\n                            )\n                            {\n                                if (channel.IsCategory)\n                                    continue;\n\n                                if (!IncludeVoiceChannels && channel.IsVoice)\n                                    continue;\n\n                                channels.Add(channel);\n\n                                ctx.Status(\n                                    Markup.Escape($\"Fetched '{channel.GetHierarchicalName()}'.\")\n                                );\n\n                                fetchedChannelsCount++;\n                            }\n                        }\n                    );\n\n                await console.Output.WriteLineAsync($\"Fetched {fetchedChannelsCount} channel(s).\");\n            }\n        }\n        // Pull from the data package\n        else\n        {\n            await console.Output.WriteLineAsync(\"Extracting channels...\");\n\n            var dump = await DataDump.LoadAsync(DataPackageFilePath, cancellationToken);\n            var inaccessibleChannels = new List<DataDumpChannel>();\n\n            await console\n                .CreateStatusTicker()\n                .StartAsync(\n                    \"...\",\n                    async ctx =>\n                    {\n                        foreach (var dumpChannel in dump.Channels)\n                        {\n                            ctx.Status(\n                                Markup.Escape(\n                                    $\"Fetching '{dumpChannel.Name}' ({dumpChannel.Id})...\"\n                                )\n                            );\n\n                            try\n                            {\n                                var channel = await Discord.GetChannelAsync(\n                                    dumpChannel.Id,\n                                    cancellationToken\n                                );\n\n                                channels.Add(channel);\n                            }\n                            catch (DiscordChatExporterException)\n                            {\n                                inaccessibleChannels.Add(dumpChannel);\n                            }\n                        }\n                    }\n                );\n\n            await console.Output.WriteLineAsync($\"Fetched {channels} channel(s).\");\n\n            // Print inaccessible channels\n            if (inaccessibleChannels.Any())\n            {\n                await console.Output.WriteLineAsync();\n\n                using (console.WithForegroundColor(ConsoleColor.Red))\n                {\n                    await console.Error.WriteLineAsync(\n                        \"Failed to access the following channel(s):\"\n                    );\n                }\n\n                foreach (var dumpChannel in inaccessibleChannels)\n                    await console.Error.WriteLineAsync($\"{dumpChannel.Name} ({dumpChannel.Id})\");\n\n                await console.Error.WriteLineAsync();\n            }\n        }\n\n        // Filter out unwanted channels\n        if (!IncludeDirectChannels)\n            channels.RemoveAll(c => c.IsDirect);\n        if (!IncludeGuildChannels)\n            channels.RemoveAll(c => c.IsGuild);\n        if (!IncludeVoiceChannels)\n            channels.RemoveAll(c => c.IsVoice);\n\n        await ExportAsync(console, channels);\n    }\n}\n"
  },
  {
    "path": "DiscordChatExporter.Cli/Commands/ExportChannelsCommand.cs",
    "content": "﻿using System.Collections.Generic;\nusing System.Threading.Tasks;\nusing CliFx.Attributes;\nusing CliFx.Infrastructure;\nusing DiscordChatExporter.Cli.Commands.Base;\nusing DiscordChatExporter.Core.Discord;\nusing DiscordChatExporter.Core.Discord.Data;\nusing DiscordChatExporter.Core.Utils.Extensions;\n\nnamespace DiscordChatExporter.Cli.Commands;\n\n[Command(\"export\", Description = \"Exports one or multiple channels.\")]\npublic class ExportChannelsCommand : ExportCommandBase\n{\n    // TODO: change this to plural (breaking change)\n    [CommandOption(\n        \"channel\",\n        'c',\n        Description = \"Channel ID(s). \"\n            + \"If provided with category ID(s), all channels inside those categories will be exported.\"\n    )]\n    public required IReadOnlyList<Snowflake> ChannelIds { get; init; }\n\n    public override async ValueTask ExecuteAsync(IConsole console)\n    {\n        await base.ExecuteAsync(console);\n\n        var cancellationToken = console.RegisterCancellationHandler();\n\n        await console.Output.WriteLineAsync(\"Resolving channel(s)...\");\n\n        var channels = new List<Channel>();\n        var channelsByGuild = new Dictionary<Snowflake, IReadOnlyList<Channel>>();\n\n        foreach (var channelId in ChannelIds)\n        {\n            var channel = await Discord.GetChannelAsync(channelId, cancellationToken);\n\n            // Unwrap categories\n            if (channel.IsCategory)\n            {\n                var guildChannels =\n                    channelsByGuild.GetValueOrDefault(channel.GuildId)\n                    ?? await Discord.GetGuildChannelsAsync(channel.GuildId, cancellationToken);\n\n                foreach (var guildChannel in guildChannels)\n                {\n                    if (guildChannel.Parent?.Id == channel.Id)\n                        channels.Add(guildChannel);\n                }\n\n                // Cache the guild channels to avoid redundant work\n                channelsByGuild[channel.GuildId] = guildChannels;\n            }\n            else\n            {\n                channels.Add(channel);\n            }\n        }\n\n        await ExportAsync(console, channels);\n    }\n}\n"
  },
  {
    "path": "DiscordChatExporter.Cli/Commands/ExportDirectMessagesCommand.cs",
    "content": "﻿using System.Threading.Tasks;\nusing CliFx.Attributes;\nusing CliFx.Infrastructure;\nusing DiscordChatExporter.Cli.Commands.Base;\nusing DiscordChatExporter.Core.Discord.Data;\nusing DiscordChatExporter.Core.Utils.Extensions;\n\nnamespace DiscordChatExporter.Cli.Commands;\n\n[Command(\"exportdm\", Description = \"Exports all direct message channels.\")]\npublic class ExportDirectMessagesCommand : ExportCommandBase\n{\n    public override async ValueTask ExecuteAsync(IConsole console)\n    {\n        await base.ExecuteAsync(console);\n\n        var cancellationToken = console.RegisterCancellationHandler();\n\n        await console.Output.WriteLineAsync(\"Fetching channels...\");\n        var channels = await Discord.GetGuildChannelsAsync(\n            Guild.DirectMessages.Id,\n            cancellationToken\n        );\n\n        await ExportAsync(console, channels);\n    }\n}\n"
  },
  {
    "path": "DiscordChatExporter.Cli/Commands/ExportGuildCommand.cs",
    "content": "﻿using System.Collections.Generic;\nusing System.Threading.Tasks;\nusing CliFx.Attributes;\nusing CliFx.Infrastructure;\nusing DiscordChatExporter.Cli.Commands.Base;\nusing DiscordChatExporter.Cli.Utils.Extensions;\nusing DiscordChatExporter.Core.Discord;\nusing DiscordChatExporter.Core.Discord.Data;\nusing Spectre.Console;\n\nnamespace DiscordChatExporter.Cli.Commands;\n\n[Command(\"exportguild\", Description = \"Exports all channels within the specified server.\")]\npublic class ExportGuildCommand : ExportCommandBase\n{\n    [CommandOption(\"guild\", 'g', Description = \"Server ID.\")]\n    public required Snowflake GuildId { get; init; }\n\n    [CommandOption(\"include-vc\", Description = \"Include voice channels.\")]\n    public bool IncludeVoiceChannels { get; init; } = true;\n\n    public override async ValueTask ExecuteAsync(IConsole console)\n    {\n        await base.ExecuteAsync(console);\n\n        var cancellationToken = console.RegisterCancellationHandler();\n        var channels = new List<Channel>();\n\n        await console.Output.WriteLineAsync(\"Fetching channels...\");\n\n        var fetchedChannelsCount = 0;\n        await console\n            .CreateStatusTicker()\n            .StartAsync(\n                \"...\",\n                async ctx =>\n                {\n                    await foreach (\n                        var channel in Discord.GetGuildChannelsAsync(GuildId, cancellationToken)\n                    )\n                    {\n                        if (channel.IsCategory)\n                            continue;\n\n                        if (!IncludeVoiceChannels && channel.IsVoice)\n                            continue;\n\n                        channels.Add(channel);\n\n                        ctx.Status(Markup.Escape($\"Fetched '{channel.GetHierarchicalName()}'.\"));\n\n                        fetchedChannelsCount++;\n                    }\n                }\n            );\n\n        await console.Output.WriteLineAsync($\"Fetched {fetchedChannelsCount} channel(s).\");\n\n        await ExportAsync(console, channels);\n    }\n}\n"
  },
  {
    "path": "DiscordChatExporter.Cli/Commands/GetChannelsCommand.cs",
    "content": "﻿using System;\nusing System.Linq;\nusing System.Threading.Tasks;\nusing CliFx.Attributes;\nusing CliFx.Infrastructure;\nusing DiscordChatExporter.Cli.Commands.Base;\nusing DiscordChatExporter.Cli.Commands.Converters;\nusing DiscordChatExporter.Cli.Commands.Shared;\nusing DiscordChatExporter.Core.Discord;\nusing DiscordChatExporter.Core.Utils.Extensions;\n\nnamespace DiscordChatExporter.Cli.Commands;\n\n[Command(\"channels\", Description = \"Get the list of channels in a server.\")]\npublic class GetChannelsCommand : DiscordCommandBase\n{\n    [CommandOption(\"guild\", 'g', Description = \"Server ID.\")]\n    public required Snowflake GuildId { get; init; }\n\n    [CommandOption(\"include-vc\", Description = \"Include voice channels.\")]\n    public bool IncludeVoiceChannels { get; init; } = true;\n\n    [CommandOption(\n        \"include-threads\",\n        Description = \"Which types of threads should be included.\",\n        Converter = typeof(ThreadInclusionModeBindingConverter)\n    )]\n    public ThreadInclusionMode ThreadInclusionMode { get; init; } = ThreadInclusionMode.None;\n\n    public override async ValueTask ExecuteAsync(IConsole console)\n    {\n        await base.ExecuteAsync(console);\n\n        var cancellationToken = console.RegisterCancellationHandler();\n\n        var channels = (await Discord.GetGuildChannelsAsync(GuildId, cancellationToken))\n            .Where(c => !c.IsCategory)\n            .Where(c => IncludeVoiceChannels || !c.IsVoice)\n            .OrderBy(c => c.Parent?.Position)\n            .ThenBy(c => c.Name)\n            .ToArray();\n\n        var channelIdMaxLength = channels\n            .Select(c => c.Id.ToString().Length)\n            .OrderDescending()\n            .FirstOrDefault();\n\n        var threads =\n            ThreadInclusionMode != ThreadInclusionMode.None\n                ? (\n                    await Discord.GetGuildThreadsAsync(\n                        GuildId,\n                        ThreadInclusionMode == ThreadInclusionMode.All,\n                        null,\n                        null,\n                        cancellationToken\n                    )\n                )\n                    .OrderBy(c => c.Name)\n                    .ToArray()\n                : [];\n\n        foreach (var channel in channels)\n        {\n            // Channel ID\n            await console.Output.WriteAsync(\n                channel.Id.ToString().PadRight(channelIdMaxLength, ' ')\n            );\n\n            // Separator\n            using (console.WithForegroundColor(ConsoleColor.DarkGray))\n                await console.Output.WriteAsync(\" | \");\n\n            // Channel name\n            using (console.WithForegroundColor(ConsoleColor.White))\n                await console.Output.WriteLineAsync(channel.GetHierarchicalName());\n\n            var channelThreads = threads.Where(t => t.Parent?.Id == channel.Id).ToArray();\n            var channelThreadIdMaxLength = channelThreads\n                .Select(t => t.Id.ToString().Length)\n                .OrderDescending()\n                .FirstOrDefault();\n\n            foreach (var channelThread in channelThreads)\n            {\n                // Indent\n                await console.Output.WriteAsync(\" * \");\n\n                // Thread ID\n                await console.Output.WriteAsync(\n                    channelThread.Id.ToString().PadRight(channelThreadIdMaxLength, ' ')\n                );\n\n                // Separator\n                using (console.WithForegroundColor(ConsoleColor.DarkGray))\n                    await console.Output.WriteAsync(\" | \");\n\n                // Thread name\n                using (console.WithForegroundColor(ConsoleColor.White))\n                    await console.Output.WriteAsync($\"Thread / {channelThread.Name}\");\n\n                // Separator\n                using (console.WithForegroundColor(ConsoleColor.DarkGray))\n                    await console.Output.WriteAsync(\" | \");\n\n                // Thread status\n                using (console.WithForegroundColor(ConsoleColor.White))\n                    await console.Output.WriteLineAsync(\n                        channelThread.IsArchived ? \"Archived\" : \"Active\"\n                    );\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "DiscordChatExporter.Cli/Commands/GetDirectChannelsCommand.cs",
    "content": "﻿using System;\nusing System.Linq;\nusing System.Threading.Tasks;\nusing CliFx.Attributes;\nusing CliFx.Infrastructure;\nusing DiscordChatExporter.Cli.Commands.Base;\nusing DiscordChatExporter.Core.Discord.Data;\nusing DiscordChatExporter.Core.Utils.Extensions;\n\nnamespace DiscordChatExporter.Cli.Commands;\n\n[Command(\"dm\", Description = \"Gets the list of all direct message channels.\")]\npublic class GetDirectChannelsCommand : DiscordCommandBase\n{\n    public override async ValueTask ExecuteAsync(IConsole console)\n    {\n        await base.ExecuteAsync(console);\n\n        var cancellationToken = console.RegisterCancellationHandler();\n\n        var channels = (\n            await Discord.GetGuildChannelsAsync(Guild.DirectMessages.Id, cancellationToken)\n        )\n            .OrderByDescending(c => c.LastMessageId)\n            .ThenBy(c => c.Name)\n            .ToArray();\n\n        var channelIdMaxLength = channels\n            .Select(c => c.Id.ToString().Length)\n            .OrderDescending()\n            .FirstOrDefault();\n\n        foreach (var channel in channels)\n        {\n            // Channel ID\n            await console.Output.WriteAsync(\n                channel.Id.ToString().PadRight(channelIdMaxLength, ' ')\n            );\n\n            // Separator\n            using (console.WithForegroundColor(ConsoleColor.DarkGray))\n                await console.Output.WriteAsync(\" | \");\n\n            // Channel name\n            using (console.WithForegroundColor(ConsoleColor.White))\n                await console.Output.WriteLineAsync(channel.GetHierarchicalName());\n        }\n    }\n}\n"
  },
  {
    "path": "DiscordChatExporter.Cli/Commands/GetGuildsCommand.cs",
    "content": "﻿using System;\nusing System.Linq;\nusing System.Threading.Tasks;\nusing CliFx.Attributes;\nusing CliFx.Infrastructure;\nusing DiscordChatExporter.Cli.Commands.Base;\nusing DiscordChatExporter.Core.Discord.Data;\nusing DiscordChatExporter.Core.Utils.Extensions;\n\nnamespace DiscordChatExporter.Cli.Commands;\n\n[Command(\"guilds\", Description = \"Gets the list of accessible servers.\")]\npublic class GetGuildsCommand : DiscordCommandBase\n{\n    public override async ValueTask ExecuteAsync(IConsole console)\n    {\n        await base.ExecuteAsync(console);\n\n        var cancellationToken = console.RegisterCancellationHandler();\n\n        var guilds = (await Discord.GetUserGuildsAsync(cancellationToken))\n            // Show direct messages first\n            .OrderByDescending(g => g.Id == Guild.DirectMessages.Id)\n            .ThenBy(g => g.Name)\n            .ToArray();\n\n        var guildIdMaxLength = guilds\n            .Select(g => g.Id.ToString().Length)\n            .OrderDescending()\n            .FirstOrDefault();\n\n        foreach (var guild in guilds)\n        {\n            // Guild ID\n            await console.Output.WriteAsync(guild.Id.ToString().PadRight(guildIdMaxLength, ' '));\n\n            // Separator\n            using (console.WithForegroundColor(ConsoleColor.DarkGray))\n                await console.Output.WriteAsync(\" | \");\n\n            // Guild name\n            using (console.WithForegroundColor(ConsoleColor.White))\n                await console.Output.WriteLineAsync(guild.Name);\n        }\n    }\n}\n"
  },
  {
    "path": "DiscordChatExporter.Cli/Commands/GuideCommand.cs",
    "content": "using System;\nusing System.Threading.Tasks;\nusing CliFx;\nusing CliFx.Attributes;\nusing CliFx.Infrastructure;\n\nnamespace DiscordChatExporter.Cli.Commands;\n\n[Command(\"guide\", Description = \"Explains how to obtain the token, server or channel ID.\")]\npublic class GuideCommand : ICommand\n{\n    public ValueTask ExecuteAsync(IConsole console)\n    {\n        // User token\n        using (console.WithForegroundColor(ConsoleColor.White))\n            console.Output.WriteLine(\"To get the token for your personal account:\");\n\n        console.Output.WriteLine(\n            \" *  Automating user accounts is technically against TOS — USE AT YOUR OWN RISK!\"\n        );\n        console.Output.WriteLine(\" 1. Open Discord in your web browser and login\");\n        console.Output.WriteLine(\" 2. Open any server or direct message channel\");\n        console.Output.WriteLine(\" 3. Press Ctrl+Shift+I to show developer tools\");\n        console.Output.WriteLine(\" 4. Navigate to the Network tab\");\n        console.Output.WriteLine(\" 5. Press Ctrl+R to reload\");\n        console.Output.WriteLine(\" 6. Switch between random channels to trigger network requests\");\n        console.Output.WriteLine(\" 7. Search for a request that starts with \\\"messages\\\"\");\n        console.Output.WriteLine(\" 8. Select the Headers tab on the right\");\n        console.Output.WriteLine(\" 9. Scroll down to the Request Headers section\");\n        console.Output.WriteLine(\" 10. Copy the value of the \\\"authorization\\\" header\");\n        console.Output.WriteLine();\n\n        // Bot token\n        using (console.WithForegroundColor(ConsoleColor.White))\n            console.Output.WriteLine(\"To get the token for your bot:\");\n\n        console.Output.WriteLine(\n            \" The token is generated during bot creation. If you lost it, generate a new one:\"\n        );\n        console.Output.WriteLine(\" 1. Go to Discord developer portal\");\n        console.Output.WriteLine(\" 2. Open your application's settings\");\n        console.Output.WriteLine(\" 3. Navigate to the Bot section on the left\");\n        console.Output.WriteLine(\" 4. Under Token click Reset Token\");\n        console.Output.WriteLine(\" 5. Click Yes, do it! and authenticate to confirm\");\n        console.Output.WriteLine(\n            \" *  Integrations using the previous token will stop working until updated\"\n        );\n        console.Output.WriteLine(\n            \" *  Your bot needs to have the Message Content Intent enabled to read messages\"\n        );\n        console.Output.WriteLine();\n\n        // Guild or channel ID\n        using (console.WithForegroundColor(ConsoleColor.White))\n            console.Output.WriteLine(\"To get the ID of a server or a channel:\");\n\n        console.Output.WriteLine(\" 1. Open Discord\");\n        console.Output.WriteLine(\" 2. Open Settings\");\n        console.Output.WriteLine(\" 3. Go to Advanced section\");\n        console.Output.WriteLine(\" 4. Enable Developer Mode\");\n        console.Output.WriteLine(\n            \" 5. Right-click on the desired server or channel and click Copy Server ID or Copy Channel ID\"\n        );\n        console.Output.WriteLine();\n\n        // Docs link\n        using (console.WithForegroundColor(ConsoleColor.White))\n        {\n            console.Output.WriteLine(\n                \"If you have questions or issues, please refer to the documentation:\"\n            );\n        }\n\n        using (console.WithForegroundColor(ConsoleColor.DarkCyan))\n        {\n            console.Output.WriteLine(\n                \"https://github.com/Tyrrrz/DiscordChatExporter/blob/prime/.docs\"\n            );\n        }\n\n        return default;\n    }\n}\n"
  },
  {
    "path": "DiscordChatExporter.Cli/Commands/Shared/ThreadInclusionMode.cs",
    "content": "﻿namespace DiscordChatExporter.Cli.Commands.Shared;\n\npublic enum ThreadInclusionMode\n{\n    None,\n    Active,\n    All,\n}\n"
  },
  {
    "path": "DiscordChatExporter.Cli/DiscordChatExporter.Cli.csproj",
    "content": "<Project Sdk=\"Microsoft.NET.Sdk\">\n  <PropertyGroup>\n    <OutputType>Exe</OutputType>\n    <ApplicationIcon>..\\favicon.ico</ApplicationIcon>\n    <PublishTrimmed>true</PublishTrimmed>\n    <CopyOutputSymbolsToPublishDirectory>false</CopyOutputSymbolsToPublishDirectory>\n  </PropertyGroup>\n\n  <!-- HACK: Disable trim warnings because they seem to break when the code contains C# 14 extension blocks -->\n  <PropertyGroup>\n    <EnableTrimAnalyzer>false</EnableTrimAnalyzer>\n    <EnableAotAnalyzer>false</EnableAotAnalyzer>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <PackageReference Include=\"CliFx\" />\n    <PackageReference Include=\"CSharpier.MsBuild\" PrivateAssets=\"all\" />\n    <PackageReference Include=\"Deorcify\" PrivateAssets=\"all\" />\n    <PackageReference Include=\"Gress\" />\n    <PackageReference Include=\"Spectre.Console\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <ProjectReference Include=\"..\\DiscordChatExporter.Core\\DiscordChatExporter.Core.csproj\" />\n  </ItemGroup>\n</Project>\n"
  },
  {
    "path": "DiscordChatExporter.Cli/Program.cs",
    "content": "﻿using System.Diagnostics.CodeAnalysis;\nusing System.Threading.Tasks;\nusing CliFx;\nusing DiscordChatExporter.Cli.Commands;\nusing DiscordChatExporter.Cli.Commands.Converters;\nusing DiscordChatExporter.Core.Exporting.Filtering;\nusing DiscordChatExporter.Core.Exporting.Partitioning;\n\nnamespace DiscordChatExporter.Cli;\n\npublic static class Program\n{\n    // Explicit references because CliFx relies on reflection and we're publishing with trimming enabled\n    [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(ExportAllCommand))]\n    [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(ExportChannelsCommand))]\n    [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(ExportDirectMessagesCommand))]\n    [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(ExportGuildCommand))]\n    [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(GetChannelsCommand))]\n    [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(GetDirectChannelsCommand))]\n    [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(GetGuildsCommand))]\n    [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(GuideCommand))]\n    [DynamicDependency(\n        DynamicallyAccessedMemberTypes.All,\n        typeof(ThreadInclusionModeBindingConverter)\n    )]\n    [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(TruthyBooleanBindingConverter))]\n    [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(PartitionLimit))]\n    [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(MessageFilter))]\n    public static async Task<int> Main(string[] args) =>\n        await new CliApplicationBuilder()\n            .AddCommand<ExportAllCommand>()\n            .AddCommand<ExportChannelsCommand>()\n            .AddCommand<ExportDirectMessagesCommand>()\n            .AddCommand<ExportGuildCommand>()\n            .AddCommand<GetChannelsCommand>()\n            .AddCommand<GetDirectChannelsCommand>()\n            .AddCommand<GetGuildsCommand>()\n            .AddCommand<GuideCommand>()\n            .Build()\n            .RunAsync(args);\n}\n"
  },
  {
    "path": "DiscordChatExporter.Cli/Utils/Extensions/ConsoleExtensions.cs",
    "content": "﻿using System;\nusing System.Threading.Tasks;\nusing CliFx.Infrastructure;\nusing Spectre.Console;\n\nnamespace DiscordChatExporter.Cli.Utils.Extensions;\n\ninternal static class ConsoleExtensions\n{\n    extension(IConsole console)\n    {\n        public IAnsiConsole CreateAnsiConsole() =>\n            AnsiConsole.Create(\n                new AnsiConsoleSettings\n                {\n                    Ansi = AnsiSupport.Detect,\n                    ColorSystem = ColorSystemSupport.Detect,\n                    Out = new AnsiConsoleOutput(console.Output),\n                }\n            );\n\n        public Status CreateStatusTicker() =>\n            console.CreateAnsiConsole().Status().AutoRefresh(true);\n\n        public Progress CreateProgressTicker() =>\n            console\n                .CreateAnsiConsole()\n                .Progress()\n                .AutoClear(false)\n                .AutoRefresh(true)\n                .HideCompleted(false)\n                .Columns(\n                    new TaskDescriptionColumn { Alignment = Justify.Left },\n                    new ProgressBarColumn(),\n                    new PercentageColumn()\n                );\n    }\n\n    public static async ValueTask StartTaskAsync(\n        this ProgressContext context,\n        string description,\n        Func<ProgressTask, ValueTask> performOperationAsync\n    )\n    {\n        // Description cannot be empty\n        // https://github.com/Tyrrrz/DiscordChatExporter/issues/1133\n        var actualDescription = !string.IsNullOrWhiteSpace(description) ? description : \"...\";\n\n        var progressTask = context.AddTask(\n            actualDescription,\n            new ProgressTaskSettings { MaxValue = 1 }\n        );\n\n        try\n        {\n            await performOperationAsync(progressTask);\n        }\n        finally\n        {\n            progressTask.Value = progressTask.MaxValue;\n            progressTask.StopTask();\n        }\n    }\n}\n"
  },
  {
    "path": "DiscordChatExporter.Cli.Tests/DiscordChatExporter.Cli.Tests.csproj",
    "content": "<Project Sdk=\"Microsoft.NET.Sdk\">\n  <PropertyGroup>\n    <IsPackable>false</IsPackable>\n    <IsTestProject>true</IsTestProject>\n    <UserSecretsId>d1fe5ae2-2a19-404d-a36e-81ba9eada1c1</UserSecretsId>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <Content Include=\"xunit.runner.json\" CopyToOutputDirectory=\"PreserveNewest\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <PackageReference Include=\"AngleSharp\" />\n    <PackageReference Include=\"coverlet.collector\" PrivateAssets=\"all\" />\n    <PackageReference Include=\"CSharpier.MsBuild\" PrivateAssets=\"all\" />\n    <PackageReference Include=\"FluentAssertions\" />\n    <PackageReference Include=\"GitHubActionsTestLogger\" PrivateAssets=\"all\" />\n    <PackageReference Include=\"JsonExtensions\" />\n    <PackageReference Include=\"Microsoft.Extensions.Configuration\" />\n    <PackageReference Include=\"Microsoft.Extensions.Configuration.EnvironmentVariables\" />\n    <PackageReference Include=\"Microsoft.Extensions.Configuration.UserSecrets\" />\n    <PackageReference Include=\"Microsoft.NET.Test.Sdk\" />\n    <PackageReference Include=\"xunit\" />\n    <PackageReference Include=\"xunit.runner.visualstudio\" PrivateAssets=\"all\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <ProjectReference Include=\"..\\DiscordChatExporter.Cli\\DiscordChatExporter.Cli.csproj\" />\n  </ItemGroup>\n</Project>\n"
  },
  {
    "path": "DiscordChatExporter.Cli.Tests/Infra/ChannelIds.cs",
    "content": "﻿using DiscordChatExporter.Core.Discord;\n\nnamespace DiscordChatExporter.Cli.Tests.Infra;\n\npublic static class ChannelIds\n{\n    public static Snowflake AttachmentTestCases { get; } = Snowflake.Parse(\"885587741654536192\");\n\n    public static Snowflake DateRangeTestCases { get; } = Snowflake.Parse(\"866674248747319326\");\n\n    public static Snowflake EmbedTestCases { get; } = Snowflake.Parse(\"866472452459462687\");\n\n    public static Snowflake EmojiTestCases { get; } = Snowflake.Parse(\"866768438290415636\");\n\n    public static Snowflake GroupingTestCases { get; } = Snowflake.Parse(\"992092091545034842\");\n\n    public static Snowflake FilterTestCases { get; } = Snowflake.Parse(\"866744075033641020\");\n\n    public static Snowflake ForwardTestCases { get; } = Snowflake.Parse(\"1455202357204877477\");\n\n    public static Snowflake MarkdownTestCases { get; } = Snowflake.Parse(\"866459526819348521\");\n\n    public static Snowflake MentionTestCases { get; } = Snowflake.Parse(\"866458801389174794\");\n\n    public static Snowflake ReplyTestCases { get; } = Snowflake.Parse(\"866459871934677052\");\n\n    public static Snowflake SelfContainedTestCases { get; } = Snowflake.Parse(\"887441432678379560\");\n\n    public static Snowflake StickerTestCases { get; } = Snowflake.Parse(\"939668868253769729\");\n}\n"
  },
  {
    "path": "DiscordChatExporter.Cli.Tests/Infra/ExportWrapper.cs",
    "content": "﻿using System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Linq;\nusing System.Reflection;\nusing System.Text.Json;\nusing System.Threading.Tasks;\nusing AngleSharp.Dom;\nusing AngleSharp.Html.Dom;\nusing AsyncKeyedLock;\nusing CliFx.Infrastructure;\nusing DiscordChatExporter.Cli.Commands;\nusing DiscordChatExporter.Cli.Tests.Utils;\nusing DiscordChatExporter.Core.Discord;\nusing DiscordChatExporter.Core.Exporting;\nusing JsonExtensions;\n\nnamespace DiscordChatExporter.Cli.Tests.Infra;\n\npublic static class ExportWrapper\n{\n    private static readonly AsyncKeyedLocker<string> Locker = new();\n\n    private static readonly string DirPath = Path.Combine(\n        Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)\n            ?? Directory.GetCurrentDirectory(),\n        \"ExportCache\"\n    );\n\n    static ExportWrapper()\n    {\n        try\n        {\n            Directory.Delete(DirPath, true);\n        }\n        catch (DirectoryNotFoundException) { }\n\n        Directory.CreateDirectory(DirPath);\n    }\n\n    private static async ValueTask<string> ExportAsync(Snowflake channelId, ExportFormat format)\n    {\n        var fileName = channelId.ToString() + '.' + format.GetFileExtension();\n        var filePath = Path.Combine(DirPath, fileName);\n\n        using var _ = await Locker.LockAsync(filePath);\n        using var console = new FakeConsole();\n\n        // Perform the export only if it hasn't been done before\n        if (!File.Exists(filePath))\n        {\n            await new ExportChannelsCommand\n            {\n                Token = Secrets.DiscordToken,\n                ChannelIds = [channelId],\n                ExportFormat = format,\n                OutputPath = filePath,\n                Locale = \"en-US\",\n                IsUtcNormalizationEnabled = true,\n            }.ExecuteAsync(console);\n        }\n\n        return await File.ReadAllTextAsync(filePath);\n    }\n\n    public static async ValueTask<IHtmlDocument> ExportAsHtmlAsync(Snowflake channelId) =>\n        Html.Parse(await ExportAsync(channelId, ExportFormat.HtmlDark));\n\n    public static async ValueTask<JsonElement> ExportAsJsonAsync(Snowflake channelId) =>\n        Json.Parse(await ExportAsync(channelId, ExportFormat.Json));\n\n    public static async ValueTask<string> ExportAsPlainTextAsync(Snowflake channelId) =>\n        await ExportAsync(channelId, ExportFormat.PlainText);\n\n    public static async ValueTask<string> ExportAsCsvAsync(Snowflake channelId) =>\n        await ExportAsync(channelId, ExportFormat.Csv);\n\n    public static async ValueTask<IReadOnlyList<IElement>> GetMessagesAsHtmlAsync(\n        Snowflake channelId\n    ) => (await ExportAsHtmlAsync(channelId)).QuerySelectorAll(\"[data-message-id]\").ToArray();\n\n    public static async ValueTask<IReadOnlyList<JsonElement>> GetMessagesAsJsonAsync(\n        Snowflake channelId\n    ) => (await ExportAsJsonAsync(channelId)).GetProperty(\"messages\").EnumerateArray().ToArray();\n\n    public static async ValueTask<IElement> GetMessageAsHtmlAsync(\n        Snowflake channelId,\n        Snowflake messageId\n    )\n    {\n        var message = (await GetMessagesAsHtmlAsync(channelId)).SingleOrDefault(e =>\n            string.Equals(\n                e.GetAttribute(\"data-message-id\"),\n                messageId.ToString(),\n                StringComparison.OrdinalIgnoreCase\n            )\n        );\n\n        if (message is null)\n        {\n            throw new InvalidOperationException(\n                $\"Message #{messageId} not found in the export of channel #{channelId}.\"\n            );\n        }\n\n        return message;\n    }\n\n    public static async ValueTask<JsonElement> GetMessageAsJsonAsync(\n        Snowflake channelId,\n        Snowflake messageId\n    )\n    {\n        var message = (await GetMessagesAsJsonAsync(channelId)).SingleOrDefault(j =>\n            string.Equals(\n                j.GetProperty(\"id\").GetString(),\n                messageId.ToString(),\n                StringComparison.OrdinalIgnoreCase\n            )\n        );\n\n        if (message.ValueKind == JsonValueKind.Undefined)\n        {\n            throw new InvalidOperationException(\n                $\"Message #{messageId} not found in the export of channel #{channelId}.\"\n            );\n        }\n\n        return message;\n    }\n}\n"
  },
  {
    "path": "DiscordChatExporter.Cli.Tests/Infra/Secrets.cs",
    "content": "﻿using System;\nusing System.Reflection;\nusing Microsoft.Extensions.Configuration;\n\nnamespace DiscordChatExporter.Cli.Tests.Infra;\n\ninternal static class Secrets\n{\n    private static readonly IConfigurationRoot Configuration = new ConfigurationBuilder()\n        .AddUserSecrets(Assembly.GetExecutingAssembly())\n        .AddEnvironmentVariables()\n        .Build();\n\n    public static string DiscordToken =>\n        Configuration[\"DISCORD_TOKEN\"]\n        ?? throw new InvalidOperationException(\"Discord token not provided for tests.\");\n}\n"
  },
  {
    "path": "DiscordChatExporter.Cli.Tests/Readme.md",
    "content": "﻿# DiscordChatExporter Tests\n\nThis test suite runs against a real Discord server, specifically created to exercise different behaviors required by the test scenarios.\nIn order to run these tests locally, you need to join the test server and configure your authentication token.\n\n1. [Join the test server](https://discord.gg/eRV8Vap5bm)\n2. Locate your Discord authentication token\n3. Add your token to user secrets: `dotnet user-secrets set DISCORD_TOKEN <token>`\n4. Run the tests: `dotnet test`\n\n> [!NOTE]\n> If you want to add a new test case, please let me know and I will give you the required permissions on the server."
  },
  {
    "path": "DiscordChatExporter.Cli.Tests/Specs/CsvContentSpecs.cs",
    "content": "﻿using System.Threading.Tasks;\nusing DiscordChatExporter.Cli.Tests.Infra;\nusing FluentAssertions;\nusing Xunit;\n\nnamespace DiscordChatExporter.Cli.Tests.Specs;\n\npublic class CsvContentSpecs\n{\n    [Fact]\n    public async Task I_can_export_a_channel_in_the_CSV_format()\n    {\n        // Act\n        var document = await ExportWrapper.ExportAsCsvAsync(ChannelIds.DateRangeTestCases);\n\n        // Assert\n        document\n            .Should()\n            .ContainAll(\n                \"tyrrrz\",\n                \"Hello world\",\n                \"Goodbye world\",\n                \"Foo bar\",\n                \"Hurdle Durdle\",\n                \"One\",\n                \"Two\",\n                \"Three\",\n                \"Yeet\"\n            );\n    }\n}\n"
  },
  {
    "path": "DiscordChatExporter.Cli.Tests/Specs/DateRangeSpecs.cs",
    "content": "﻿using System;\nusing System.IO;\nusing System.Linq;\nusing System.Threading.Tasks;\nusing CliFx.Infrastructure;\nusing DiscordChatExporter.Cli.Commands;\nusing DiscordChatExporter.Cli.Tests.Infra;\nusing DiscordChatExporter.Cli.Tests.Utils;\nusing DiscordChatExporter.Core.Discord;\nusing DiscordChatExporter.Core.Exporting;\nusing FluentAssertions;\nusing JsonExtensions;\nusing Xunit;\n\nnamespace DiscordChatExporter.Cli.Tests.Specs;\n\npublic class DateRangeSpecs\n{\n    [Fact]\n    public async Task I_can_filter_the_export_to_only_include_messages_sent_after_the_specified_date()\n    {\n        // Arrange\n        var after = new DateTimeOffset(2021, 07, 24, 0, 0, 0, TimeSpan.Zero);\n        using var file = TempFile.Create();\n\n        // Act\n        await new ExportChannelsCommand\n        {\n            Token = Secrets.DiscordToken,\n            ChannelIds = [ChannelIds.DateRangeTestCases],\n            ExportFormat = ExportFormat.Json,\n            OutputPath = file.Path,\n            After = Snowflake.FromDate(after),\n        }.ExecuteAsync(new FakeConsole());\n\n        // Assert\n        var timestamps = Json.Parse(await File.ReadAllTextAsync(file.Path))\n            .GetProperty(\"messages\")\n            .EnumerateArray()\n            .Select(j => j.GetProperty(\"timestamp\").GetDateTimeOffset())\n            .ToArray();\n\n        timestamps.All(t => t > after).Should().BeTrue();\n\n        timestamps\n            .Should()\n            .BeEquivalentTo(\n                [\n                    new DateTimeOffset(2021, 07, 24, 13, 49, 13, TimeSpan.Zero),\n                    new DateTimeOffset(2021, 07, 24, 14, 52, 38, TimeSpan.Zero),\n                    new DateTimeOffset(2021, 07, 24, 14, 52, 39, TimeSpan.Zero),\n                    new DateTimeOffset(2021, 07, 24, 14, 52, 40, TimeSpan.Zero),\n                    new DateTimeOffset(2021, 09, 08, 14, 26, 35, TimeSpan.Zero),\n                ],\n                o =>\n                    o.Using<DateTimeOffset>(ctx =>\n                            ctx.Subject.Should().BeCloseTo(ctx.Expectation, TimeSpan.FromSeconds(1))\n                        )\n                        .WhenTypeIs<DateTimeOffset>()\n            );\n    }\n\n    [Fact]\n    public async Task I_can_filter_the_export_to_only_include_messages_sent_before_the_specified_date()\n    {\n        // Arrange\n        var before = new DateTimeOffset(2021, 07, 24, 0, 0, 0, TimeSpan.Zero);\n        using var file = TempFile.Create();\n\n        // Act\n        await new ExportChannelsCommand\n        {\n            Token = Secrets.DiscordToken,\n            ChannelIds = [ChannelIds.DateRangeTestCases],\n            ExportFormat = ExportFormat.Json,\n            OutputPath = file.Path,\n            Before = Snowflake.FromDate(before),\n        }.ExecuteAsync(new FakeConsole());\n\n        // Assert\n        var timestamps = Json.Parse(await File.ReadAllTextAsync(file.Path))\n            .GetProperty(\"messages\")\n            .EnumerateArray()\n            .Select(j => j.GetProperty(\"timestamp\").GetDateTimeOffset())\n            .ToArray();\n\n        timestamps.All(t => t < before).Should().BeTrue();\n\n        timestamps\n            .Should()\n            .BeEquivalentTo(\n                [\n                    new DateTimeOffset(2021, 07, 19, 13, 34, 18, TimeSpan.Zero),\n                    new DateTimeOffset(2021, 07, 19, 15, 58, 48, TimeSpan.Zero),\n                    new DateTimeOffset(2021, 07, 19, 17, 23, 58, TimeSpan.Zero),\n                ],\n                o =>\n                    o.Using<DateTimeOffset>(ctx =>\n                            ctx.Subject.Should().BeCloseTo(ctx.Expectation, TimeSpan.FromSeconds(1))\n                        )\n                        .WhenTypeIs<DateTimeOffset>()\n            );\n    }\n\n    [Fact]\n    public async Task I_can_filter_the_export_to_only_include_messages_sent_between_the_specified_dates()\n    {\n        // Arrange\n        var after = new DateTimeOffset(2021, 07, 24, 0, 0, 0, TimeSpan.Zero);\n        var before = new DateTimeOffset(2021, 08, 01, 0, 0, 0, TimeSpan.Zero);\n        using var file = TempFile.Create();\n\n        // Act\n        await new ExportChannelsCommand\n        {\n            Token = Secrets.DiscordToken,\n            ChannelIds = [ChannelIds.DateRangeTestCases],\n            ExportFormat = ExportFormat.Json,\n            OutputPath = file.Path,\n            Before = Snowflake.FromDate(before),\n            After = Snowflake.FromDate(after),\n        }.ExecuteAsync(new FakeConsole());\n\n        // Assert\n        var timestamps = Json.Parse(await File.ReadAllTextAsync(file.Path))\n            .GetProperty(\"messages\")\n            .EnumerateArray()\n            .Select(j => j.GetProperty(\"timestamp\").GetDateTimeOffset())\n            .ToArray();\n\n        timestamps.All(t => t < before && t > after).Should().BeTrue();\n\n        timestamps\n            .Should()\n            .BeEquivalentTo(\n                [\n                    new DateTimeOffset(2021, 07, 24, 13, 49, 13, TimeSpan.Zero),\n                    new DateTimeOffset(2021, 07, 24, 14, 52, 38, TimeSpan.Zero),\n                    new DateTimeOffset(2021, 07, 24, 14, 52, 39, TimeSpan.Zero),\n                    new DateTimeOffset(2021, 07, 24, 14, 52, 40, TimeSpan.Zero),\n                ],\n                o =>\n                    o.Using<DateTimeOffset>(ctx =>\n                            ctx.Subject.Should().BeCloseTo(ctx.Expectation, TimeSpan.FromSeconds(1))\n                        )\n                        .WhenTypeIs<DateTimeOffset>()\n            );\n    }\n\n    [Fact]\n    public async Task I_can_filter_the_export_to_not_include_any_messages()\n    {\n        // Arrange\n        var before = new DateTimeOffset(2020, 08, 01, 0, 0, 0, TimeSpan.Zero);\n        using var file = TempFile.Create();\n\n        // Act\n        await new ExportChannelsCommand\n        {\n            Token = Secrets.DiscordToken,\n            ChannelIds = [ChannelIds.DateRangeTestCases],\n            ExportFormat = ExportFormat.Json,\n            OutputPath = file.Path,\n            Before = Snowflake.FromDate(before),\n        }.ExecuteAsync(new FakeConsole());\n\n        // Assert\n        var timestamps = Json.Parse(await File.ReadAllTextAsync(file.Path))\n            .GetProperty(\"messages\")\n            .EnumerateArray()\n            .Select(j => j.GetProperty(\"timestamp\").GetDateTimeOffset())\n            .ToArray();\n\n        timestamps.Should().BeEmpty();\n    }\n}\n"
  },
  {
    "path": "DiscordChatExporter.Cli.Tests/Specs/FilterSpecs.cs",
    "content": "﻿using System;\nusing System.IO;\nusing System.Linq;\nusing System.Threading.Tasks;\nusing CliFx.Infrastructure;\nusing DiscordChatExporter.Cli.Commands;\nusing DiscordChatExporter.Cli.Tests.Infra;\nusing DiscordChatExporter.Cli.Tests.Utils;\nusing DiscordChatExporter.Core.Exporting;\nusing DiscordChatExporter.Core.Exporting.Filtering;\nusing FluentAssertions;\nusing JsonExtensions;\nusing Xunit;\n\nnamespace DiscordChatExporter.Cli.Tests.Specs;\n\npublic class FilterSpecs\n{\n    [Fact]\n    public async Task I_can_filter_the_export_to_only_include_messages_that_contain_the_specified_text()\n    {\n        // Arrange\n        using var file = TempFile.Create();\n\n        // Act\n        await new ExportChannelsCommand\n        {\n            Token = Secrets.DiscordToken,\n            ChannelIds = [ChannelIds.FilterTestCases],\n            ExportFormat = ExportFormat.Json,\n            OutputPath = file.Path,\n            MessageFilter = MessageFilter.Parse(\"some text\"),\n        }.ExecuteAsync(new FakeConsole());\n\n        // Assert\n        Json.Parse(await File.ReadAllTextAsync(file.Path))\n            .GetProperty(\"messages\")\n            .EnumerateArray()\n            .Select(j => j.GetProperty(\"content\").GetString())\n            .Should()\n            .AllSatisfy(c => c.Contains(\"Some random text\", StringComparison.Ordinal));\n    }\n\n    [Fact]\n    public async Task I_can_filter_the_export_to_only_include_messages_that_were_sent_by_the_specified_author()\n    {\n        // Arrange\n        using var file = TempFile.Create();\n\n        // Act\n        await new ExportChannelsCommand\n        {\n            Token = Secrets.DiscordToken,\n            ChannelIds = [ChannelIds.FilterTestCases],\n            ExportFormat = ExportFormat.Json,\n            OutputPath = file.Path,\n            MessageFilter = MessageFilter.Parse(\"from:Tyrrrz\"),\n        }.ExecuteAsync(new FakeConsole());\n\n        // Assert\n        Json.Parse(await File.ReadAllTextAsync(file.Path))\n            .GetProperty(\"messages\")\n            .EnumerateArray()\n            .Select(j => j.GetProperty(\"author\").GetProperty(\"name\").GetString())\n            .Should()\n            .AllBe(\"tyrrrz\");\n    }\n\n    [Fact]\n    public async Task I_can_filter_the_export_to_only_include_messages_that_contain_images()\n    {\n        // Arrange\n        using var file = TempFile.Create();\n\n        // Act\n        await new ExportChannelsCommand\n        {\n            Token = Secrets.DiscordToken,\n            ChannelIds = [ChannelIds.FilterTestCases],\n            ExportFormat = ExportFormat.Json,\n            OutputPath = file.Path,\n            MessageFilter = MessageFilter.Parse(\"has:image\"),\n        }.ExecuteAsync(new FakeConsole());\n\n        // Assert\n        Json.Parse(await File.ReadAllTextAsync(file.Path))\n            .GetProperty(\"messages\")\n            .EnumerateArray()\n            .Select(j => j.GetProperty(\"content\").GetString())\n            .Should()\n            .AllSatisfy(c => c.Contains(\"This has image\", StringComparison.Ordinal));\n    }\n\n    [Fact]\n    public async Task I_can_filter_the_export_to_only_include_messages_that_have_been_pinned()\n    {\n        // Arrange\n        using var file = TempFile.Create();\n\n        // Act\n        await new ExportChannelsCommand\n        {\n            Token = Secrets.DiscordToken,\n            ChannelIds = [ChannelIds.FilterTestCases],\n            ExportFormat = ExportFormat.Json,\n            OutputPath = file.Path,\n            MessageFilter = MessageFilter.Parse(\"has:pin\"),\n        }.ExecuteAsync(new FakeConsole());\n\n        // Assert\n        Json.Parse(await File.ReadAllTextAsync(file.Path))\n            .GetProperty(\"messages\")\n            .EnumerateArray()\n            .Select(j => j.GetProperty(\"content\").GetString())\n            .Should()\n            .AllSatisfy(c => c.Contains(\"This is pinned\", StringComparison.Ordinal));\n    }\n\n    [Fact]\n    public async Task I_can_filter_the_export_to_only_include_messages_that_contain_guild_invites()\n    {\n        // Arrange\n        using var file = TempFile.Create();\n\n        // Act\n        await new ExportChannelsCommand\n        {\n            Token = Secrets.DiscordToken,\n            ChannelIds = [ChannelIds.FilterTestCases],\n            ExportFormat = ExportFormat.Json,\n            OutputPath = file.Path,\n            MessageFilter = MessageFilter.Parse(\"has:invite\"),\n        }.ExecuteAsync(new FakeConsole());\n\n        // Assert\n        Json.Parse(await File.ReadAllTextAsync(file.Path))\n            .GetProperty(\"messages\")\n            .EnumerateArray()\n            .Select(j => j.GetProperty(\"content\").GetString())\n            .Should()\n            .AllSatisfy(c => c.Contains(\"This has invite\", StringComparison.Ordinal));\n    }\n\n    [Fact]\n    public async Task I_can_filter_the_export_to_only_include_messages_that_contain_the_specified_mention()\n    {\n        // Arrange\n        using var file = TempFile.Create();\n\n        // Act\n        await new ExportChannelsCommand\n        {\n            Token = Secrets.DiscordToken,\n            ChannelIds = [ChannelIds.FilterTestCases],\n            ExportFormat = ExportFormat.Json,\n            OutputPath = file.Path,\n            MessageFilter = MessageFilter.Parse(\"mentions:Tyrrrz\"),\n        }.ExecuteAsync(new FakeConsole());\n\n        // Assert\n        Json.Parse(await File.ReadAllTextAsync(file.Path))\n            .GetProperty(\"messages\")\n            .EnumerateArray()\n            .Select(j => j.GetProperty(\"content\").GetString())\n            .Should()\n            .AllSatisfy(c => c.Contains(\"This has mention\", StringComparison.Ordinal));\n    }\n}\n"
  },
  {
    "path": "DiscordChatExporter.Cli.Tests/Specs/HtmlAttachmentSpecs.cs",
    "content": "﻿using System;\nusing System.Linq;\nusing System.Threading.Tasks;\nusing AngleSharp.Dom;\nusing DiscordChatExporter.Cli.Tests.Infra;\nusing DiscordChatExporter.Core.Discord;\nusing FluentAssertions;\nusing Xunit;\n\nnamespace DiscordChatExporter.Cli.Tests.Specs;\n\npublic class HtmlAttachmentSpecs\n{\n    [Fact]\n    public async Task I_can_export_a_channel_that_contains_a_message_with_a_generic_attachment()\n    {\n        // Act\n        var message = await ExportWrapper.GetMessageAsHtmlAsync(\n            ChannelIds.AttachmentTestCases,\n            Snowflake.Parse(\"885587844989612074\")\n        );\n\n        // Assert\n        message.Text().Should().ContainAll(\"Generic file attachment\", \"Test.txt\", \"11 bytes\");\n\n        message\n            .QuerySelectorAll(\"a\")\n            .Select(e => e.GetAttribute(\"href\"))\n            .Should()\n            .Contain(u => u.Contains(\"Test.txt\", StringComparison.Ordinal));\n    }\n\n    [Fact]\n    public async Task I_can_export_a_channel_that_contains_a_message_with_an_image_attachment()\n    {\n        // Act\n        var message = await ExportWrapper.GetMessageAsHtmlAsync(\n            ChannelIds.AttachmentTestCases,\n            Snowflake.Parse(\"885654862656843786\")\n        );\n\n        // Assert\n        message.Text().Should().Contain(\"Image attachment\");\n\n        message\n            .QuerySelectorAll(\"img\")\n            .Select(e => e.GetAttribute(\"src\"))\n            .Should()\n            .Contain(u => u.Contains(\"bird-thumbnail.png\", StringComparison.Ordinal));\n    }\n\n    [Fact]\n    public async Task I_can_export_a_channel_that_contains_a_message_with_a_video_attachment()\n    {\n        // https://github.com/Tyrrrz/DiscordChatExporter/issues/333\n\n        // Act\n        var message = await ExportWrapper.GetMessageAsHtmlAsync(\n            ChannelIds.AttachmentTestCases,\n            Snowflake.Parse(\"885655761919836171\")\n        );\n\n        // Assert\n        message.Text().Should().Contain(\"Video attachment\");\n\n        var videoUrl = message.QuerySelector(\"video source\")?.GetAttribute(\"src\");\n        videoUrl\n            .Should()\n            .StartWith(\n                \"https://cdn.discordapp.com/attachments/885587741654536192/885655761512968233/file_example_MP4_640_3MG.mp4\"\n            );\n    }\n\n    [Fact]\n    public async Task I_can_export_a_channel_that_contains_a_message_with_an_audio_attachment()\n    {\n        // https://github.com/Tyrrrz/DiscordChatExporter/issues/333\n\n        // Act\n        var message = await ExportWrapper.GetMessageAsHtmlAsync(\n            ChannelIds.AttachmentTestCases,\n            Snowflake.Parse(\"885656175620808734\")\n        );\n\n        // Assert\n        message.Text().Should().Contain(\"Audio attachment\");\n\n        var audioUrl = message.QuerySelector(\"audio source\")?.GetAttribute(\"src\");\n        audioUrl\n            .Should()\n            .StartWith(\n                \"https://cdn.discordapp.com/attachments/885587741654536192/885656175348187146/file_example_MP3_1MG.mp3\"\n            );\n    }\n}\n"
  },
  {
    "path": "DiscordChatExporter.Cli.Tests/Specs/HtmlContentSpecs.cs",
    "content": "﻿using System.IO;\nusing System.Linq;\nusing System.Threading.Tasks;\nusing AngleSharp.Dom;\nusing CliFx.Infrastructure;\nusing DiscordChatExporter.Cli.Commands;\nusing DiscordChatExporter.Cli.Tests.Infra;\nusing DiscordChatExporter.Cli.Tests.Utils;\nusing DiscordChatExporter.Core.Exporting;\nusing FluentAssertions;\nusing Xunit;\n\nnamespace DiscordChatExporter.Cli.Tests.Specs;\n\npublic class HtmlContentSpecs\n{\n    [Fact]\n    public async Task I_can_export_a_channel_in_the_HTML_format()\n    {\n        // Act\n        var messages = await ExportWrapper.GetMessagesAsHtmlAsync(ChannelIds.DateRangeTestCases);\n\n        // Assert\n        messages\n            .Select(e => e.GetAttribute(\"data-message-id\"))\n            .Should()\n            .Equal(\n                \"866674314627121232\",\n                \"866710679758045195\",\n                \"866732113319428096\",\n                \"868490009366396958\",\n                \"868505966528835604\",\n                \"868505969821364245\",\n                \"868505973294268457\",\n                \"885169254029213696\"\n            );\n\n        messages\n            .SelectMany(e => e.Text())\n            .Should()\n            .ContainInOrder(\n                \"Hello world\",\n                \"Goodbye world\",\n                \"Foo bar\",\n                \"Hurdle Durdle\",\n                \"One\",\n                \"Two\",\n                \"Three\",\n                \"Yeet\"\n            );\n    }\n\n    [Fact]\n    public async Task I_can_export_a_channel_in_the_HTML_format_in_the_reverse_order()\n    {\n        // Arrange\n        using var file = TempFile.Create();\n\n        // Act\n        await new ExportChannelsCommand\n        {\n            Token = Secrets.DiscordToken,\n            ChannelIds = [ChannelIds.DateRangeTestCases],\n            ExportFormat = ExportFormat.HtmlDark,\n            OutputPath = file.Path,\n            Locale = \"en-US\",\n            IsUtcNormalizationEnabled = true,\n            IsReverseMessageOrder = true,\n        }.ExecuteAsync(new FakeConsole());\n\n        var document = Html.Parse(await File.ReadAllTextAsync(file.Path));\n        var messages = document.QuerySelectorAll(\"[data-message-id]\").ToArray();\n\n        // Assert\n        messages\n            .Select(e => e.GetAttribute(\"data-message-id\"))\n            .Should()\n            .Equal(\n                \"885169254029213696\",\n                \"868505973294268457\",\n                \"868505969821364245\",\n                \"868505966528835604\",\n                \"868490009366396958\",\n                \"866732113319428096\",\n                \"866710679758045195\",\n                \"866674314627121232\"\n            );\n    }\n}\n"
  },
  {
    "path": "DiscordChatExporter.Cli.Tests/Specs/HtmlEmbedSpecs.cs",
    "content": "﻿using System;\nusing System.Linq;\nusing System.Threading.Tasks;\nusing AngleSharp.Dom;\nusing DiscordChatExporter.Cli.Tests.Infra;\nusing DiscordChatExporter.Core.Discord;\nusing DiscordChatExporter.Core.Utils.Extensions;\nusing FluentAssertions;\nusing Xunit;\n\nnamespace DiscordChatExporter.Cli.Tests.Specs;\n\npublic class HtmlEmbedSpecs\n{\n    [Fact]\n    public async Task I_can_export_a_channel_that_contains_a_message_with_a_rich_embed()\n    {\n        // Act\n        var message = await ExportWrapper.GetMessageAsHtmlAsync(\n            ChannelIds.EmbedTestCases,\n            Snowflake.Parse(\"866769910729146400\")\n        );\n\n        // Assert\n        message\n            .Text()\n            .Should()\n            .ContainAll(\n                \"Embed author\",\n                \"Embed title\",\n                \"Embed description\",\n                \"Field 1\",\n                \"Value 1\",\n                \"Field 2\",\n                \"Value 2\",\n                \"Field 3\",\n                \"Value 3\",\n                \"Embed footer\"\n            );\n    }\n\n    [Fact]\n    public async Task I_can_export_a_channel_that_contains_a_message_with_an_image_embed()\n    {\n        // https://github.com/Tyrrrz/DiscordChatExporter/issues/537\n\n        // Act\n        var message = await ExportWrapper.GetMessageAsHtmlAsync(\n            ChannelIds.EmbedTestCases,\n            Snowflake.Parse(\"991768701126852638\")\n        );\n\n        // Assert\n        message\n            .QuerySelectorAll(\"img\")\n            .Select(e => e.GetAttribute(\"src\"))\n            .WhereNotNull()\n            .Where(s => s.Contains(\"f8w05ja8s4e61.png\", StringComparison.Ordinal))\n            .Should()\n            .ContainSingle();\n    }\n\n    [Fact]\n    public async Task I_can_export_a_channel_that_contains_a_message_with_an_image_embed_and_the_text_is_hidden_if_it_only_contains_the_image_link()\n    {\n        // https://github.com/Tyrrrz/DiscordChatExporter/issues/682\n\n        // Act\n        var message = await ExportWrapper.GetMessageAsHtmlAsync(\n            ChannelIds.EmbedTestCases,\n            Snowflake.Parse(\"991768701126852638\")\n        );\n\n        // Assert\n        var content = message.QuerySelector(\".chatlog__content\")?.Text();\n        content.Should().BeNullOrEmpty();\n    }\n\n    [Fact]\n    public async Task I_can_export_a_channel_that_contains_a_message_with_a_video_embed()\n    {\n        // Act\n        var message = await ExportWrapper.GetMessageAsHtmlAsync(\n            ChannelIds.EmbedTestCases,\n            Snowflake.Parse(\"1083751036596002856\")\n        );\n\n        // Assert\n        message\n            .QuerySelectorAll(\"source\")\n            .Select(e => e.GetAttribute(\"src\"))\n            .WhereNotNull()\n            .Where(s =>\n                s.Contains(\n                    \"i_am_currently_feeling_slight_displeasure_of_what_you_have_just_sent_lqrem.mp4\",\n                    StringComparison.Ordinal\n                )\n            )\n            .Should()\n            .ContainSingle();\n    }\n\n    [Fact]\n    public async Task I_can_export_a_channel_that_contains_a_message_with_a_GIFV_embed()\n    {\n        // Act\n        var message = await ExportWrapper.GetMessageAsHtmlAsync(\n            ChannelIds.EmbedTestCases,\n            Snowflake.Parse(\"1019234520349814794\")\n        );\n\n        // Assert\n        message\n            .QuerySelectorAll(\"source\")\n            .Select(e => e.GetAttribute(\"src\"))\n            .WhereNotNull()\n            .Where(s => s.Contains(\"tooncasm-test-copy.mp4\", StringComparison.Ordinal))\n            .Should()\n            .ContainSingle();\n    }\n\n    [Fact]\n    public async Task I_can_export_a_channel_that_contains_a_message_with_a_GIFV_embed_and_the_text_is_hidden_if_it_only_contains_the_video_link()\n    {\n        // Act\n        var message = await ExportWrapper.GetMessageAsHtmlAsync(\n            ChannelIds.EmbedTestCases,\n            Snowflake.Parse(\"1019234520349814794\")\n        );\n\n        // Assert\n        var content = message.QuerySelector(\".chatlog__content\")?.Text();\n        content.Should().BeNullOrEmpty();\n    }\n\n    [Fact]\n    public async Task I_can_export_a_channel_that_contains_a_message_with_a_Spotify_track_embed()\n    {\n        // https://github.com/Tyrrrz/DiscordChatExporter/issues/657\n\n        // Act\n        var message = await ExportWrapper.GetMessageAsHtmlAsync(\n            ChannelIds.EmbedTestCases,\n            Snowflake.Parse(\"867886632203976775\")\n        );\n\n        // Assert\n        var iframeUrl = message.QuerySelector(\"iframe\")?.GetAttribute(\"src\");\n        iframeUrl.Should().StartWith(\"https://open.spotify.com/embed/track/1LHZMWefF9502NPfArRfvP\");\n    }\n\n    [Fact(Skip = \"Twitch does not allow embeds from inside local HTML files\")]\n    public async Task I_can_export_a_channel_that_contains_a_message_with_a_Twitch_clip_embed()\n    {\n        // https://github.com/Tyrrrz/DiscordChatExporter/issues/1196\n\n        // Act\n        var message = await ExportWrapper.GetMessageAsHtmlAsync(\n            ChannelIds.EmbedTestCases,\n            Snowflake.Parse(\"1207002986128216074\")\n        );\n\n        // Assert\n        var iframeUrl = message.QuerySelector(\"iframe\")?.GetAttribute(\"src\");\n        iframeUrl\n            .Should()\n            .StartWith(\n                \"https://clips.twitch.tv/embed?clip=SpicyMildCiderThisIsSparta--PQhbllrvej_Ee7v\"\n            );\n    }\n\n    [Fact]\n    public async Task I_can_export_a_channel_that_contains_a_message_with_a_YouTube_video_embed()\n    {\n        // https://github.com/Tyrrrz/DiscordChatExporter/issues/570\n\n        // Act\n        var message = await ExportWrapper.GetMessageAsHtmlAsync(\n            ChannelIds.EmbedTestCases,\n            Snowflake.Parse(\"866472508588294165\")\n        );\n\n        // Assert\n        // Check that the YouTube video thumbnail image exists with the correct video ID\n        var youtubeThumbnailSrc = message\n            .QuerySelectorAll(\"img\")\n            .Select(e => e.GetAttribute(\"src\"))\n            .WhereNotNull()\n            .FirstOrDefault(s => s.Contains(\"qOWW4OlgbvE\", StringComparison.Ordinal));\n\n        youtubeThumbnailSrc.Should().NotBeNull();\n    }\n\n    [Fact]\n    public async Task I_can_export_a_channel_that_contains_a_message_with_a_Twitter_post_embed_that_includes_multiple_images()\n    {\n        // https://github.com/Tyrrrz/DiscordChatExporter/issues/695\n\n        // Act\n        var message = await ExportWrapper.GetMessageAsHtmlAsync(\n            ChannelIds.EmbedTestCases,\n            Snowflake.Parse(\"991757444017557665\")\n        );\n\n        // Assert\n        var imageUrls = message\n            .QuerySelectorAll(\"img\")\n            .Select(e => e.GetAttribute(\"src\"))\n            .ToArray();\n\n        imageUrls\n            .Should()\n            .Contain(u =>\n                u.EndsWith(\n                    \"https/pbs.twimg.com/media/FVYIzYPWAAAMBqZ.png\",\n                    StringComparison.Ordinal\n                )\n            );\n\n        imageUrls\n            .Should()\n            .Contain(u =>\n                u.EndsWith(\n                    \"https/pbs.twimg.com/media/FVYJBWJWAAMNAx2.png\",\n                    StringComparison.Ordinal\n                )\n            );\n\n        imageUrls\n            .Should()\n            .Contain(u =>\n                u.EndsWith(\n                    \"https/pbs.twimg.com/media/FVYJHiRX0AANZcz.png\",\n                    StringComparison.Ordinal\n                )\n            );\n\n        imageUrls\n            .Should()\n            .Contain(u =>\n                u.EndsWith(\n                    \"https/pbs.twimg.com/media/FVYJNZNXwAAPnVG.png\",\n                    StringComparison.Ordinal\n                )\n            );\n\n        message.QuerySelectorAll(\".chatlog__embed\").Should().ContainSingle();\n    }\n\n    [Fact]\n    public async Task I_can_export_a_channel_that_contains_a_message_with_a_guild_invite()\n    {\n        // https://github.com/Tyrrrz/DiscordChatExporter/issues/649\n\n        // Act\n        var message = await ExportWrapper.GetMessageAsHtmlAsync(\n            ChannelIds.EmbedTestCases,\n            Snowflake.Parse(\"1075116548966064128\")\n        );\n\n        // Assert\n        message.Text().Should().Contain(\"DiscordChatExporter Test Server\");\n    }\n}\n"
  },
  {
    "path": "DiscordChatExporter.Cli.Tests/Specs/HtmlForwardSpecs.cs",
    "content": "﻿using System.Threading.Tasks;\nusing AngleSharp.Dom;\nusing DiscordChatExporter.Cli.Tests.Infra;\nusing DiscordChatExporter.Cli.Tests.Utils.Extensions;\nusing DiscordChatExporter.Core.Discord;\nusing FluentAssertions;\nusing Xunit;\n\nnamespace DiscordChatExporter.Cli.Tests.Specs;\n\npublic class HtmlForwardSpecs\n{\n    [Fact]\n    public async Task I_can_export_a_channel_that_contains_a_forwarded_message()\n    {\n        // Act\n        var message = await ExportWrapper.GetMessageAsHtmlAsync(\n            ChannelIds.ForwardTestCases,\n            Snowflake.Parse(\"1455202427115536514\")\n        );\n\n        // Assert\n        message\n            .Text()\n            .ReplaceWhiteSpace()\n            .Should()\n            .ContainAll(\"Forwarded\", @\"¯\\_(ツ)_/¯\", \"12/29/2025 2:14 PM\");\n    }\n}\n"
  },
  {
    "path": "DiscordChatExporter.Cli.Tests/Specs/HtmlGroupingSpecs.cs",
    "content": "﻿using System.IO;\nusing System.Linq;\nusing System.Threading.Tasks;\nusing AngleSharp.Dom;\nusing CliFx.Infrastructure;\nusing DiscordChatExporter.Cli.Commands;\nusing DiscordChatExporter.Cli.Tests.Infra;\nusing DiscordChatExporter.Cli.Tests.Utils;\nusing DiscordChatExporter.Core.Exporting;\nusing FluentAssertions;\nusing Xunit;\n\nnamespace DiscordChatExporter.Cli.Tests.Specs;\n\npublic class HtmlGroupingSpecs\n{\n    [Fact]\n    public async Task I_can_export_a_channel_and_the_messages_are_grouped_according_to_their_author_and_timestamps()\n    {\n        // https://github.com/Tyrrrz/DiscordChatExporter/issues/152\n\n        // Arrange\n        using var file = TempFile.Create();\n\n        // Act\n        await new ExportChannelsCommand\n        {\n            Token = Secrets.DiscordToken,\n            ChannelIds = [ChannelIds.GroupingTestCases],\n            ExportFormat = ExportFormat.HtmlDark,\n            OutputPath = file.Path,\n        }.ExecuteAsync(new FakeConsole());\n\n        // Assert\n        var messageGroups = Html.Parse(await File.ReadAllTextAsync(file.Path))\n            .QuerySelectorAll(\".chatlog__message-group\");\n\n        messageGroups.Should().HaveCount(2);\n\n        messageGroups[0]\n            .QuerySelectorAll(\".chatlog__content\")\n            .Select(e => e.Text())\n            .Should()\n            .ContainInOrder(\n                \"First\",\n                \"Second\",\n                \"Third\",\n                \"Fourth\",\n                \"Fifth\",\n                \"Sixth\",\n                \"Seventh\",\n                \"Eighth\",\n                \"Ninth\",\n                \"Tenth\"\n            );\n\n        messageGroups[1]\n            .QuerySelectorAll(\".chatlog__content\")\n            .Select(e => e.Text())\n            .Should()\n            .ContainInOrder(\"Eleventh\", \"Twelveth\", \"Thirteenth\", \"Fourteenth\", \"Fifteenth\");\n    }\n}\n"
  },
  {
    "path": "DiscordChatExporter.Cli.Tests/Specs/HtmlMarkdownSpecs.cs",
    "content": "﻿using System.Threading.Tasks;\nusing AngleSharp.Dom;\nusing DiscordChatExporter.Cli.Tests.Infra;\nusing DiscordChatExporter.Cli.Tests.Utils.Extensions;\nusing DiscordChatExporter.Core.Discord;\nusing FluentAssertions;\nusing Xunit;\n\nnamespace DiscordChatExporter.Cli.Tests.Specs;\n\npublic class HtmlMarkdownSpecs\n{\n    [Fact]\n    public async Task I_can_export_a_channel_that_contains_a_message_with_a_timestamp_marker()\n    {\n        // Act\n        var message = await ExportWrapper.GetMessageAsHtmlAsync(\n            ChannelIds.MarkdownTestCases,\n            Snowflake.Parse(\"1074323136411078787\")\n        );\n\n        // Assert\n        message\n            .Text()\n            .ReplaceWhiteSpace()\n            .Should()\n            .Contain(\"Default timestamp: 2/12/2023 1:36 PM\");\n\n        message.InnerHtml.ReplaceWhiteSpace().Should().Contain(\"Sunday, February 12, 2023 1:36 PM\");\n    }\n\n    [Fact]\n    public async Task I_can_export_a_channel_that_contains_a_message_with_a_timestamp_marker_in_the_short_format()\n    {\n        // Act\n        var message = await ExportWrapper.GetMessageAsHtmlAsync(\n            ChannelIds.MarkdownTestCases,\n            Snowflake.Parse(\"1074323205268967596\")\n        );\n\n        // Assert\n        message.Text().ReplaceWhiteSpace().Should().Contain(\"Short time timestamp: 1:36 PM\");\n        message.InnerHtml.ReplaceWhiteSpace().Should().Contain(\"Sunday, February 12, 2023 1:36 PM\");\n    }\n\n    [Fact]\n    public async Task I_can_export_a_channel_that_contains_a_message_with_a_timestamp_marker_in_the_long_format()\n    {\n        // Act\n        var message = await ExportWrapper.GetMessageAsHtmlAsync(\n            ChannelIds.MarkdownTestCases,\n            Snowflake.Parse(\"1074323235342139483\")\n        );\n\n        // Assert\n        message.Text().ReplaceWhiteSpace().Should().Contain(\"Long time timestamp: 1:36:12 PM\");\n        message.InnerHtml.ReplaceWhiteSpace().Should().Contain(\"Sunday, February 12, 2023 1:36 PM\");\n    }\n\n    [Fact]\n    public async Task I_can_export_a_channel_that_contains_a_message_with_a_timestamp_marker_in_the_short_date_format()\n    {\n        // Act\n        var message = await ExportWrapper.GetMessageAsHtmlAsync(\n            ChannelIds.MarkdownTestCases,\n            Snowflake.Parse(\"1074323326727634984\")\n        );\n\n        // Assert\n        message.Text().ReplaceWhiteSpace().Should().Contain(\"Short date timestamp: 2/12/2023\");\n        message.InnerHtml.ReplaceWhiteSpace().Should().Contain(\"Sunday, February 12, 2023 1:36 PM\");\n    }\n\n    [Fact]\n    public async Task I_can_export_a_channel_that_contains_a_message_with_a_timestamp_marker_in_the_long_date_format()\n    {\n        // Act\n        var message = await ExportWrapper.GetMessageAsHtmlAsync(\n            ChannelIds.MarkdownTestCases,\n            Snowflake.Parse(\"1074323350731640863\")\n        );\n\n        // Assert\n        message\n            .Text()\n            .ReplaceWhiteSpace()\n            .Should()\n            .Contain(\"Long date timestamp: Sunday, February 12, 2023\");\n\n        message.InnerHtml.ReplaceWhiteSpace().Should().Contain(\"Sunday, February 12, 2023 1:36 PM\");\n    }\n\n    [Fact]\n    public async Task I_can_export_a_channel_that_contains_a_message_with_a_timestamp_marker_in_the_full_format()\n    {\n        // Act\n        var message = await ExportWrapper.GetMessageAsHtmlAsync(\n            ChannelIds.MarkdownTestCases,\n            Snowflake.Parse(\"1074323374379118593\")\n        );\n\n        // Assert\n        message\n            .Text()\n            .ReplaceWhiteSpace()\n            .Should()\n            .Contain(\"Full timestamp: Sunday, February 12, 2023 1:36 PM\");\n\n        message.InnerHtml.ReplaceWhiteSpace().Should().Contain(\"Sunday, February 12, 2023 1:36 PM\");\n    }\n\n    [Fact]\n    public async Task I_can_export_a_channel_that_contains_a_message_with_a_timestamp_marker_in_the_full_long_format()\n    {\n        // Act\n        var message = await ExportWrapper.GetMessageAsHtmlAsync(\n            ChannelIds.MarkdownTestCases,\n            Snowflake.Parse(\"1074323409095376947\")\n        );\n\n        // Assert\n        message\n            .Text()\n            .ReplaceWhiteSpace()\n            .Should()\n            .Contain(\"Full long timestamp: Sunday, February 12, 2023 1:36:12 PM\");\n\n        message.InnerHtml.ReplaceWhiteSpace().Should().Contain(\"Sunday, February 12, 2023 1:36 PM\");\n    }\n\n    [Fact]\n    public async Task I_can_export_a_channel_that_contains_a_message_with_a_timestamp_marker_in_the_relative_format()\n    {\n        // Act\n        var message = await ExportWrapper.GetMessageAsHtmlAsync(\n            ChannelIds.MarkdownTestCases,\n            Snowflake.Parse(\"1074323436853285004\")\n        );\n\n        // Assert\n        message\n            .Text()\n            .ReplaceWhiteSpace()\n            .Should()\n            .Contain(\"Relative timestamp: 2/12/2023 1:36 PM\");\n\n        message.InnerHtml.ReplaceWhiteSpace().Should().Contain(\"Sunday, February 12, 2023 1:36 PM\");\n    }\n\n    [Fact]\n    public async Task I_can_export_a_channel_that_contains_a_message_with_an_invalid_timestamp_marker()\n    {\n        // Act\n        var message = await ExportWrapper.GetMessageAsHtmlAsync(\n            ChannelIds.MarkdownTestCases,\n            Snowflake.Parse(\"1074328534409019563\")\n        );\n\n        // Assert\n        message.Text().Should().Contain(\"Invalid timestamp: Invalid date\");\n    }\n}\n"
  },
  {
    "path": "DiscordChatExporter.Cli.Tests/Specs/HtmlMentionSpecs.cs",
    "content": "﻿using System.Threading.Tasks;\nusing AngleSharp.Dom;\nusing DiscordChatExporter.Cli.Tests.Infra;\nusing DiscordChatExporter.Core.Discord;\nusing FluentAssertions;\nusing Xunit;\n\nnamespace DiscordChatExporter.Cli.Tests.Specs;\n\npublic class HtmlMentionSpecs\n{\n    [Fact]\n    public async Task I_can_export_a_channel_that_contains_a_message_with_a_user_mention()\n    {\n        // Act\n        var message = await ExportWrapper.GetMessageAsHtmlAsync(\n            ChannelIds.MentionTestCases,\n            Snowflake.Parse(\"866458840245076028\")\n        );\n\n        // Assert\n        message.Text().Should().Contain(\"User mention: @Tyrrrz\");\n        message.InnerHtml.Should().Contain(\"tyrrrz\");\n    }\n\n    [Fact]\n    public async Task I_can_export_a_channel_that_contains_a_message_with_a_text_channel_mention()\n    {\n        // Act\n        var message = await ExportWrapper.GetMessageAsHtmlAsync(\n            ChannelIds.MentionTestCases,\n            Snowflake.Parse(\"866459040480624680\")\n        );\n\n        // Assert\n        message.Text().Should().Contain(\"Text channel mention: #mention-tests\");\n    }\n\n    [Fact]\n    public async Task I_can_export_a_channel_that_contains_a_message_with_a_voice_channel_mention()\n    {\n        // Act\n        var message = await ExportWrapper.GetMessageAsHtmlAsync(\n            ChannelIds.MentionTestCases,\n            Snowflake.Parse(\"866459175462633503\")\n        );\n\n        // Assert\n        message.Text().Should().Contain(\"Voice channel mention: 🔊general\");\n    }\n\n    [Fact]\n    public async Task I_can_export_a_channel_that_contains_a_message_with_a_role_mention()\n    {\n        // Act\n        var message = await ExportWrapper.GetMessageAsHtmlAsync(\n            ChannelIds.MentionTestCases,\n            Snowflake.Parse(\"866459254693429258\")\n        );\n\n        // Assert\n        message.Text().Should().Contain(\"Role mention: @Role 1\");\n    }\n\n    [Fact]\n    public async Task I_can_export_a_channel_that_contains_a_message_with_a_thread_mention()\n    {\n        // Act\n        var message = await ExportWrapper.GetMessageAsHtmlAsync(\n            ChannelIds.MentionTestCases,\n            Snowflake.Parse(\"1474874276828938290\")\n        );\n\n        // Assert\n        message.Text().Should().Contain(\"Thread mention: #Thread starting message\");\n    }\n}\n"
  },
  {
    "path": "DiscordChatExporter.Cli.Tests/Specs/HtmlReplySpecs.cs",
    "content": "﻿using System.Threading.Tasks;\nusing AngleSharp.Dom;\nusing DiscordChatExporter.Cli.Tests.Infra;\nusing DiscordChatExporter.Core.Discord;\nusing FluentAssertions;\nusing Xunit;\n\nnamespace DiscordChatExporter.Cli.Tests.Specs;\n\npublic class HtmlReplySpecs\n{\n    [Fact]\n    public async Task I_can_export_a_channel_that_contains_a_message_that_replies_to_another_message()\n    {\n        // Act\n        var message = await ExportWrapper.GetMessageAsHtmlAsync(\n            ChannelIds.ReplyTestCases,\n            Snowflake.Parse(\"866460738239725598\")\n        );\n\n        // Assert\n        message.Text().Should().Contain(\"reply to original\");\n        message.QuerySelector(\".chatlog__reply-link\")?.Text().Should().Contain(\"original\");\n    }\n\n    [Fact]\n    public async Task I_can_export_a_channel_that_contains_a_message_that_replies_to_a_deleted_message()\n    {\n        // https://github.com/Tyrrrz/DiscordChatExporter/issues/645\n\n        // Act\n        var message = await ExportWrapper.GetMessageAsHtmlAsync(\n            ChannelIds.ReplyTestCases,\n            Snowflake.Parse(\"866460975388819486\")\n        );\n\n        // Assert\n        message.Text().Should().Contain(\"reply to deleted\");\n        message\n            .QuerySelector(\".chatlog__reply-link\")\n            ?.Text()\n            .Should()\n            .Contain(\"Original message was deleted or could not be loaded.\");\n    }\n\n    [Fact]\n    public async Task I_can_export_a_channel_that_contains_a_message_that_replies_to_an_empty_message_with_an_attachment()\n    {\n        // https://github.com/Tyrrrz/DiscordChatExporter/issues/634\n\n        // Act\n        var message = await ExportWrapper.GetMessageAsHtmlAsync(\n            ChannelIds.ReplyTestCases,\n            Snowflake.Parse(\"866462470335627294\")\n        );\n\n        // Assert\n        message.Text().Should().Contain(\"reply to attachment\");\n        message\n            .QuerySelector(\".chatlog__reply-link\")\n            ?.Text()\n            .Should()\n            .Contain(\"Click to see attachment\");\n    }\n\n    [Fact]\n    public async Task I_can_export_a_channel_that_contains_a_message_that_replies_to_an_interaction()\n    {\n        // https://github.com/Tyrrrz/DiscordChatExporter/issues/569\n\n        // Act\n        var message = await ExportWrapper.GetMessageAsHtmlAsync(\n            ChannelIds.ReplyTestCases,\n            Snowflake.Parse(\"1075152916417085492\")\n        );\n\n        // Assert\n        message.Text().Should().Contain(\"used /poll\");\n    }\n\n    [Fact]\n    public async Task I_can_export_a_channel_that_contains_a_message_cross_posted_from_another_guild()\n    {\n        // https://github.com/Tyrrrz/DiscordChatExporter/issues/633\n\n        // Act\n        var message = await ExportWrapper.GetMessageAsHtmlAsync(\n            ChannelIds.ReplyTestCases,\n            Snowflake.Parse(\"1072165330853576876\")\n        );\n\n        // Assert\n        message\n            .Text()\n            .Should()\n            .Contain(\"This is a test message from an announcement channel on another server\");\n        message.Text().Should().Contain(\"SERVER\");\n        message.QuerySelector(\".chatlog__reply-link\").Should().BeNull();\n    }\n}\n"
  },
  {
    "path": "DiscordChatExporter.Cli.Tests/Specs/HtmlStickerSpecs.cs",
    "content": "﻿using System.Threading.Tasks;\nusing DiscordChatExporter.Cli.Tests.Infra;\nusing DiscordChatExporter.Core.Discord;\nusing FluentAssertions;\nusing Xunit;\n\nnamespace DiscordChatExporter.Cli.Tests.Specs;\n\npublic class HtmlStickerSpecs\n{\n    [Fact]\n    public async Task I_can_export_a_channel_that_contains_a_message_with_a_PNG_sticker()\n    {\n        // Act\n        var message = await ExportWrapper.GetMessageAsHtmlAsync(\n            ChannelIds.StickerTestCases,\n            Snowflake.Parse(\"939670623158943754\")\n        );\n\n        // Assert\n        var stickerUrl = message.QuerySelector(\"[title='rock'] img\")?.GetAttribute(\"src\");\n        stickerUrl.Should().StartWith(\"https://cdn.discordapp.com/stickers/904215665597120572.png\");\n    }\n\n    [Fact]\n    public async Task I_can_export_a_channel_that_contains_a_message_with_a_Lottie_sticker()\n    {\n        // Act\n        var message = await ExportWrapper.GetMessageAsHtmlAsync(\n            ChannelIds.StickerTestCases,\n            Snowflake.Parse(\"939670526517997590\")\n        );\n\n        // Assert\n        var stickerUrl = message\n            .QuerySelector(\"[title='Yikes'] [data-source]\")\n            ?.GetAttribute(\"data-source\");\n\n        stickerUrl\n            .Should()\n            .StartWith(\"https://cdn.discordapp.com/stickers/816087132447178774.json\");\n    }\n}\n"
  },
  {
    "path": "DiscordChatExporter.Cli.Tests/Specs/JsonAttachmentSpecs.cs",
    "content": "﻿using System.Linq;\nusing System.Threading.Tasks;\nusing DiscordChatExporter.Cli.Tests.Infra;\nusing DiscordChatExporter.Core.Discord;\nusing FluentAssertions;\nusing Xunit;\n\nnamespace DiscordChatExporter.Cli.Tests.Specs;\n\npublic class JsonAttachmentSpecs\n{\n    [Fact]\n    public async Task I_can_export_a_channel_that_contains_a_message_with_a_generic_attachment()\n    {\n        // Act\n        var message = await ExportWrapper.GetMessageAsJsonAsync(\n            ChannelIds.AttachmentTestCases,\n            Snowflake.Parse(\"885587844989612074\")\n        );\n\n        // Assert\n        message.GetProperty(\"content\").GetString().Should().Be(\"Generic file attachment\");\n\n        var attachments = message.GetProperty(\"attachments\").EnumerateArray().ToArray();\n        attachments.Should().HaveCount(1);\n\n        attachments[0]\n            .GetProperty(\"url\")\n            .GetString()\n            .Should()\n            .StartWith(\n                \"https://cdn.discordapp.com/attachments/885587741654536192/885587844964417596/Test.txt\"\n            );\n\n        attachments[0].GetProperty(\"fileName\").GetString().Should().Be(\"Test.txt\");\n        attachments[0].GetProperty(\"fileSizeBytes\").GetInt64().Should().Be(11);\n    }\n\n    [Fact]\n    public async Task I_can_export_a_channel_that_contains_a_message_with_an_image_attachment()\n    {\n        // Act\n        var message = await ExportWrapper.GetMessageAsJsonAsync(\n            ChannelIds.AttachmentTestCases,\n            Snowflake.Parse(\"885654862656843786\")\n        );\n\n        // Assert\n        message.GetProperty(\"content\").GetString().Should().Be(\"Image attachment\");\n\n        var attachments = message.GetProperty(\"attachments\").EnumerateArray().ToArray();\n        attachments.Should().HaveCount(1);\n\n        attachments[0]\n            .GetProperty(\"url\")\n            .GetString()\n            .Should()\n            .StartWith(\n                \"https://cdn.discordapp.com/attachments/885587741654536192/885654862430359613/bird-thumbnail.png\"\n            );\n\n        attachments[0].GetProperty(\"fileName\").GetString().Should().Be(\"bird-thumbnail.png\");\n        attachments[0].GetProperty(\"fileSizeBytes\").GetInt64().Should().Be(466335);\n    }\n\n    [Fact]\n    public async Task I_can_export_a_channel_that_contains_a_message_with_a_video_attachment()\n    {\n        // Act\n        var message = await ExportWrapper.GetMessageAsJsonAsync(\n            ChannelIds.AttachmentTestCases,\n            Snowflake.Parse(\"885655761919836171\")\n        );\n\n        // Assert\n        message.GetProperty(\"content\").GetString().Should().Be(\"Video attachment\");\n\n        var attachments = message.GetProperty(\"attachments\").EnumerateArray().ToArray();\n        attachments.Should().HaveCount(1);\n\n        attachments[0]\n            .GetProperty(\"url\")\n            .GetString()\n            .Should()\n            .StartWith(\n                \"https://cdn.discordapp.com/attachments/885587741654536192/885655761512968233/file_example_MP4_640_3MG.mp4\"\n            );\n\n        attachments[0]\n            .GetProperty(\"fileName\")\n            .GetString()\n            .Should()\n            .Be(\"file_example_MP4_640_3MG.mp4\");\n\n        attachments[0].GetProperty(\"fileSizeBytes\").GetInt64().Should().Be(3114374);\n    }\n\n    [Fact]\n    public async Task I_can_export_a_channel_that_contains_a_message_with_an_audio_attachment()\n    {\n        // Act\n        var message = await ExportWrapper.GetMessageAsJsonAsync(\n            ChannelIds.AttachmentTestCases,\n            Snowflake.Parse(\"885656175620808734\")\n        );\n\n        // Assert\n        message.GetProperty(\"content\").GetString().Should().Be(\"Audio attachment\");\n\n        var attachments = message.GetProperty(\"attachments\").EnumerateArray().ToArray();\n        attachments.Should().HaveCount(1);\n\n        attachments[0]\n            .GetProperty(\"url\")\n            .GetString()\n            .Should()\n            .StartWith(\n                \"https://cdn.discordapp.com/attachments/885587741654536192/885656175348187146/file_example_MP3_1MG.mp3\"\n            );\n\n        attachments[0].GetProperty(\"fileName\").GetString().Should().Be(\"file_example_MP3_1MG.mp3\");\n        attachments[0].GetProperty(\"fileSizeBytes\").GetInt64().Should().Be(1087849);\n    }\n}\n"
  },
  {
    "path": "DiscordChatExporter.Cli.Tests/Specs/JsonContentSpecs.cs",
    "content": "﻿using System.IO;\nusing System.Linq;\nusing System.Threading.Tasks;\nusing CliFx.Infrastructure;\nusing DiscordChatExporter.Cli.Commands;\nusing DiscordChatExporter.Cli.Tests.Infra;\nusing DiscordChatExporter.Cli.Tests.Utils;\nusing DiscordChatExporter.Core.Exporting;\nusing FluentAssertions;\nusing JsonExtensions;\nusing Xunit;\n\nnamespace DiscordChatExporter.Cli.Tests.Specs;\n\npublic class JsonContentSpecs\n{\n    [Fact]\n    public async Task I_can_export_a_channel_in_the_JSON_format()\n    {\n        // Act\n        var messages = await ExportWrapper.GetMessagesAsJsonAsync(ChannelIds.DateRangeTestCases);\n\n        // Assert\n        messages\n            .Select(j => j.GetProperty(\"id\").GetString())\n            .Should()\n            .Equal(\n                \"866674314627121232\",\n                \"866710679758045195\",\n                \"866732113319428096\",\n                \"868490009366396958\",\n                \"868505966528835604\",\n                \"868505969821364245\",\n                \"868505973294268457\",\n                \"885169254029213696\"\n            );\n\n        messages\n            .Select(j => j.GetProperty(\"content\").GetString())\n            .Should()\n            .Equal(\n                \"Hello world\",\n                \"Goodbye world\",\n                \"Foo bar\",\n                \"Hurdle Durdle\",\n                \"One\",\n                \"Two\",\n                \"Three\",\n                \"Yeet\"\n            );\n    }\n\n    [Fact]\n    public async Task I_can_export_a_channel_in_the_JSON_format_in_the_reverse_order()\n    {\n        // Arrange\n        using var file = TempFile.Create();\n\n        // Act\n        await new ExportChannelsCommand\n        {\n            Token = Secrets.DiscordToken,\n            ChannelIds = [ChannelIds.DateRangeTestCases],\n            ExportFormat = ExportFormat.Json,\n            OutputPath = file.Path,\n            Locale = \"en-US\",\n            IsUtcNormalizationEnabled = true,\n            IsReverseMessageOrder = true,\n        }.ExecuteAsync(new FakeConsole());\n\n        var messages = Json.Parse(await File.ReadAllTextAsync(file.Path))\n            .GetProperty(\"messages\")\n            .EnumerateArray()\n            .ToArray();\n\n        // Assert\n        messages\n            .Select(j => j.GetProperty(\"id\").GetString())\n            .Should()\n            .Equal(\n                \"885169254029213696\",\n                \"868505973294268457\",\n                \"868505969821364245\",\n                \"868505966528835604\",\n                \"868490009366396958\",\n                \"866732113319428096\",\n                \"866710679758045195\",\n                \"866674314627121232\"\n            );\n    }\n}\n"
  },
  {
    "path": "DiscordChatExporter.Cli.Tests/Specs/JsonEmbedSpecs.cs",
    "content": "﻿using System.Linq;\nusing System.Threading.Tasks;\nusing DiscordChatExporter.Cli.Tests.Infra;\nusing DiscordChatExporter.Core.Discord;\nusing FluentAssertions;\nusing Xunit;\n\nnamespace DiscordChatExporter.Cli.Tests.Specs;\n\npublic class JsonEmbedSpecs\n{\n    [Fact]\n    public async Task I_can_export_a_channel_that_contains_a_message_with_a_rich_embed()\n    {\n        // Act\n        var message = await ExportWrapper.GetMessageAsJsonAsync(\n            ChannelIds.EmbedTestCases,\n            Snowflake.Parse(\"866769910729146400\")\n        );\n\n        // Assert\n        var embed = message.GetProperty(\"embeds\").EnumerateArray().Single();\n        embed.GetProperty(\"title\").GetString().Should().Be(\"Embed title\");\n        embed.GetProperty(\"url\").GetString().Should().Be(\"https://example.com\");\n        embed.GetProperty(\"timestamp\").GetString().Should().Be(\"2021-07-14T21:00:00+00:00\");\n        embed.GetProperty(\"description\").GetString().Should().Be(\"**Embed** _description_\");\n        embed.GetProperty(\"color\").GetString().Should().Be(\"#58B9FF\");\n\n        var embedAuthor = embed.GetProperty(\"author\");\n        embedAuthor.GetProperty(\"name\").GetString().Should().Be(\"Embed author\");\n        embedAuthor.GetProperty(\"url\").GetString().Should().Be(\"https://example.com/author\");\n        embedAuthor.GetProperty(\"iconUrl\").GetString().Should().NotBeNullOrWhiteSpace();\n\n        var embedThumbnail = embed.GetProperty(\"thumbnail\");\n        embedThumbnail.GetProperty(\"url\").GetString().Should().NotBeNullOrWhiteSpace();\n        embedThumbnail.GetProperty(\"width\").GetInt32().Should().Be(120);\n        embedThumbnail.GetProperty(\"height\").GetInt32().Should().Be(120);\n\n        var embedFooter = embed.GetProperty(\"footer\");\n        embedFooter.GetProperty(\"text\").GetString().Should().Be(\"Embed footer\");\n        embedFooter.GetProperty(\"iconUrl\").GetString().Should().NotBeNullOrWhiteSpace();\n\n        var embedFields = embed.GetProperty(\"fields\").EnumerateArray().ToArray();\n        embedFields.Should().HaveCount(3);\n        embedFields[0].GetProperty(\"name\").GetString().Should().Be(\"Field 1\");\n        embedFields[0].GetProperty(\"value\").GetString().Should().Be(\"Value 1\");\n        embedFields[0].GetProperty(\"isInline\").GetBoolean().Should().BeTrue();\n        embedFields[1].GetProperty(\"name\").GetString().Should().Be(\"Field 2\");\n        embedFields[1].GetProperty(\"value\").GetString().Should().Be(\"Value 2\");\n        embedFields[1].GetProperty(\"isInline\").GetBoolean().Should().BeTrue();\n        embedFields[2].GetProperty(\"name\").GetString().Should().Be(\"Field 3\");\n        embedFields[2].GetProperty(\"value\").GetString().Should().Be(\"Value 3\");\n        embedFields[2].GetProperty(\"isInline\").GetBoolean().Should().BeTrue();\n    }\n}\n"
  },
  {
    "path": "DiscordChatExporter.Cli.Tests/Specs/JsonEmojiSpecs.cs",
    "content": "﻿using System.Linq;\nusing System.Threading.Tasks;\nusing DiscordChatExporter.Cli.Tests.Infra;\nusing DiscordChatExporter.Core.Discord;\nusing FluentAssertions;\nusing Xunit;\n\nnamespace DiscordChatExporter.Cli.Tests.Specs;\n\npublic class JsonEmojiSpecs\n{\n    [Fact]\n    public async Task I_can_export_a_channel_that_contains_a_message_with_inline_emoji_and_have_them_listed_separately()\n    {\n        // Act\n        var message = await ExportWrapper.GetMessageAsJsonAsync(\n            ChannelIds.EmojiTestCases,\n            Snowflake.Parse(\"866768521052553216\")\n        );\n\n        // Assert\n        var inlineEmojis = message.GetProperty(\"inlineEmojis\").EnumerateArray().ToArray();\n        inlineEmojis.Should().HaveCount(4);\n\n        inlineEmojis[0].GetProperty(\"id\").GetString().Should().BeNullOrEmpty();\n        inlineEmojis[0].GetProperty(\"name\").GetString().Should().Be(\"🙂\");\n        inlineEmojis[0].GetProperty(\"code\").GetString().Should().Be(\"slight_smile\");\n        inlineEmojis[0].GetProperty(\"isAnimated\").GetBoolean().Should().BeFalse();\n        inlineEmojis[0].GetProperty(\"imageUrl\").GetString().Should().NotBeNullOrWhiteSpace();\n\n        inlineEmojis[1].GetProperty(\"id\").GetString().Should().BeNullOrEmpty();\n        inlineEmojis[1].GetProperty(\"name\").GetString().Should().Be(\"😦\");\n        inlineEmojis[1].GetProperty(\"code\").GetString().Should().Be(\"frowning\");\n        inlineEmojis[1].GetProperty(\"isAnimated\").GetBoolean().Should().BeFalse();\n        inlineEmojis[1].GetProperty(\"imageUrl\").GetString().Should().NotBeNullOrWhiteSpace();\n\n        inlineEmojis[2].GetProperty(\"id\").GetString().Should().BeNullOrEmpty();\n        inlineEmojis[2].GetProperty(\"name\").GetString().Should().Be(\"😔\");\n        inlineEmojis[2].GetProperty(\"code\").GetString().Should().Be(\"pensive\");\n        inlineEmojis[2].GetProperty(\"isAnimated\").GetBoolean().Should().BeFalse();\n        inlineEmojis[2].GetProperty(\"imageUrl\").GetString().Should().NotBeNullOrWhiteSpace();\n\n        inlineEmojis[3].GetProperty(\"id\").GetString().Should().BeNullOrEmpty();\n        inlineEmojis[3].GetProperty(\"name\").GetString().Should().Be(\"😂\");\n        inlineEmojis[3].GetProperty(\"code\").GetString().Should().Be(\"joy\");\n        inlineEmojis[3].GetProperty(\"isAnimated\").GetBoolean().Should().BeFalse();\n        inlineEmojis[3].GetProperty(\"imageUrl\").GetString().Should().NotBeNullOrWhiteSpace();\n    }\n\n    [Fact]\n    public async Task I_can_export_a_channel_that_contains_a_message_with_custom_inline_emoji_and_have_them_listed_separately()\n    {\n        // Act\n        var message = await ExportWrapper.GetMessageAsJsonAsync(\n            ChannelIds.EmojiTestCases,\n            Snowflake.Parse(\"1299804867447230594\")\n        );\n\n        // Assert\n        var inlineEmojis = message.GetProperty(\"inlineEmojis\").EnumerateArray().ToArray();\n        inlineEmojis.Should().HaveCount(1);\n\n        inlineEmojis[0].GetProperty(\"id\").GetString().Should().Be(\"754441880066064584\");\n        inlineEmojis[0].GetProperty(\"name\").GetString().Should().Be(\"lemon_blush\");\n        inlineEmojis[0].GetProperty(\"code\").GetString().Should().Be(\"lemon_blush\");\n        inlineEmojis[0].GetProperty(\"isAnimated\").GetBoolean().Should().BeFalse();\n        inlineEmojis[0].GetProperty(\"imageUrl\").GetString().Should().NotBeNullOrWhiteSpace();\n    }\n}\n"
  },
  {
    "path": "DiscordChatExporter.Cli.Tests/Specs/JsonForwardSpecs.cs",
    "content": "﻿using System.Threading.Tasks;\nusing DiscordChatExporter.Cli.Tests.Infra;\nusing DiscordChatExporter.Core.Discord;\nusing FluentAssertions;\nusing Xunit;\n\nnamespace DiscordChatExporter.Cli.Tests.Specs;\n\npublic class JsonForwardSpecs\n{\n    [Fact]\n    public async Task I_can_export_a_channel_that_contains_a_forwarded_message()\n    {\n        // Act\n        var message = await ExportWrapper.GetMessageAsJsonAsync(\n            ChannelIds.ForwardTestCases,\n            Snowflake.Parse(\"1455202427115536514\")\n        );\n\n        // Assert\n        var reference = message.GetProperty(\"reference\");\n        reference.GetProperty(\"type\").GetString().Should().Be(\"Forward\");\n        reference.GetProperty(\"guildId\").GetString().Should().Be(\"869237470565392384\");\n\n        var forwardedMessage = message.GetProperty(\"forwardedMessage\");\n        forwardedMessage.GetProperty(\"content\").GetString().Should().Contain(@\"¯\\_(ツ)_/¯\");\n        forwardedMessage\n            .GetProperty(\"timestamp\")\n            .GetString()\n            .Should()\n            .StartWith(\"2025-12-28T22:52:42.175+00:00\");\n    }\n}\n"
  },
  {
    "path": "DiscordChatExporter.Cli.Tests/Specs/JsonMentionSpecs.cs",
    "content": "﻿using System.Linq;\nusing System.Threading.Tasks;\nusing DiscordChatExporter.Cli.Tests.Infra;\nusing DiscordChatExporter.Core.Discord;\nusing FluentAssertions;\nusing Xunit;\n\nnamespace DiscordChatExporter.Cli.Tests.Specs;\n\npublic class JsonMentionSpecs\n{\n    [Fact]\n    public async Task I_can_export_a_channel_that_contains_a_message_with_a_user_mention()\n    {\n        // Act\n        var message = await ExportWrapper.GetMessageAsJsonAsync(\n            ChannelIds.MentionTestCases,\n            Snowflake.Parse(\"866458840245076028\")\n        );\n\n        // Assert\n        message.GetProperty(\"content\").GetString().Should().Be(\"User mention: @Tyrrrz\");\n\n        message\n            .GetProperty(\"mentions\")\n            .EnumerateArray()\n            .Select(j => j.GetProperty(\"id\").GetString())\n            .Should()\n            .Contain(\"128178626683338752\");\n    }\n\n    [Fact]\n    public async Task I_can_export_a_channel_that_contains_a_message_with_a_text_channel_mention()\n    {\n        // Act\n        var message = await ExportWrapper.GetMessageAsJsonAsync(\n            ChannelIds.MentionTestCases,\n            Snowflake.Parse(\"866459040480624680\")\n        );\n\n        // Assert\n        message\n            .GetProperty(\"content\")\n            .GetString()\n            .Should()\n            .Be(\"Text channel mention: #mention-tests\");\n    }\n\n    [Fact]\n    public async Task I_can_export_a_channel_that_contains_a_message_with_a_voice_channel_mention()\n    {\n        // Act\n        var message = await ExportWrapper.GetMessageAsJsonAsync(\n            ChannelIds.MentionTestCases,\n            Snowflake.Parse(\"866459175462633503\")\n        );\n\n        // Assert\n        message\n            .GetProperty(\"content\")\n            .GetString()\n            .Should()\n            .Be(\"Voice channel mention: #general [voice]\");\n    }\n\n    [Fact]\n    public async Task I_can_export_a_channel_that_contains_a_message_with_a_role_mention()\n    {\n        // Act\n        var message = await ExportWrapper.GetMessageAsJsonAsync(\n            ChannelIds.MentionTestCases,\n            Snowflake.Parse(\"866459254693429258\")\n        );\n\n        // Assert\n        message.GetProperty(\"content\").GetString().Should().Be(\"Role mention: @Role 1\");\n    }\n\n    [Fact]\n    public async Task I_can_export_a_channel_that_contains_a_message_with_a_thread_mention()\n    {\n        // Act\n        var message = await ExportWrapper.GetMessageAsJsonAsync(\n            ChannelIds.MentionTestCases,\n            Snowflake.Parse(\"1474874276828938290\")\n        );\n\n        // Assert\n        message\n            .GetProperty(\"content\")\n            .GetString()\n            .Should()\n            .Be(\"Thread mention: #Thread starting message\");\n    }\n}\n"
  },
  {
    "path": "DiscordChatExporter.Cli.Tests/Specs/JsonStickerSpecs.cs",
    "content": "﻿using System.Linq;\nusing System.Threading.Tasks;\nusing DiscordChatExporter.Cli.Tests.Infra;\nusing DiscordChatExporter.Core.Discord;\nusing FluentAssertions;\nusing Xunit;\n\nnamespace DiscordChatExporter.Cli.Tests.Specs;\n\npublic class JsonStickerSpecs\n{\n    [Fact]\n    public async Task I_can_export_a_channel_that_contains_a_message_with_a_PNG_sticker()\n    {\n        // Act\n        var message = await ExportWrapper.GetMessageAsJsonAsync(\n            ChannelIds.StickerTestCases,\n            Snowflake.Parse(\"939670623158943754\")\n        );\n\n        // Assert\n        var sticker = message.GetProperty(\"stickers\").EnumerateArray().Single();\n\n        sticker.GetProperty(\"id\").GetString().Should().Be(\"904215665597120572\");\n        sticker.GetProperty(\"name\").GetString().Should().Be(\"rock\");\n        sticker.GetProperty(\"format\").GetString().Should().Be(\"Apng\");\n        sticker\n            .GetProperty(\"sourceUrl\")\n            .GetString()\n            .Should()\n            .StartWith(\"https://cdn.discordapp.com/stickers/904215665597120572.png\");\n    }\n\n    [Fact]\n    public async Task I_can_export_a_channel_that_contains_a_message_with_a_Lottie_sticker()\n    {\n        // Act\n        var message = await ExportWrapper.GetMessageAsJsonAsync(\n            ChannelIds.StickerTestCases,\n            Snowflake.Parse(\"939670526517997590\")\n        );\n\n        // Assert\n        var sticker = message.GetProperty(\"stickers\").EnumerateArray().Single();\n\n        sticker.GetProperty(\"id\").GetString().Should().Be(\"816087132447178774\");\n        sticker.GetProperty(\"name\").GetString().Should().Be(\"Yikes\");\n        sticker.GetProperty(\"format\").GetString().Should().Be(\"Lottie\");\n        sticker\n            .GetProperty(\"sourceUrl\")\n            .GetString()\n            .Should()\n            .StartWith(\"https://cdn.discordapp.com/stickers/816087132447178774.json\");\n    }\n}\n"
  },
  {
    "path": "DiscordChatExporter.Cli.Tests/Specs/PartitioningSpecs.cs",
    "content": "﻿using System.IO;\nusing System.Threading.Tasks;\nusing CliFx.Infrastructure;\nusing DiscordChatExporter.Cli.Commands;\nusing DiscordChatExporter.Cli.Tests.Infra;\nusing DiscordChatExporter.Cli.Tests.Utils;\nusing DiscordChatExporter.Core.Exporting;\nusing DiscordChatExporter.Core.Exporting.Partitioning;\nusing FluentAssertions;\nusing Xunit;\n\nnamespace DiscordChatExporter.Cli.Tests.Specs;\n\npublic class PartitioningSpecs\n{\n    [Fact]\n    public async Task I_can_export_a_channel_with_partitioning_based_on_message_count()\n    {\n        // Arrange\n        using var dir = TempDir.Create();\n        var filePath = Path.Combine(dir.Path, \"output.html\");\n\n        // Act\n        await new ExportChannelsCommand\n        {\n            Token = Secrets.DiscordToken,\n            ChannelIds = [ChannelIds.DateRangeTestCases],\n            ExportFormat = ExportFormat.HtmlDark,\n            OutputPath = filePath,\n            PartitionLimit = PartitionLimit.Parse(\"3\"),\n        }.ExecuteAsync(new FakeConsole());\n\n        // Assert\n        Directory.EnumerateFiles(dir.Path, \"output*\").Should().HaveCount(3);\n    }\n\n    [Fact]\n    public async Task I_can_export_a_channel_with_partitioning_based_on_file_size()\n    {\n        // Arrange\n        using var dir = TempDir.Create();\n        var filePath = Path.Combine(dir.Path, \"output.html\");\n\n        // Act\n        await new ExportChannelsCommand\n        {\n            Token = Secrets.DiscordToken,\n            ChannelIds = [ChannelIds.DateRangeTestCases],\n            ExportFormat = ExportFormat.HtmlDark,\n            OutputPath = filePath,\n            PartitionLimit = PartitionLimit.Parse(\"1kb\"),\n        }.ExecuteAsync(new FakeConsole());\n\n        // Assert\n        Directory.EnumerateFiles(dir.Path, \"output*\").Should().HaveCount(8);\n    }\n}\n"
  },
  {
    "path": "DiscordChatExporter.Cli.Tests/Specs/PlainTextContentSpecs.cs",
    "content": "﻿using System.Threading.Tasks;\nusing DiscordChatExporter.Cli.Tests.Infra;\nusing FluentAssertions;\nusing Xunit;\n\nnamespace DiscordChatExporter.Cli.Tests.Specs;\n\npublic class PlainTextContentSpecs\n{\n    [Fact]\n    public async Task I_can_export_a_channel_in_the_TXT_format()\n    {\n        // Act\n        var document = await ExportWrapper.ExportAsPlainTextAsync(ChannelIds.DateRangeTestCases);\n\n        // Assert\n        document\n            .Should()\n            .ContainAll(\n                \"tyrrrz\",\n                \"Hello world\",\n                \"Goodbye world\",\n                \"Foo bar\",\n                \"Hurdle Durdle\",\n                \"One\",\n                \"Two\",\n                \"Three\",\n                \"Yeet\"\n            );\n    }\n}\n"
  },
  {
    "path": "DiscordChatExporter.Cli.Tests/Specs/PlainTextForwardSpecs.cs",
    "content": "﻿using System.Threading.Tasks;\nusing DiscordChatExporter.Cli.Tests.Infra;\nusing DiscordChatExporter.Cli.Tests.Utils.Extensions;\nusing FluentAssertions;\nusing Xunit;\n\nnamespace DiscordChatExporter.Cli.Tests.Specs;\n\npublic class PlainTextForwardSpecs\n{\n    [Fact]\n    public async Task I_can_export_a_channel_that_contains_a_forwarded_message()\n    {\n        // Act\n        var document = await ExportWrapper.ExportAsPlainTextAsync(ChannelIds.ForwardTestCases);\n\n        // Assert\n        document\n            .ReplaceWhiteSpace()\n            .Should()\n            .ContainAll(\"{Forwarded Message}\", @\"¯\\_(ツ)_/¯\", \"12/28/2025 10:52 PM\");\n    }\n}\n"
  },
  {
    "path": "DiscordChatExporter.Cli.Tests/Specs/SelfContainedSpecs.cs",
    "content": "﻿using System.IO;\nusing System.Linq;\nusing System.Threading.Tasks;\nusing CliFx.Infrastructure;\nusing DiscordChatExporter.Cli.Commands;\nusing DiscordChatExporter.Cli.Tests.Infra;\nusing DiscordChatExporter.Cli.Tests.Utils;\nusing DiscordChatExporter.Core.Exporting;\nusing FluentAssertions;\nusing Xunit;\n\nnamespace DiscordChatExporter.Cli.Tests.Specs;\n\npublic class SelfContainedSpecs\n{\n    [Fact]\n    public async Task I_can_export_a_channel_and_download_all_referenced_assets()\n    {\n        // Arrange\n        using var dir = TempDir.Create();\n        var filePath = Path.Combine(dir.Path, \"output.html\");\n\n        // Act\n        await new ExportChannelsCommand\n        {\n            Token = Secrets.DiscordToken,\n            ChannelIds = [ChannelIds.SelfContainedTestCases],\n            ExportFormat = ExportFormat.HtmlDark,\n            OutputPath = filePath,\n            ShouldDownloadAssets = true,\n        }.ExecuteAsync(new FakeConsole());\n\n        // Assert\n        Html.Parse(await File.ReadAllTextAsync(filePath))\n            .QuerySelectorAll(\"body [src]\")\n            .Select(e => e.GetAttribute(\"src\")!)\n            .Select(f => Path.GetFullPath(f, dir.Path))\n            .All(File.Exists)\n            .Should()\n            .BeTrue();\n    }\n}\n"
  },
  {
    "path": "DiscordChatExporter.Cli.Tests/Utils/Extensions/StringExtensions.cs",
    "content": "﻿using System.Text;\n\nnamespace DiscordChatExporter.Cli.Tests.Utils.Extensions;\n\ninternal static class StringExtensions\n{\n    extension(string str)\n    {\n        public string ReplaceWhiteSpace(string replacement = \" \")\n        {\n            var buffer = new StringBuilder(str.Length);\n\n            foreach (var ch in str)\n                buffer.Append(char.IsWhiteSpace(ch) ? replacement : ch);\n\n            return buffer.ToString();\n        }\n    }\n}\n"
  },
  {
    "path": "DiscordChatExporter.Cli.Tests/Utils/Html.cs",
    "content": "﻿using AngleSharp.Html.Dom;\nusing AngleSharp.Html.Parser;\n\nnamespace DiscordChatExporter.Cli.Tests.Utils;\n\ninternal static class Html\n{\n    private static readonly IHtmlParser Parser = new HtmlParser();\n\n    public static IHtmlDocument Parse(string source) => Parser.ParseDocument(source);\n}\n"
  },
  {
    "path": "DiscordChatExporter.Cli.Tests/Utils/TempDir.cs",
    "content": "﻿using System;\nusing System.IO;\nusing System.Reflection;\n\nnamespace DiscordChatExporter.Cli.Tests.Utils;\n\ninternal partial class TempDir(string path) : IDisposable\n{\n    public string Path { get; } = path;\n\n    public void Dispose()\n    {\n        try\n        {\n            Directory.Delete(Path, true);\n        }\n        catch (DirectoryNotFoundException) { }\n    }\n}\n\ninternal partial class TempDir\n{\n    public static TempDir Create()\n    {\n        var dirPath = System.IO.Path.Combine(\n            System.IO.Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)\n                ?? Directory.GetCurrentDirectory(),\n            \"Temp\",\n            Guid.NewGuid().ToString()\n        );\n\n        Directory.CreateDirectory(dirPath);\n\n        return new TempDir(dirPath);\n    }\n}\n"
  },
  {
    "path": "DiscordChatExporter.Cli.Tests/Utils/TempFile.cs",
    "content": "﻿using System;\nusing System.IO;\nusing System.Reflection;\n\nnamespace DiscordChatExporter.Cli.Tests.Utils;\n\ninternal partial class TempFile(string path) : IDisposable\n{\n    public string Path { get; } = path;\n\n    public void Dispose()\n    {\n        try\n        {\n            File.Delete(Path);\n        }\n        catch (FileNotFoundException) { }\n    }\n}\n\ninternal partial class TempFile\n{\n    public static TempFile Create()\n    {\n        var dirPath = System.IO.Path.Combine(\n            System.IO.Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)\n                ?? Directory.GetCurrentDirectory(),\n            \"Temp\"\n        );\n\n        Directory.CreateDirectory(dirPath);\n\n        var filePath = System.IO.Path.Combine(dirPath, Guid.NewGuid() + \".tmp\");\n\n        return new TempFile(filePath);\n    }\n}\n"
  },
  {
    "path": "DiscordChatExporter.Cli.Tests/xunit.runner.json",
    "content": "﻿{\n  \"$schema\": \"https://xunit.net/schema/current/xunit.runner.schema.json\",\n  \"methodDisplayOptions\": \"all\",\n  \"methodDisplay\": \"method\"\n}"
  },
  {
    "path": "DiscordChatExporter.Cli.dockerfile",
    "content": "# -- Build\n# Specify the platform here so that we pull the SDK image matching the host platform,\n# instead of the target platform specified during build by the `--platform` option.\nFROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:10.0-alpine AS build\n\n# Expose the target architecture set by the `docker build --platform` option, so that\n# we can build the assembly for the correct platform.\nARG TARGETARCH\n\n# Allow setting the assembly version from the build command\nARG VERSION=0.0.0\n\nWORKDIR /tmp/app\n\nCOPY favicon.ico .\nCOPY NuGet.config .\nCOPY Directory.Build.props .\nCOPY Directory.Packages.props .\nCOPY DiscordChatExporter.Core DiscordChatExporter.Core\nCOPY DiscordChatExporter.Cli DiscordChatExporter.Cli\n\n# Publish a self-contained assembly so we can use a slimmer runtime image\nRUN dotnet publish DiscordChatExporter.Cli \\\n    -p:Version=$VERSION \\\n    -p:CSharpier_Bypass=true \\\n    --configuration Release \\\n    --self-contained \\\n    --use-current-runtime \\\n    --arch $TARGETARCH \\\n    --output DiscordChatExporter.Cli/bin/publish/\n\n# -- Run\n# Use `runtime-deps` instead of `runtime` because we have a self-contained assembly\nFROM --platform=$TARGETPLATFORM mcr.microsoft.com/dotnet/runtime-deps:10.0-alpine AS run\n\nLABEL org.opencontainers.image.title=\"DiscordChatExporter.Cli\"\nLABEL org.opencontainers.image.description=\"DiscordChatExporter is an application that can be used to export message history from any Discord channel to a file.\"\nLABEL org.opencontainers.image.authors=\"tyrrrz.me\"\nLABEL org.opencontainers.image.source=\"https://github.com/Tyrrrz/DiscordChatExporter\"\nLABEL org.opencontainers.image.licenses=\"MIT\"\n\n# Alpine image doesn't come with the ICU libraries pre-installed, so we need to install them manually.\n# We need the full ICU data because we allow the user to specify any locale for formatting purposes.\nRUN apk add --no-cache icu-libs icu-data-full\nENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=false\nENV LC_ALL=en_US.UTF-8\nENV LANG=en_US.UTF-8\n\n# Alpine is missing tzdata, which we need to support timezones\nRUN apk add --no-cache tzdata\n\n# Use a non-root user to ensure that the files shared with the host are accessible by the host user\n# https://github.com/Tyrrrz/DiscordChatExporter/issues/851\n# https://github.com/Tyrrrz/DiscordChatExporter/issues/1174\nRUN apk add --no-cache su-exec\nRUN addgroup -S -g 1000 dce && adduser -S -H -G dce -u 1000 dce\n\n# This directory is exposed to the user for mounting purposes, so it's important that it always\n# stays the same for backwards compatibility.\nWORKDIR /out\n\nCOPY --from=build /tmp/app/DiscordChatExporter.Cli/bin/publish /opt/app\nCOPY docker-entrypoint.sh /opt/app\nENTRYPOINT [\"/opt/app/docker-entrypoint.sh\"]\n"
  },
  {
    "path": "DiscordChatExporter.Core/Discord/Data/Application.cs",
    "content": "﻿using System.Text.Json;\nusing DiscordChatExporter.Core.Utils.Extensions;\nusing JsonExtensions.Reading;\n\nnamespace DiscordChatExporter.Core.Discord.Data;\n\n// https://discord.com/developers/docs/resources/application#application-object\npublic partial record Application(Snowflake Id, string Name, ApplicationFlags Flags)\n{\n    public bool IsMessageContentIntentEnabled { get; } =\n        Flags.HasFlag(ApplicationFlags.GatewayMessageContent)\n        || Flags.HasFlag(ApplicationFlags.GatewayMessageContentLimited);\n}\n\npublic partial record Application\n{\n    public static Application Parse(JsonElement json)\n    {\n        var id = json.GetProperty(\"id\").GetNonWhiteSpaceString().Pipe(Snowflake.Parse);\n        var name = json.GetProperty(\"name\").GetNonWhiteSpaceString();\n\n        var flags =\n            json.GetPropertyOrNull(\"flags\")?.GetInt32OrNull()?.Pipe(x => (ApplicationFlags)x)\n            ?? ApplicationFlags.None;\n\n        return new Application(id, name, flags);\n    }\n}\n"
  },
  {
    "path": "DiscordChatExporter.Core/Discord/Data/ApplicationFlags.cs",
    "content": "﻿using System;\n\nnamespace DiscordChatExporter.Core.Discord.Data;\n\n// https://discord.com/developers/docs/resources/application#application-object-application-flags\n[Flags]\npublic enum ApplicationFlags\n{\n    None = 0,\n    ApplicationAutoModerationRuleCreateBadge = 64,\n    GatewayPresence = 4096,\n    GatewayPresenceLimited = 8192,\n    GatewayGuildMembers = 16384,\n    GatewayGuildMembersLimited = 32768,\n    VerificationPendingGuildLimit = 65536,\n    Embedded = 131072,\n    GatewayMessageContent = 262144,\n    GatewayMessageContentLimited = 524288,\n    ApplicationCommandBadge = 8388608,\n}\n"
  },
  {
    "path": "DiscordChatExporter.Core/Discord/Data/Attachment.cs",
    "content": "﻿using System;\nusing System.IO;\nusing System.Text.Json;\nusing DiscordChatExporter.Core.Discord.Data.Common;\nusing DiscordChatExporter.Core.Utils.Extensions;\nusing JsonExtensions.Reading;\n\nnamespace DiscordChatExporter.Core.Discord.Data;\n\n// https://discord.com/developers/docs/resources/channel#attachment-object\npublic partial record Attachment(\n    Snowflake Id,\n    string Url,\n    string FileName,\n    string? Description,\n    int? Width,\n    int? Height,\n    FileSize FileSize\n) : IHasId\n{\n    public string FileExtension { get; } = Path.GetExtension(FileName);\n\n    public bool IsImage =>\n        string.Equals(FileExtension, \".jpg\", StringComparison.OrdinalIgnoreCase)\n        || string.Equals(FileExtension, \".jpeg\", StringComparison.OrdinalIgnoreCase)\n        || string.Equals(FileExtension, \".png\", StringComparison.OrdinalIgnoreCase)\n        || string.Equals(FileExtension, \".gif\", StringComparison.OrdinalIgnoreCase)\n        || string.Equals(FileExtension, \".bmp\", StringComparison.OrdinalIgnoreCase)\n        || string.Equals(FileExtension, \".webp\", StringComparison.OrdinalIgnoreCase);\n\n    public bool IsVideo =>\n        string.Equals(FileExtension, \".gifv\", StringComparison.OrdinalIgnoreCase)\n        || string.Equals(FileExtension, \".mp4\", StringComparison.OrdinalIgnoreCase)\n        || string.Equals(FileExtension, \".webm\", StringComparison.OrdinalIgnoreCase)\n        || string.Equals(FileExtension, \".mov\", StringComparison.OrdinalIgnoreCase);\n\n    public bool IsAudio =>\n        string.Equals(FileExtension, \".mp3\", StringComparison.OrdinalIgnoreCase)\n        || string.Equals(FileExtension, \".wav\", StringComparison.OrdinalIgnoreCase)\n        || string.Equals(FileExtension, \".ogg\", StringComparison.OrdinalIgnoreCase)\n        || string.Equals(FileExtension, \".flac\", StringComparison.OrdinalIgnoreCase)\n        || string.Equals(FileExtension, \".m4a\", StringComparison.OrdinalIgnoreCase);\n\n    public bool IsSpoiler { get; } = FileName.StartsWith(\"SPOILER_\", StringComparison.Ordinal);\n}\n\npublic partial record Attachment\n{\n    public static Attachment Parse(JsonElement json)\n    {\n        var id = json.GetProperty(\"id\").GetNonWhiteSpaceString().Pipe(Snowflake.Parse);\n        var url = json.GetProperty(\"url\").GetNonWhiteSpaceString();\n        var fileName = json.GetProperty(\"filename\").GetNonNullString();\n        var description = json.GetPropertyOrNull(\"description\")?.GetNonWhiteSpaceStringOrNull();\n        var width = json.GetPropertyOrNull(\"width\")?.GetInt32OrNull();\n        var height = json.GetPropertyOrNull(\"height\")?.GetInt32OrNull();\n        var fileSize = json.GetProperty(\"size\").GetInt64().Pipe(FileSize.FromBytes);\n\n        return new Attachment(id, url, fileName, description, width, height, fileSize);\n    }\n}\n"
  },
  {
    "path": "DiscordChatExporter.Core/Discord/Data/Channel.cs",
    "content": "﻿using System.Collections.Generic;\nusing System.Linq;\nusing System.Text.Json;\nusing DiscordChatExporter.Core.Discord.Data.Common;\nusing DiscordChatExporter.Core.Utils.Extensions;\nusing JsonExtensions.Reading;\n\nnamespace DiscordChatExporter.Core.Discord.Data;\n\n// https://discord.com/developers/docs/resources/channel#channel-object\npublic partial record Channel(\n    Snowflake Id,\n    ChannelKind Kind,\n    Snowflake GuildId,\n    Channel? Parent,\n    string Name,\n    int? Position,\n    string? IconUrl,\n    string? Topic,\n    bool IsArchived,\n    Snowflake? LastMessageId\n) : IHasId\n{\n    public bool IsDirect { get; } =\n        Kind is ChannelKind.DirectTextChat or ChannelKind.DirectGroupTextChat;\n\n    public bool IsGuild => !IsDirect;\n\n    public bool IsCategory { get; } = Kind == ChannelKind.GuildCategory;\n\n    public bool IsVoice { get; } =\n        Kind is ChannelKind.GuildVoiceChat or ChannelKind.GuildStageVoice;\n\n    public bool IsThread { get; } =\n        Kind\n        is ChannelKind.GuildNewsThread\n            or ChannelKind.GuildPublicThread\n            or ChannelKind.GuildPrivateThread;\n\n    public bool IsEmpty { get; } = LastMessageId is null;\n\n    public IEnumerable<Channel> GetParents()\n    {\n        var current = Parent;\n        while (current is not null)\n        {\n            yield return current;\n            current = current.Parent;\n        }\n    }\n\n    public Channel? TryGetRootParent() => GetParents().LastOrDefault();\n\n    public string GetHierarchicalName() =>\n        string.Join(\" / \", GetParents().Reverse().Select(c => c.Name).Append(Name));\n\n    public bool MayHaveMessagesAfter(Snowflake messageId) => !IsEmpty && messageId < LastMessageId;\n\n    public bool MayHaveMessagesBefore(Snowflake messageId) => !IsEmpty && messageId > Id;\n}\n\npublic partial record Channel\n{\n    public static Channel Parse(JsonElement json, Channel? parent = null, int? positionHint = null)\n    {\n        var id = json.GetProperty(\"id\").GetNonWhiteSpaceString().Pipe(Snowflake.Parse);\n        var kind = json.GetProperty(\"type\").GetInt32().Pipe(t => (ChannelKind)t);\n\n        var guildId =\n            json.GetPropertyOrNull(\"guild_id\")\n                ?.GetNonWhiteSpaceStringOrNull()\n                ?.Pipe(Snowflake.Parse)\n            ?? Guild.DirectMessages.Id;\n\n        var name =\n            // Guild channel\n            json.GetPropertyOrNull(\"name\")?.GetNonWhiteSpaceStringOrNull()\n            // DM channel\n            ?? json.GetPropertyOrNull(\"recipients\")\n                ?.EnumerateArrayOrNull()\n                ?.Select(User.Parse)\n                .OrderBy(u => u.Id)\n                .Select(u => u.DisplayName)\n                .Pipe(s => string.Join(\", \", s))\n            // Fallback\n            ?? id.ToString();\n\n        var position = positionHint ?? json.GetPropertyOrNull(\"position\")?.GetInt32OrNull();\n\n        // Icons can only be set for group DM channels\n        var iconUrl = json.GetPropertyOrNull(\"icon\")\n            ?.GetNonWhiteSpaceStringOrNull()\n            ?.Pipe(h => ImageCdn.GetChannelIconUrl(id, h));\n\n        var topic = json.GetPropertyOrNull(\"topic\")?.GetStringOrNull();\n\n        var isArchived =\n            json.GetPropertyOrNull(\"thread_metadata\")\n                ?.GetPropertyOrNull(\"archived\")\n                ?.GetBooleanOrNull()\n            ?? false;\n\n        var lastMessageId = json.GetPropertyOrNull(\"last_message_id\")\n            ?.GetNonWhiteSpaceStringOrNull()\n            ?.Pipe(Snowflake.Parse);\n\n        return new Channel(\n            id,\n            kind,\n            guildId,\n            parent,\n            name,\n            position,\n            iconUrl,\n            topic,\n            isArchived,\n            lastMessageId\n        );\n    }\n}\n"
  },
  {
    "path": "DiscordChatExporter.Core/Discord/Data/ChannelConnection.cs",
    "content": "﻿using System.Collections.Generic;\nusing System.Linq;\n\nnamespace DiscordChatExporter.Core.Discord.Data;\n\npublic record ChannelConnection(Channel Channel, IReadOnlyList<ChannelConnection> Children)\n{\n    public static IReadOnlyList<ChannelConnection> BuildTree(IReadOnlyList<Channel> channels)\n    {\n        IReadOnlyList<ChannelConnection> GetChildren(Channel parent) =>\n            channels\n                .Where(c => c.Parent?.Id == parent.Id)\n                .Select(c => new ChannelConnection(c, GetChildren(c)))\n                .ToArray();\n\n        return channels\n            .Where(c => c.Parent is null)\n            .Select(c => new ChannelConnection(c, GetChildren(c)))\n            .ToArray();\n    }\n}\n"
  },
  {
    "path": "DiscordChatExporter.Core/Discord/Data/ChannelKind.cs",
    "content": "﻿namespace DiscordChatExporter.Core.Discord.Data;\n\n// https://discord.com/developers/docs/resources/channel#channel-object-channel-types\npublic enum ChannelKind\n{\n    GuildTextChat = 0,\n    DirectTextChat = 1,\n    GuildVoiceChat = 2,\n    DirectGroupTextChat = 3,\n    GuildCategory = 4,\n    GuildNews = 5,\n    GuildNewsThread = 10,\n    GuildPublicThread = 11,\n    GuildPrivateThread = 12,\n    GuildStageVoice = 13,\n    GuildDirectory = 14,\n    GuildForum = 15,\n}\n"
  },
  {
    "path": "DiscordChatExporter.Core/Discord/Data/Common/FileSize.cs",
    "content": "﻿using System;\nusing System.Diagnostics.CodeAnalysis;\nusing System.Globalization;\n\nnamespace DiscordChatExporter.Core.Discord.Data.Common;\n\n// Loosely based on https://github.com/omar/ByteSize (MIT license)\npublic readonly partial record struct FileSize(long TotalBytes)\n{\n    public double TotalKiloBytes => TotalBytes / 1024.0;\n    public double TotalMegaBytes => TotalKiloBytes / 1024.0;\n    public double TotalGigaBytes => TotalMegaBytes / 1024.0;\n\n    private double GetLargestWholeNumberValue()\n    {\n        if (Math.Abs(TotalGigaBytes) >= 1)\n            return TotalGigaBytes;\n\n        if (Math.Abs(TotalMegaBytes) >= 1)\n            return TotalMegaBytes;\n\n        if (Math.Abs(TotalKiloBytes) >= 1)\n            return TotalKiloBytes;\n\n        return TotalBytes;\n    }\n\n    private string GetLargestWholeNumberSymbol()\n    {\n        if (Math.Abs(TotalGigaBytes) >= 1)\n            return \"GB\";\n\n        if (Math.Abs(TotalMegaBytes) >= 1)\n            return \"MB\";\n\n        if (Math.Abs(TotalKiloBytes) >= 1)\n            return \"KB\";\n\n        return \"bytes\";\n    }\n\n    [ExcludeFromCodeCoverage]\n    public override string ToString() =>\n        string.Create(\n            CultureInfo.InvariantCulture,\n            $\"{GetLargestWholeNumberValue():0.##} {GetLargestWholeNumberSymbol()}\"\n        );\n}\n\npublic partial record struct FileSize\n{\n    public static FileSize FromBytes(long bytes) => new(bytes);\n}\n"
  },
  {
    "path": "DiscordChatExporter.Core/Discord/Data/Common/IHasId.cs",
    "content": "﻿namespace DiscordChatExporter.Core.Discord.Data.Common;\n\npublic interface IHasId\n{\n    Snowflake Id { get; }\n}\n"
  },
  {
    "path": "DiscordChatExporter.Core/Discord/Data/Common/ImageCdn.cs",
    "content": "﻿using System;\nusing System.Globalization;\nusing System.Linq;\n\nnamespace DiscordChatExporter.Core.Discord.Data.Common;\n\n// https://discord.com/developers/docs/reference#image-formatting\npublic static class ImageCdn\n{\n    // Standard emoji are rendered through Twemoji\n    public static string GetStandardEmojiUrl(string emojiName)\n    {\n        var runes = emojiName.EnumerateRunes().ToArray();\n\n        // Variant selector rune is skipped in Twemoji IDs,\n        // except when the emoji also contains a zero-width joiner.\n        // VS = 0xfe0f; ZWJ = 0x200d.\n        var filteredRunes = runes.Any(r => r.Value == 0x200d)\n            ? runes\n            : runes.Where(r => r.Value != 0xfe0f);\n\n        var twemojiId = string.Join(\n            \"-\",\n            filteredRunes.Select(r => r.Value.ToString(\"x\", CultureInfo.InvariantCulture))\n        );\n\n        return $\"https://cdn.jsdelivr.net/gh/twitter/twemoji@latest/assets/svg/{twemojiId}.svg\";\n    }\n\n    public static string GetCustomEmojiUrl(Snowflake emojiId, bool isAnimated = false) =>\n        isAnimated\n            ? $\"https://cdn.discordapp.com/emojis/{emojiId}.gif\"\n            : $\"https://cdn.discordapp.com/emojis/{emojiId}.png\";\n\n    public static string GetGuildIconUrl(Snowflake guildId, string iconHash, int size = 512) =>\n        iconHash.StartsWith(\"a_\", StringComparison.Ordinal)\n            ? $\"https://cdn.discordapp.com/icons/{guildId}/{iconHash}.gif?size={size}\"\n            : $\"https://cdn.discordapp.com/icons/{guildId}/{iconHash}.png?size={size}\";\n\n    public static string GetChannelIconUrl(Snowflake channelId, string iconHash, int size = 512) =>\n        iconHash.StartsWith(\"a_\", StringComparison.Ordinal)\n            ? $\"https://cdn.discordapp.com/channel-icons/{channelId}/{iconHash}.gif?size={size}\"\n            : $\"https://cdn.discordapp.com/channel-icons/{channelId}/{iconHash}.png?size={size}\";\n\n    public static string GetUserAvatarUrl(Snowflake userId, string avatarHash, int size = 512) =>\n        avatarHash.StartsWith(\"a_\", StringComparison.Ordinal)\n            ? $\"https://cdn.discordapp.com/avatars/{userId}/{avatarHash}.gif?size={size}\"\n            : $\"https://cdn.discordapp.com/avatars/{userId}/{avatarHash}.png?size={size}\";\n\n    public static string GetFallbackUserAvatarUrl(int index = 0) =>\n        $\"https://cdn.discordapp.com/embed/avatars/{index}.png\";\n\n    public static string GetMemberAvatarUrl(\n        Snowflake guildId,\n        Snowflake userId,\n        string avatarHash,\n        int size = 512\n    ) =>\n        avatarHash.StartsWith(\"a_\", StringComparison.Ordinal)\n            ? $\"https://cdn.discordapp.com/guilds/{guildId}/users/{userId}/avatars/{avatarHash}.gif?size={size}\"\n            : $\"https://cdn.discordapp.com/guilds/{guildId}/users/{userId}/avatars/{avatarHash}.png?size={size}\";\n\n    public static string GetStickerUrl(Snowflake stickerId, string format = \"png\") =>\n        $\"https://cdn.discordapp.com/stickers/{stickerId}.{format}\";\n}\n"
  },
  {
    "path": "DiscordChatExporter.Core/Discord/Data/Embeds/Embed.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.Drawing;\nusing System.Linq;\nusing System.Text.Json;\nusing DiscordChatExporter.Core.Utils.Extensions;\nusing JsonExtensions.Reading;\n\nnamespace DiscordChatExporter.Core.Discord.Data.Embeds;\n\n// https://discord.com/developers/docs/resources/channel#embed-object\npublic partial record Embed(\n    string? Title,\n    EmbedKind Kind,\n    string? Url,\n    DateTimeOffset? Timestamp,\n    Color? Color,\n    EmbedAuthor? Author,\n    string? Description,\n    IReadOnlyList<EmbedField> Fields,\n    EmbedImage? Thumbnail,\n    IReadOnlyList<EmbedImage> Images,\n    EmbedVideo? Video,\n    EmbedFooter? Footer\n)\n{\n    // Embeds can only have one image according to the API model,\n    // but the client can render multiple images in some cases.\n    public EmbedImage? Image => Images.FirstOrDefault();\n\n    public SpotifyTrackEmbedProjection? TryGetSpotifyTrack() =>\n        SpotifyTrackEmbedProjection.TryResolve(this);\n\n    public TwitchClipEmbedProjection? TryGetTwitchClip() =>\n        TwitchClipEmbedProjection.TryResolve(this);\n\n    public YouTubeVideoEmbedProjection? TryGetYouTubeVideo() =>\n        YouTubeVideoEmbedProjection.TryResolve(this);\n}\n\npublic partial record Embed\n{\n    public static Embed Parse(JsonElement json)\n    {\n        var title = json.GetPropertyOrNull(\"title\")?.GetStringOrNull();\n\n        var kind =\n            json.GetPropertyOrNull(\"type\")\n                ?.GetStringOrNull()\n                ?.Pipe(s =>\n                    Enum.TryParse<EmbedKind>(s, true, out var result) ? result : (EmbedKind?)null\n                )\n            ?? EmbedKind.Rich;\n\n        var url = json.GetPropertyOrNull(\"url\")?.GetNonWhiteSpaceStringOrNull();\n        var timestamp = json.GetPropertyOrNull(\"timestamp\")?.GetDateTimeOffsetOrNull();\n\n        var color = json.GetPropertyOrNull(\"color\")\n            ?.GetInt32OrNull()\n            ?.Pipe(System.Drawing.Color.FromArgb)\n            .ResetAlpha();\n\n        var author = json.GetPropertyOrNull(\"author\")?.Pipe(EmbedAuthor.Parse);\n        var description = json.GetPropertyOrNull(\"description\")?.GetStringOrNull();\n\n        var fields =\n            json.GetPropertyOrNull(\"fields\")\n                ?.EnumerateArrayOrNull()\n                ?.Select(EmbedField.Parse)\n                .ToArray()\n            ?? [];\n\n        var thumbnail = json.GetPropertyOrNull(\"thumbnail\")?.Pipe(EmbedImage.Parse);\n\n        // Under the Discord API model, embeds can only have at most one image.\n        // Because of that, embeds that are rendered with multiple images on the client\n        // (e.g. tweet embeds), are exposed from the API as multiple separate embeds.\n        // Our embed model is consistent with the user-facing side of Discord, so images\n        // are stored as an array. The API will only ever return one image, but we deal\n        // with this by merging related embeds at the end of the message parsing process.\n        // https://github.com/Tyrrrz/DiscordChatExporter/issues/695\n        var images =\n            json.GetPropertyOrNull(\"image\")\n                ?.Pipe(EmbedImage.Parse)\n                .ToSingletonEnumerable()\n                .ToArray()\n            ?? [];\n\n        var video = json.GetPropertyOrNull(\"video\")?.Pipe(EmbedVideo.Parse);\n\n        var footer = json.GetPropertyOrNull(\"footer\")?.Pipe(EmbedFooter.Parse);\n\n        return new Embed(\n            title,\n            kind,\n            url,\n            timestamp,\n            color,\n            author,\n            description,\n            fields,\n            thumbnail,\n            images,\n            video,\n            footer\n        );\n    }\n}\n"
  },
  {
    "path": "DiscordChatExporter.Core/Discord/Data/Embeds/EmbedAuthor.cs",
    "content": "using System.Text.Json;\nusing JsonExtensions.Reading;\n\nnamespace DiscordChatExporter.Core.Discord.Data.Embeds;\n\n// https://discord.com/developers/docs/resources/channel#embed-object-embed-author-structure\npublic record EmbedAuthor(string? Name, string? Url, string? IconUrl, string? IconProxyUrl)\n{\n    public static EmbedAuthor Parse(JsonElement json)\n    {\n        var name = json.GetPropertyOrNull(\"name\")?.GetStringOrNull();\n        var url = json.GetPropertyOrNull(\"url\")?.GetNonWhiteSpaceStringOrNull();\n        var iconUrl = json.GetPropertyOrNull(\"icon_url\")?.GetNonWhiteSpaceStringOrNull();\n        var iconProxyUrl = json.GetPropertyOrNull(\"proxy_icon_url\")?.GetNonWhiteSpaceStringOrNull();\n\n        return new EmbedAuthor(name, url, iconUrl, iconProxyUrl);\n    }\n}\n"
  },
  {
    "path": "DiscordChatExporter.Core/Discord/Data/Embeds/EmbedField.cs",
    "content": "using System.Text.Json;\nusing JsonExtensions.Reading;\n\nnamespace DiscordChatExporter.Core.Discord.Data.Embeds;\n\n// https://discord.com/developers/docs/resources/channel#embed-object-embed-field-structure\npublic record EmbedField(string Name, string Value, bool IsInline)\n{\n    public static EmbedField Parse(JsonElement json)\n    {\n        var name = json.GetProperty(\"name\").GetNonNullString();\n        var value = json.GetProperty(\"value\").GetNonNullString();\n        var isInline = json.GetPropertyOrNull(\"inline\")?.GetBooleanOrNull() ?? false;\n\n        return new EmbedField(name, value, isInline);\n    }\n}\n"
  },
  {
    "path": "DiscordChatExporter.Core/Discord/Data/Embeds/EmbedFooter.cs",
    "content": "using System.Text.Json;\nusing JsonExtensions.Reading;\n\nnamespace DiscordChatExporter.Core.Discord.Data.Embeds;\n\n// https://discord.com/developers/docs/resources/channel#embed-object-embed-footer-structure\npublic record EmbedFooter(string Text, string? IconUrl, string? IconProxyUrl)\n{\n    public static EmbedFooter Parse(JsonElement json)\n    {\n        var text = json.GetProperty(\"text\").GetNonNullString();\n        var iconUrl = json.GetPropertyOrNull(\"icon_url\")?.GetNonWhiteSpaceStringOrNull();\n        var iconProxyUrl = json.GetPropertyOrNull(\"proxy_icon_url\")?.GetNonWhiteSpaceStringOrNull();\n\n        return new EmbedFooter(text, iconUrl, iconProxyUrl);\n    }\n}\n"
  },
  {
    "path": "DiscordChatExporter.Core/Discord/Data/Embeds/EmbedImage.cs",
    "content": "using System.Text.Json;\nusing JsonExtensions.Reading;\n\nnamespace DiscordChatExporter.Core.Discord.Data.Embeds;\n\n// https://discord.com/developers/docs/resources/channel#embed-object-embed-image-structure\npublic record EmbedImage(string? Url, string? ProxyUrl, int? Width, int? Height)\n{\n    public static EmbedImage Parse(JsonElement json)\n    {\n        var url = json.GetPropertyOrNull(\"url\")?.GetNonWhiteSpaceStringOrNull();\n        var proxyUrl = json.GetPropertyOrNull(\"proxy_url\")?.GetNonWhiteSpaceStringOrNull();\n        var width = json.GetPropertyOrNull(\"width\")?.GetInt32OrNull();\n        var height = json.GetPropertyOrNull(\"height\")?.GetInt32OrNull();\n\n        return new EmbedImage(url, proxyUrl, width, height);\n    }\n}\n"
  },
  {
    "path": "DiscordChatExporter.Core/Discord/Data/Embeds/EmbedKind.cs",
    "content": "namespace DiscordChatExporter.Core.Discord.Data.Embeds;\n\n// https://discord.com/developers/docs/resources/channel#embed-object-embed-types\npublic enum EmbedKind\n{\n    Rich,\n    Image,\n    Video,\n    Gifv,\n    Link,\n}\n"
  },
  {
    "path": "DiscordChatExporter.Core/Discord/Data/Embeds/EmbedVideo.cs",
    "content": "using System.Text.Json;\nusing JsonExtensions.Reading;\n\nnamespace DiscordChatExporter.Core.Discord.Data.Embeds;\n\n// https://discord.com/developers/docs/resources/channel#embed-object-embed-video-structure\npublic record EmbedVideo(string? Url, string? ProxyUrl, int? Width, int? Height)\n{\n    public static EmbedVideo Parse(JsonElement json)\n    {\n        var url = json.GetPropertyOrNull(\"url\")?.GetNonWhiteSpaceStringOrNull();\n        var proxyUrl = json.GetPropertyOrNull(\"proxy_url\")?.GetNonWhiteSpaceStringOrNull();\n        var width = json.GetPropertyOrNull(\"width\")?.GetInt32OrNull();\n        var height = json.GetPropertyOrNull(\"height\")?.GetInt32OrNull();\n\n        return new EmbedVideo(url, proxyUrl, width, height);\n    }\n}\n"
  },
  {
    "path": "DiscordChatExporter.Core/Discord/Data/Embeds/SpotifyTrackEmbedProjection.cs",
    "content": "﻿using System.Text.RegularExpressions;\n\nnamespace DiscordChatExporter.Core.Discord.Data.Embeds;\n\npublic partial record SpotifyTrackEmbedProjection(string TrackId)\n{\n    public string Url { get; } = $\"https://open.spotify.com/embed/track/{TrackId}\";\n}\n\npublic partial record SpotifyTrackEmbedProjection\n{\n    private static string? TryParseTrackId(string embedUrl)\n    {\n        // https://open.spotify.com/track/1LHZMWefF9502NPfArRfvP?si=3efac6ce9be04f0a\n        var trackId = Regex\n            .Match(embedUrl, @\"spotify\\.com/track/(.*?)(?:\\?|&|/|$)\")\n            .Groups[1]\n            .Value;\n\n        if (!string.IsNullOrWhiteSpace(trackId))\n            return trackId;\n\n        return null;\n    }\n\n    public static SpotifyTrackEmbedProjection? TryResolve(Embed embed)\n    {\n        if (embed.Kind != EmbedKind.Link)\n            return null;\n\n        if (string.IsNullOrWhiteSpace(embed.Url))\n            return null;\n\n        var trackId = TryParseTrackId(embed.Url);\n        if (string.IsNullOrWhiteSpace(trackId))\n            return null;\n\n        return new SpotifyTrackEmbedProjection(trackId);\n    }\n}\n"
  },
  {
    "path": "DiscordChatExporter.Core/Discord/Data/Embeds/TwitchClipEmbedProjection.cs",
    "content": "﻿using System.Text.RegularExpressions;\n\nnamespace DiscordChatExporter.Core.Discord.Data.Embeds;\n\npublic partial record TwitchClipEmbedProjection(string ClipId)\n{\n    public string Url { get; } = $\"https://clips.twitch.tv/embed?clip={ClipId}&parent=localhost\";\n}\n\npublic partial record TwitchClipEmbedProjection\n{\n    private static string? TryParseClipId(string embedUrl)\n    {\n        // https://clips.twitch.tv/SpookyTenuousPidgeonPanicVis\n        {\n            var clipId = Regex\n                .Match(embedUrl, @\"clips\\.twitch\\.tv/(.*?)(?:\\?|&|/|$)\")\n                .Groups[1]\n                .Value;\n\n            if (!string.IsNullOrWhiteSpace(clipId))\n                return clipId;\n        }\n\n        // https://twitch.tv/clip/SpookyTenuousPidgeonPanicVis\n        {\n            var clipId = Regex\n                .Match(embedUrl, @\"twitch\\.tv/clip/(.*?)(?:\\?|&|/|$)\")\n                .Groups[1]\n                .Value;\n\n            if (!string.IsNullOrWhiteSpace(clipId))\n                return clipId;\n        }\n\n        return null;\n    }\n\n    public static TwitchClipEmbedProjection? TryResolve(Embed embed)\n    {\n        if (embed.Kind != EmbedKind.Video)\n            return null;\n\n        if (string.IsNullOrWhiteSpace(embed.Url))\n            return null;\n\n        var clipId = TryParseClipId(embed.Url);\n        if (string.IsNullOrWhiteSpace(clipId))\n            return null;\n\n        return new TwitchClipEmbedProjection(clipId);\n    }\n}\n"
  },
  {
    "path": "DiscordChatExporter.Core/Discord/Data/Embeds/YouTubeVideoEmbedProjection.cs",
    "content": "﻿namespace DiscordChatExporter.Core.Discord.Data.Embeds;\n\npublic partial record YouTubeVideoEmbedProjection(string VideoId)\n{\n    public string Url { get; } = $\"https://www.youtube.com/watch?v={VideoId}\";\n\n    // Using hqdefault.jpg which is guaranteed to exist for all YouTube videos\n    public string ThumbnailUrl { get; } = $\"https://i.ytimg.com/vi/{VideoId}/hqdefault.jpg\";\n}\n\npublic partial record YouTubeVideoEmbedProjection\n{\n    public static YouTubeVideoEmbedProjection? TryResolve(Embed embed)\n    {\n        if (embed.Kind != EmbedKind.Video)\n            return null;\n\n        if (string.IsNullOrWhiteSpace(embed.Url))\n            return null;\n\n        var videoId = YoutubeExplode.Videos.VideoId.TryParse(embed.Url);\n        if (videoId is null)\n            return null;\n\n        return new YouTubeVideoEmbedProjection(videoId);\n    }\n}\n"
  },
  {
    "path": "DiscordChatExporter.Core/Discord/Data/Emoji.cs",
    "content": "﻿using System.Text.Json;\nusing DiscordChatExporter.Core.Discord.Data.Common;\nusing DiscordChatExporter.Core.Utils;\nusing DiscordChatExporter.Core.Utils.Extensions;\nusing JsonExtensions.Reading;\n\nnamespace DiscordChatExporter.Core.Discord.Data;\n\n// https://discord.com/developers/docs/resources/emoji#emoji-object\npublic partial record Emoji(\n    // Only present on custom emoji\n    Snowflake? Id,\n    // Name of a custom emoji (e.g. LUL) or actual representation of a standard emoji (e.g. 🙂)\n    string Name,\n    bool IsAnimated\n)\n{\n    public bool IsCustomEmoji { get; } = Id is not null;\n\n    // Name of a custom emoji (e.g. LUL) or name of a standard emoji (e.g. slight_smile)\n    public string Code { get; } = Id is not null ? Name : EmojiIndex.TryGetCode(Name) ?? Name;\n\n    public string ImageUrl { get; } =\n        Id is not null\n            ? ImageCdn.GetCustomEmojiUrl(Id.Value, IsAnimated)\n            : ImageCdn.GetStandardEmojiUrl(Name);\n}\n\npublic partial record Emoji\n{\n    public static Emoji Parse(JsonElement json)\n    {\n        var id = json.GetPropertyOrNull(\"id\")\n            ?.GetNonWhiteSpaceStringOrNull()\n            ?.Pipe(Snowflake.Parse);\n\n        // Names may be missing on custom emoji within reactions\n        var name =\n            json.GetPropertyOrNull(\"name\")?.GetNonWhiteSpaceStringOrNull() ?? \"Unknown Emoji\";\n\n        var isAnimated = json.GetPropertyOrNull(\"animated\")?.GetBooleanOrNull() ?? false;\n\n        return new Emoji(id, name, isAnimated);\n    }\n}\n"
  },
  {
    "path": "DiscordChatExporter.Core/Discord/Data/EmojiIndex.cs",
    "content": "﻿using System;\nusing System.Collections.Generic;\nusing System.Diagnostics.CodeAnalysis;\n\nnamespace DiscordChatExporter.Core.Utils;\n\n// Data sourced from: https://github.com/Tyrrrz/DiscordChatExporter/issues/599#issuecomment-863431045\n[ExcludeFromCodeCoverage]\ninternal static class EmojiIndex\n{\n    private static Dictionary<string, string> _toCodes = new(5000, StringComparer.Ordinal)\n    {\n        [\"😀\"] = \"grinning\",\n        [\"😃\"] = \"smiley\",\n        [\"😄\"] = \"smile\",\n        [\"😁\"] = \"grin\",\n        [\"😆\"] = \"laughing\",\n        [\"😅\"] = \"sweat_smile\",\n        [\"😂\"] = \"joy\",\n        [\"🤣\"] = \"rofl\",\n        [\"☺️\"] = \"relaxed\",\n        [\"😊\"] = \"blush\",\n        [\"😇\"] = \"innocent\",\n        [\"🙂\"] = \"slight_smile\",\n        [\"🙃\"] = \"upside_down\",\n        [\"😉\"] = \"wink\",\n        [\"😌\"] = \"relieved\",\n        [\"🥲\"] = \"smiling_face_with_tear\",\n        [\"😍\"] = \"heart_eyes\",\n        [\"🥰\"] = \"smiling_face_with_3_hearts\",\n        [\"😘\"] = \"kissing_heart\",\n        [\"😗\"] = \"kissing\",\n        [\"😙\"] = \"kissing_smiling_eyes\",\n        [\"😚\"] = \"kissing_closed_eyes\",\n        [\"😋\"] = \"yum\",\n        [\"😛\"] = \"stuck_out_tongue\",\n        [\"😝\"] = \"stuck_out_tongue_closed_eyes\",\n        [\"😜\"] = \"stuck_out_tongue_winking_eye\",\n        [\"🤪\"] = \"zany_face\",\n        [\"🤨\"] = \"face_with_raised_eyebrow\",\n        [\"🧐\"] = \"face_with_monocle\",\n        [\"🤓\"] = \"nerd\",\n        [\"😎\"] = \"sunglasses\",\n        [\"🤩\"] = \"star_struck\",\n        [\"🥳\"] = \"partying_face\",\n        [\"😏\"] = \"smirk\",\n        [\"😒\"] = \"unamused\",\n        [\"😞\"] = \"disappointed\",\n        [\"😔\"] = \"pensive\",\n        [\"😟\"] = \"worried\",\n        [\"😕\"] = \"confused\",\n        [\"🙁\"] = \"slight_frown\",\n        [\"☹️\"] = \"frowning2\",\n        [\"😣\"] = \"persevere\",\n        [\"😖\"] = \"confounded\",\n        [\"😫\"] = \"tired_face\",\n        [\"😩\"] = \"weary\",\n        [\"🥺\"] = \"pleading_face\",\n        [\"😢\"] = \"cry\",\n        [\"😭\"] = \"sob\",\n        [\"😤\"] = \"triumph\",\n        [\"😮‍💨\"] = \"face_exhaling\",\n        [\"😠\"] = \"angry\",\n        [\"😡\"] = \"rage\",\n        [\"🤬\"] = \"face_with_symbols_over_mouth\",\n        [\"🤯\"] = \"exploding_head\",\n        [\"😳\"] = \"flushed\",\n        [\"😶‍🌫️\"] = \"face_in_clouds\",\n        [\"🥵\"] = \"hot_face\",\n        [\"🥶\"] = \"cold_face\",\n        [\"😱\"] = \"scream\",\n        [\"😨\"] = \"fearful\",\n        [\"😰\"] = \"cold_sweat\",\n        [\"😥\"] = \"disappointed_relieved\",\n        [\"😓\"] = \"sweat\",\n        [\"🤗\"] = \"hugging\",\n        [\"🤔\"] = \"thinking\",\n        [\"🤭\"] = \"face_with_hand_over_mouth\",\n        [\"🥱\"] = \"yawning_face\",\n        [\"🤫\"] = \"shushing_face\",\n        [\"🤥\"] = \"lying_face\",\n        [\"😶\"] = \"no_mouth\",\n        [\"😐\"] = \"neutral_face\",\n        [\"😑\"] = \"expressionless\",\n        [\"😬\"] = \"grimacing\",\n        [\"🙄\"] = \"rolling_eyes\",\n        [\"😯\"] = \"hushed\",\n        [\"😦\"] = \"frowning\",\n        [\"😧\"] = \"anguished\",\n        [\"😮\"] = \"open_mouth\",\n        [\"😲\"] = \"astonished\",\n        [\"😴\"] = \"sleeping\",\n        [\"🤤\"] = \"drooling_face\",\n        [\"😪\"] = \"sleepy\",\n        [\"😵\"] = \"dizzy_face\",\n        [\"😵‍💫\"] = \"face_with_spiral_eyes\",\n        [\"🤐\"] = \"zipper_mouth\",\n        [\"🥴\"] = \"woozy_face\",\n        [\"🤢\"] = \"nauseated_face\",\n        [\"🤮\"] = \"face_vomiting\",\n        [\"🤧\"] = \"sneezing_face\",\n        [\"😷\"] = \"mask\",\n        [\"🤒\"] = \"thermometer_face\",\n        [\"🤕\"] = \"head_bandage\",\n        [\"🤑\"] = \"money_mouth\",\n        [\"🤠\"] = \"cowboy\",\n        [\"🥸\"] = \"disguised_face\",\n        [\"😈\"] = \"smiling_imp\",\n        [\"👿\"] = \"imp\",\n        [\"👹\"] = \"japanese_ogre\",\n        [\"👺\"] = \"japanese_goblin\",\n        [\"🤡\"] = \"clown\",\n        [\"💩\"] = \"poop\",\n        [\"👻\"] = \"ghost\",\n        [\"💀\"] = \"skull\",\n        [\"☠️\"] = \"skull_crossbones\",\n        [\"👽\"] = \"alien\",\n        [\"👾\"] = \"space_invader\",\n        [\"🤖\"] = \"robot\",\n        [\"🎃\"] = \"jack_o_lantern\",\n        [\"😺\"] = \"smiley_cat\",\n        [\"😸\"] = \"smile_cat\",\n        [\"😹\"] = \"joy_cat\",\n        [\"😻\"] = \"heart_eyes_cat\",\n        [\"😼\"] = \"smirk_cat\",\n        [\"😽\"] = \"kissing_cat\",\n        [\"🙀\"] = \"scream_cat\",\n        [\"😿\"] = \"crying_cat_face\",\n        [\"😾\"] = \"pouting_cat\",\n        [\"🤲\"] = \"palms_up_together\",\n        [\"🤲🏻\"] = \"palms_up_together_tone1\",\n        [\"🤲🏼\"] = \"palms_up_together_tone2\",\n        [\"🤲🏽\"] = \"palms_up_together_tone3\",\n        [\"🤲🏾\"] = \"palms_up_together_tone4\",\n        [\"🤲🏿\"] = \"palms_up_together_tone5\",\n        [\"👐\"] = \"open_hands\",\n        [\"👐🏻\"] = \"open_hands_tone1\",\n        [\"👐🏼\"] = \"open_hands_tone2\",\n        [\"👐🏽\"] = \"open_hands_tone3\",\n        [\"👐🏾\"] = \"open_hands_tone4\",\n        [\"👐🏿\"] = \"open_hands_tone5\",\n        [\"🙌\"] = \"raised_hands\",\n        [\"🙌🏻\"] = \"raised_hands_tone1\",\n        [\"🙌🏼\"] = \"raised_hands_tone2\",\n        [\"🙌🏽\"] = \"raised_hands_tone3\",\n        [\"🙌🏾\"] = \"raised_hands_tone4\",\n        [\"🙌🏿\"] = \"raised_hands_tone5\",\n        [\"👏\"] = \"clap\",\n        [\"👏🏻\"] = \"clap_tone1\",\n        [\"👏🏼\"] = \"clap_tone2\",\n        [\"👏🏽\"] = \"clap_tone3\",\n        [\"👏🏾\"] = \"clap_tone4\",\n        [\"👏🏿\"] = \"clap_tone5\",\n        [\"🤝\"] = \"handshake\",\n        [\"👍\"] = \"thumbsup\",\n        [\"👍🏻\"] = \"thumbsup_tone1\",\n        [\"👍🏼\"] = \"thumbsup_tone2\",\n        [\"👍🏽\"] = \"thumbsup_tone3\",\n        [\"👍🏾\"] = \"thumbsup_tone4\",\n        [\"👍🏿\"] = \"thumbsup_tone5\",\n        [\"👎\"] = \"thumbsdown\",\n        [\"👎🏻\"] = \"thumbsdown_tone1\",\n        [\"👎🏼\"] = \"thumbsdown_tone2\",\n        [\"👎🏽\"] = \"thumbsdown_tone3\",\n        [\"👎🏾\"] = \"thumbsdown_tone4\",\n        [\"👎🏿\"] = \"thumbsdown_tone5\",\n        [\"👊\"] = \"punch\",\n        [\"👊🏻\"] = \"punch_tone1\",\n        [\"👊🏼\"] = \"punch_tone2\",\n        [\"👊🏽\"] = \"punch_tone3\",\n        [\"👊🏾\"] = \"punch_tone4\",\n        [\"👊🏿\"] = \"punch_tone5\",\n        [\"✊\"] = \"fist\",\n        [\"✊🏻\"] = \"fist_tone1\",\n        [\"✊🏼\"] = \"fist_tone2\",\n        [\"✊🏽\"] = \"fist_tone3\",\n        [\"✊🏾\"] = \"fist_tone4\",\n        [\"✊🏿\"] = \"fist_tone5\",\n        [\"🤛\"] = \"left_facing_fist\",\n        [\"🤛🏻\"] = \"left_facing_fist_tone1\",\n        [\"🤛🏼\"] = \"left_facing_fist_tone2\",\n        [\"🤛🏽\"] = \"left_facing_fist_tone3\",\n        [\"🤛🏾\"] = \"left_facing_fist_tone4\",\n        [\"🤛🏿\"] = \"left_facing_fist_tone5\",\n        [\"🤜\"] = \"right_facing_fist\",\n        [\"🤜🏻\"] = \"right_facing_fist_tone1\",\n        [\"🤜🏼\"] = \"right_facing_fist_tone2\",\n        [\"🤜🏽\"] = \"right_facing_fist_tone3\",\n        [\"🤜🏾\"] = \"right_facing_fist_tone4\",\n        [\"🤜🏿\"] = \"right_facing_fist_tone5\",\n        [\"🤞\"] = \"fingers_crossed\",\n        [\"🤞🏻\"] = \"fingers_crossed_tone1\",\n        [\"🤞🏼\"] = \"fingers_crossed_tone2\",\n        [\"🤞🏽\"] = \"fingers_crossed_tone3\",\n        [\"🤞🏾\"] = \"fingers_crossed_tone4\",\n        [\"🤞🏿\"] = \"fingers_crossed_tone5\",\n        [\"✌️\"] = \"v\",\n        [\"✌🏻\"] = \"v_tone1\",\n        [\"✌🏼\"] = \"v_tone2\",\n        [\"✌🏽\"] = \"v_tone3\",\n        [\"✌🏾\"] = \"v_tone4\",\n        [\"✌🏿\"] = \"v_tone5\",\n        [\"🤟\"] = \"love_you_gesture\",\n        [\"🤟🏻\"] = \"love_you_gesture_tone1\",\n        [\"🤟🏼\"] = \"love_you_gesture_tone2\",\n        [\"🤟🏽\"] = \"love_you_gesture_tone3\",\n        [\"🤟🏾\"] = \"love_you_gesture_tone4\",\n        [\"🤟🏿\"] = \"love_you_gesture_tone5\",\n        [\"🤘\"] = \"metal\",\n        [\"🤘🏻\"] = \"metal_tone1\",\n        [\"🤘🏼\"] = \"metal_tone2\",\n        [\"🤘🏽\"] = \"metal_tone3\",\n        [\"🤘🏾\"] = \"metal_tone4\",\n        [\"🤘🏿\"] = \"metal_tone5\",\n        [\"👌\"] = \"ok_hand\",\n        [\"👌🏻\"] = \"ok_hand_tone1\",\n        [\"👌🏼\"] = \"ok_hand_tone2\",\n        [\"👌🏽\"] = \"ok_hand_tone3\",\n        [\"👌🏾\"] = \"ok_hand_tone4\",\n        [\"👌🏿\"] = \"ok_hand_tone5\",\n        [\"🤏\"] = \"pinching_hand\",\n        [\"🤏🏻\"] = \"pinching_hand_tone1\",\n        [\"🤏🏼\"] = \"pinching_hand_tone2\",\n        [\"🤏🏽\"] = \"pinching_hand_tone3\",\n        [\"🤏🏾\"] = \"pinching_hand_tone4\",\n        [\"🤏🏿\"] = \"pinching_hand_tone5\",\n        [\"🤌\"] = \"pinched_fingers\",\n        [\"🤌🏼\"] = \"pinched_fingers_tone2\",\n        [\"🤌🏻\"] = \"pinched_fingers_tone1\",\n        [\"🤌🏽\"] = \"pinched_fingers_tone3\",\n        [\"🤌🏾\"] = \"pinched_fingers_tone4\",\n        [\"🤌🏿\"] = \"pinched_fingers_tone5\",\n        [\"👈\"] = \"point_left\",\n        [\"👈🏻\"] = \"point_left_tone1\",\n        [\"👈🏼\"] = \"point_left_tone2\",\n        [\"👈🏽\"] = \"point_left_tone3\",\n        [\"👈🏾\"] = \"point_left_tone4\",\n        [\"👈🏿\"] = \"point_left_tone5\",\n        [\"👉\"] = \"point_right\",\n        [\"👉🏻\"] = \"point_right_tone1\",\n        [\"👉🏼\"] = \"point_right_tone2\",\n        [\"👉🏽\"] = \"point_right_tone3\",\n        [\"👉🏾\"] = \"point_right_tone4\",\n        [\"👉🏿\"] = \"point_right_tone5\",\n        [\"👆\"] = \"point_up_2\",\n        [\"👆🏻\"] = \"point_up_2_tone1\",\n        [\"👆🏼\"] = \"point_up_2_tone2\",\n        [\"👆🏽\"] = \"point_up_2_tone3\",\n        [\"👆🏾\"] = \"point_up_2_tone4\",\n        [\"👆🏿\"] = \"point_up_2_tone5\",\n        [\"👇\"] = \"point_down\",\n        [\"👇🏻\"] = \"point_down_tone1\",\n        [\"👇🏼\"] = \"point_down_tone2\",\n        [\"👇🏽\"] = \"point_down_tone3\",\n        [\"👇🏾\"] = \"point_down_tone4\",\n        [\"👇🏿\"] = \"point_down_tone5\",\n        [\"☝️\"] = \"point_up\",\n        [\"☝🏻\"] = \"point_up_tone1\",\n        [\"☝🏼\"] = \"point_up_tone2\",\n        [\"☝🏽\"] = \"point_up_tone3\",\n        [\"☝🏾\"] = \"point_up_tone4\",\n        [\"☝🏿\"] = \"point_up_tone5\",\n        [\"✋\"] = \"raised_hand\",\n        [\"✋🏻\"] = \"raised_hand_tone1\",\n        [\"✋🏼\"] = \"raised_hand_tone2\",\n        [\"✋🏽\"] = \"raised_hand_tone3\",\n        [\"✋🏾\"] = \"raised_hand_tone4\",\n        [\"✋🏿\"] = \"raised_hand_tone5\",\n        [\"🤚\"] = \"raised_back_of_hand\",\n        [\"🤚🏻\"] = \"raised_back_of_hand_tone1\",\n        [\"🤚🏼\"] = \"raised_back_of_hand_tone2\",\n        [\"🤚🏽\"] = \"raised_back_of_hand_tone3\",\n        [\"🤚🏾\"] = \"raised_back_of_hand_tone4\",\n        [\"🤚🏿\"] = \"raised_back_of_hand_tone5\",\n        [\"🖐️\"] = \"hand_splayed\",\n        [\"🖐🏻\"] = \"hand_splayed_tone1\",\n        [\"🖐🏼\"] = \"hand_splayed_tone2\",\n        [\"🖐🏽\"] = \"hand_splayed_tone3\",\n        [\"🖐🏾\"] = \"hand_splayed_tone4\",\n        [\"🖐🏿\"] = \"hand_splayed_tone5\",\n        [\"🖖\"] = \"vulcan\",\n        [\"🖖🏻\"] = \"vulcan_tone1\",\n        [\"🖖🏼\"] = \"vulcan_tone2\",\n        [\"🖖🏽\"] = \"vulcan_tone3\",\n        [\"🖖🏾\"] = \"vulcan_tone4\",\n        [\"🖖🏿\"] = \"vulcan_tone5\",\n        [\"👋\"] = \"wave\",\n        [\"👋🏻\"] = \"wave_tone1\",\n        [\"👋🏼\"] = \"wave_tone2\",\n        [\"👋🏽\"] = \"wave_tone3\",\n        [\"👋🏾\"] = \"wave_tone4\",\n        [\"👋🏿\"] = \"wave_tone5\",\n        [\"🤙\"] = \"call_me\",\n        [\"🤙🏻\"] = \"call_me_tone1\",\n        [\"🤙🏼\"] = \"call_me_tone2\",\n        [\"🤙🏽\"] = \"call_me_tone3\",\n        [\"🤙🏾\"] = \"call_me_tone4\",\n        [\"🤙🏿\"] = \"call_me_tone5\",\n        [\"💪\"] = \"muscle\",\n        [\"💪🏻\"] = \"muscle_tone1\",\n        [\"💪🏼\"] = \"muscle_tone2\",\n        [\"💪🏽\"] = \"muscle_tone3\",\n        [\"💪🏾\"] = \"muscle_tone4\",\n        [\"💪🏿\"] = \"muscle_tone5\",\n        [\"🦾\"] = \"mechanical_arm\",\n        [\"🖕\"] = \"middle_finger\",\n        [\"🖕🏻\"] = \"middle_finger_tone1\",\n        [\"🖕🏼\"] = \"middle_finger_tone2\",\n        [\"🖕🏽\"] = \"middle_finger_tone3\",\n        [\"🖕🏾\"] = \"middle_finger_tone4\",\n        [\"🖕🏿\"] = \"middle_finger_tone5\",\n        [\"✍️\"] = \"writing_hand\",\n        [\"✍🏻\"] = \"writing_hand_tone1\",\n        [\"✍🏼\"] = \"writing_hand_tone2\",\n        [\"✍🏽\"] = \"writing_hand_tone3\",\n        [\"✍🏾\"] = \"writing_hand_tone4\",\n        [\"✍🏿\"] = \"writing_hand_tone5\",\n        [\"🙏\"] = \"pray\",\n        [\"🙏🏻\"] = \"pray_tone1\",\n        [\"🙏🏼\"] = \"pray_tone2\",\n        [\"🙏🏽\"] = \"pray_tone3\",\n        [\"🙏🏾\"] = \"pray_tone4\",\n        [\"🙏🏿\"] = \"pray_tone5\",\n        [\"🦶\"] = \"foot\",\n        [\"🦶🏻\"] = \"foot_tone1\",\n        [\"🦶🏼\"] = \"foot_tone2\",\n        [\"🦶🏽\"] = \"foot_tone3\",\n        [\"🦶🏾\"] = \"foot_tone4\",\n        [\"🦶🏿\"] = \"foot_tone5\",\n        [\"🦵\"] = \"leg\",\n        [\"🦵🏻\"] = \"leg_tone1\",\n        [\"🦵🏼\"] = \"leg_tone2\",\n        [\"🦵🏽\"] = \"leg_tone3\",\n        [\"🦵🏾\"] = \"leg_tone4\",\n        [\"🦵🏿\"] = \"leg_tone5\",\n        [\"🦿\"] = \"mechanical_leg\",\n        [\"💄\"] = \"lipstick\",\n        [\"💋\"] = \"kiss\",\n        [\"👄\"] = \"lips\",\n        [\"🦷\"] = \"tooth\",\n        [\"👅\"] = \"tongue\",\n        [\"👂\"] = \"ear\",\n        [\"👂🏻\"] = \"ear_tone1\",\n        [\"👂🏼\"] = \"ear_tone2\",\n        [\"👂🏽\"] = \"ear_tone3\",\n        [\"👂🏾\"] = \"ear_tone4\",\n        [\"👂🏿\"] = \"ear_tone5\",\n        [\"🦻\"] = \"ear_with_hearing_aid\",\n        [\"🦻🏻\"] = \"ear_with_hearing_aid_tone1\",\n        [\"🦻🏼\"] = \"ear_with_hearing_aid_tone2\",\n        [\"🦻🏽\"] = \"ear_with_hearing_aid_tone3\",\n        [\"🦻🏾\"] = \"ear_with_hearing_aid_tone4\",\n        [\"🦻🏿\"] = \"ear_with_hearing_aid_tone5\",\n        [\"👃\"] = \"nose\",\n        [\"👃🏻\"] = \"nose_tone1\",\n        [\"👃🏼\"] = \"nose_tone2\",\n        [\"👃🏽\"] = \"nose_tone3\",\n        [\"👃🏾\"] = \"nose_tone4\",\n        [\"👃🏿\"] = \"nose_tone5\",\n        [\"👣\"] = \"footprints\",\n        [\"👁️\"] = \"eye\",\n        [\"👀\"] = \"eyes\",\n        [\"🧠\"] = \"brain\",\n        [\"🫀\"] = \"anatomical_heart\",\n        [\"🫁\"] = \"lungs\",\n        [\"🦴\"] = \"bone\",\n        [\"🗣️\"] = \"speaking_head\",\n        [\"👤\"] = \"bust_in_silhouette\",\n        [\"👥\"] = \"busts_in_silhouette\",\n        [\"🫂\"] = \"people_hugging\",\n        [\"👶\"] = \"baby\",\n        [\"👶🏻\"] = \"baby_tone1\",\n        [\"👶🏼\"] = \"baby_tone2\",\n        [\"👶🏽\"] = \"baby_tone3\",\n        [\"👶🏾\"] = \"baby_tone4\",\n        [\"👶🏿\"] = \"baby_tone5\",\n        [\"👧\"] = \"girl\",\n        [\"👧🏻\"] = \"girl_tone1\",\n        [\"👧🏼\"] = \"girl_tone2\",\n        [\"👧🏽\"] = \"girl_tone3\",\n        [\"👧🏾\"] = \"girl_tone4\",\n        [\"👧🏿\"] = \"girl_tone5\",\n        [\"🧒\"] = \"child\",\n        [\"🧒🏻\"] = \"child_tone1\",\n        [\"🧒🏼\"] = \"child_tone2\",\n        [\"🧒🏽\"] = \"child_tone3\",\n        [\"🧒🏾\"] = \"child_tone4\",\n        [\"🧒🏿\"] = \"child_tone5\",\n        [\"👦\"] = \"boy\",\n        [\"👦🏻\"] = \"boy_tone1\",\n        [\"👦🏼\"] = \"boy_tone2\",\n        [\"👦🏽\"] = \"boy_tone3\",\n        [\"👦🏾\"] = \"boy_tone4\",\n        [\"👦🏿\"] = \"boy_tone5\",\n        [\"👩\"] = \"woman\",\n        [\"👩🏻\"] = \"woman_tone1\",\n        [\"👩🏼\"] = \"woman_tone2\",\n        [\"👩🏽\"] = \"woman_tone3\",\n        [\"👩🏾\"] = \"woman_tone4\",\n        [\"👩🏿\"] = \"woman_tone5\",\n        [\"🧑\"] = \"adult\",\n        [\"🧑🏻\"] = \"adult_tone1\",\n        [\"🧑🏼\"] = \"adult_tone2\",\n        [\"🧑🏽\"] = \"adult_tone3\",\n        [\"🧑🏾\"] = \"adult_tone4\",\n        [\"🧑🏿\"] = \"adult_tone5\",\n        [\"👨\"] = \"man\",\n        [\"👨🏻\"] = \"man_tone1\",\n        [\"👨🏼\"] = \"man_tone2\",\n        [\"👨🏽\"] = \"man_tone3\",\n        [\"👨🏾\"] = \"man_tone4\",\n        [\"👨🏿\"] = \"man_tone5\",\n        [\"🧑‍🦱\"] = \"person_curly_hair\",\n        [\"🧑🏻‍🦱\"] = \"person_tone1_curly_hair\",\n        [\"🧑🏼‍🦱\"] = \"person_tone2_curly_hair\",\n        [\"🧑🏽‍🦱\"] = \"person_tone3_curly_hair\",\n        [\"🧑🏾‍🦱\"] = \"person_tone4_curly_hair\",\n        [\"🧑🏿‍🦱\"] = \"person_tone5_curly_hair\",\n        [\"👩‍🦱\"] = \"woman_curly_haired\",\n        [\"👩🏻‍🦱\"] = \"woman_curly_haired_tone1\",\n        [\"👩🏼‍🦱\"] = \"woman_curly_haired_tone2\",\n        [\"👩🏽‍🦱\"] = \"woman_curly_haired_tone3\",\n        [\"👩🏾‍🦱\"] = \"woman_curly_haired_tone4\",\n        [\"👩🏿‍🦱\"] = \"woman_curly_haired_tone5\",\n        [\"👨‍🦱\"] = \"man_curly_haired\",\n        [\"👨🏻‍🦱\"] = \"man_curly_haired_tone1\",\n        [\"👨🏼‍🦱\"] = \"man_curly_haired_tone2\",\n        [\"👨🏽‍🦱\"] = \"man_curly_haired_tone3\",\n        [\"👨🏾‍🦱\"] = \"man_curly_haired_tone4\",\n        [\"👨🏿‍🦱\"] = \"man_curly_haired_tone5\",\n        [\"🧑‍🦰\"] = \"person_red_hair\",\n        [\"🧑🏻‍🦰\"] = \"person_tone1_red_hair\",\n        [\"🧑🏼‍🦰\"] = \"person_tone2_red_hair\",\n        [\"🧑🏽‍🦰\"] = \"person_tone3_red_hair\",\n        [\"🧑🏾‍🦰\"] = \"person_tone4_red_hair\",\n        [\"🧑🏿‍🦰\"] = \"person_tone5_red_hair\",\n        [\"👩‍🦰\"] = \"woman_red_haired\",\n        [\"👩🏻‍🦰\"] = \"woman_red_haired_tone1\",\n        [\"👩🏼‍🦰\"] = \"woman_red_haired_tone2\",\n        [\"👩🏽‍🦰\"] = \"woman_red_haired_tone3\",\n        [\"👩🏾‍🦰\"] = \"woman_red_haired_tone4\",\n        [\"👩🏿‍🦰\"] = \"woman_red_haired_tone5\",\n        [\"👨‍🦰\"] = \"man_red_haired\",\n        [\"👨🏻‍🦰\"] = \"man_red_haired_tone1\",\n        [\"👨🏼‍🦰\"] = \"man_red_haired_tone2\",\n        [\"👨🏽‍🦰\"] = \"man_red_haired_tone3\",\n        [\"👨🏾‍🦰\"] = \"man_red_haired_tone4\",\n        [\"👨🏿‍🦰\"] = \"man_red_haired_tone5\",\n        [\"👱‍♀️\"] = \"blond_haired_woman\",\n        [\"👱🏻‍♀️\"] = \"blond_haired_woman_tone1\",\n        [\"👱🏼‍♀️\"] = \"blond_haired_woman_tone2\",\n        [\"👱🏽‍♀️\"] = \"blond_haired_woman_tone3\",\n        [\"👱🏾‍♀️\"] = \"blond_haired_woman_tone4\",\n        [\"👱🏿‍♀️\"] = \"blond_haired_woman_tone5\",\n        [\"👱\"] = \"blond_haired_person\",\n        [\"👱🏻\"] = \"blond_haired_person_tone1\",\n        [\"👱🏼\"] = \"blond_haired_person_tone2\",\n        [\"👱🏽\"] = \"blond_haired_person_tone3\",\n        [\"👱🏾\"] = \"blond_haired_person_tone4\",\n        [\"👱🏿\"] = \"blond_haired_person_tone5\",\n        [\"👱‍♂️\"] = \"blond_haired_man\",\n        [\"👱🏻‍♂️\"] = \"blond_haired_man_tone1\",\n        [\"👱🏼‍♂️\"] = \"blond_haired_man_tone2\",\n        [\"👱🏽‍♂️\"] = \"blond_haired_man_tone3\",\n        [\"👱🏾‍♂️\"] = \"blond_haired_man_tone4\",\n        [\"👱🏿‍♂️\"] = \"blond_haired_man_tone5\",\n        [\"🧑‍🦳\"] = \"person_white_hair\",\n        [\"🧑🏻‍🦳\"] = \"person_tone1_white_hair\",\n        [\"🧑🏼‍🦳\"] = \"person_tone2_white_hair\",\n        [\"🧑🏽‍🦳\"] = \"person_tone3_white_hair\",\n        [\"🧑🏾‍🦳\"] = \"person_tone4_white_hair\",\n        [\"🧑🏿‍🦳\"] = \"person_tone5_white_hair\",\n        [\"👩‍🦳\"] = \"woman_white_haired\",\n        [\"👩🏻‍🦳\"] = \"woman_white_haired_tone1\",\n        [\"👩🏼‍🦳\"] = \"woman_white_haired_tone2\",\n        [\"👩🏽‍🦳\"] = \"woman_white_haired_tone3\",\n        [\"👩🏾‍🦳\"] = \"woman_white_haired_tone4\",\n        [\"👩🏿‍🦳\"] = \"woman_white_haired_tone5\",\n        [\"👨‍🦳\"] = \"man_white_haired\",\n        [\"👨🏻‍🦳\"] = \"man_white_haired_tone1\",\n        [\"👨🏼‍🦳\"] = \"man_white_haired_tone2\",\n        [\"👨🏽‍🦳\"] = \"man_white_haired_tone3\",\n        [\"👨🏾‍🦳\"] = \"man_white_haired_tone4\",\n        [\"👨🏿‍🦳\"] = \"man_white_haired_tone5\",\n        [\"🧑‍🦲\"] = \"person_bald\",\n        [\"🧑🏻‍🦲\"] = \"person_tone1_bald\",\n        [\"🧑🏼‍🦲\"] = \"person_tone2_bald\",\n        [\"🧑🏽‍🦲\"] = \"person_tone3_bald\",\n        [\"🧑🏾‍🦲\"] = \"person_tone4_bald\",\n        [\"🧑🏿‍🦲\"] = \"person_tone5_bald\",\n        [\"👩‍🦲\"] = \"woman_bald\",\n        [\"👩🏻‍🦲\"] = \"woman_bald_tone1\",\n        [\"👩🏼‍🦲\"] = \"woman_bald_tone2\",\n        [\"👩🏽‍🦲\"] = \"woman_bald_tone3\",\n        [\"👩🏾‍🦲\"] = \"woman_bald_tone4\",\n        [\"👩🏿‍🦲\"] = \"woman_bald_tone5\",\n        [\"👨‍🦲\"] = \"man_bald\",\n        [\"👨🏻‍🦲\"] = \"man_bald_tone1\",\n        [\"👨🏼‍🦲\"] = \"man_bald_tone2\",\n        [\"👨🏽‍🦲\"] = \"man_bald_tone3\",\n        [\"👨🏾‍🦲\"] = \"man_bald_tone4\",\n        [\"👨🏿‍🦲\"] = \"man_bald_tone5\",\n        [\"🧔\"] = \"bearded_person\",\n        [\"🧔🏻\"] = \"bearded_person_tone1\",\n        [\"🧔🏼\"] = \"bearded_person_tone2\",\n        [\"🧔🏽\"] = \"bearded_person_tone3\",\n        [\"🧔🏾\"] = \"bearded_person_tone4\",\n        [\"🧔🏿\"] = \"bearded_person_tone5\",\n        [\"🧔‍♂️\"] = \"man_beard\",\n        [\"🧔🏻‍♂️\"] = \"man_tone1_beard\",\n        [\"🧔🏼‍♂️\"] = \"man_tone2_beard\",\n        [\"🧔🏽‍♂️\"] = \"man_tone3_beard\",\n        [\"🧔🏾‍♂️\"] = \"man_tone4_beard\",\n        [\"🧔🏿‍♂️\"] = \"man_tone5_beard\",\n        [\"🧔‍♀️\"] = \"woman_beard\",\n        [\"🧔🏻‍♀️\"] = \"woman_tone1_beard\",\n        [\"🧔🏼‍♀️\"] = \"woman_tone2_beard\",\n        [\"🧔🏽‍♀️\"] = \"woman_tone3_beard\",\n        [\"🧔🏾‍♀️\"] = \"woman_tone4_beard\",\n        [\"🧔🏿‍♀️\"] = \"woman_tone5_beard\",\n        [\"👵\"] = \"older_woman\",\n        [\"👵🏻\"] = \"older_woman_tone1\",\n        [\"👵🏼\"] = \"older_woman_tone2\",\n        [\"👵🏽\"] = \"older_woman_tone3\",\n        [\"👵🏾\"] = \"older_woman_tone4\",\n        [\"👵🏿\"] = \"older_woman_tone5\",\n        [\"🧓\"] = \"older_adult\",\n        [\"🧓🏻\"] = \"older_adult_tone1\",\n        [\"🧓🏼\"] = \"older_adult_tone2\",\n        [\"🧓🏽\"] = \"older_adult_tone3\",\n        [\"🧓🏾\"] = \"older_adult_tone4\",\n        [\"🧓🏿\"] = \"older_adult_tone5\",\n        [\"👴\"] = \"older_man\",\n        [\"👴🏻\"] = \"older_man_tone1\",\n        [\"👴🏼\"] = \"older_man_tone2\",\n        [\"👴🏽\"] = \"older_man_tone3\",\n        [\"👴🏾\"] = \"older_man_tone4\",\n        [\"👴🏿\"] = \"older_man_tone5\",\n        [\"👲\"] = \"man_with_chinese_cap\",\n        [\"👲🏻\"] = \"man_with_chinese_cap_tone1\",\n        [\"👲🏼\"] = \"man_with_chinese_cap_tone2\",\n        [\"👲🏽\"] = \"man_with_chinese_cap_tone3\",\n        [\"👲🏾\"] = \"man_with_chinese_cap_tone4\",\n        [\"👲🏿\"] = \"man_with_chinese_cap_tone5\",\n        [\"👳\"] = \"person_wearing_turban\",\n        [\"👳🏻\"] = \"person_wearing_turban_tone1\",\n        [\"👳🏼\"] = \"person_wearing_turban_tone2\",\n        [\"👳🏽\"] = \"person_wearing_turban_tone3\",\n        [\"👳🏾\"] = \"person_wearing_turban_tone4\",\n        [\"👳🏿\"] = \"person_wearing_turban_tone5\",\n        [\"👳‍♀️\"] = \"woman_wearing_turban\",\n        [\"👳🏻‍♀️\"] = \"woman_wearing_turban_tone1\",\n        [\"👳🏼‍♀️\"] = \"woman_wearing_turban_tone2\",\n        [\"👳🏽‍♀️\"] = \"woman_wearing_turban_tone3\",\n        [\"👳🏾‍♀️\"] = \"woman_wearing_turban_tone4\",\n        [\"👳🏿‍♀️\"] = \"woman_wearing_turban_tone5\",\n        [\"👳‍♂️\"] = \"man_wearing_turban\",\n        [\"👳🏻‍♂️\"] = \"man_wearing_turban_tone1\",\n        [\"👳🏼‍♂️\"] = \"man_wearing_turban_tone2\",\n        [\"👳🏽‍♂️\"] = \"man_wearing_turban_tone3\",\n        [\"👳🏾‍♂️\"] = \"man_wearing_turban_tone4\",\n        [\"👳🏿‍♂️\"] = \"man_wearing_turban_tone5\",\n        [\"🧕\"] = \"woman_with_headscarf\",\n        [\"🧕🏻\"] = \"woman_with_headscarf_tone1\",\n        [\"🧕🏼\"] = \"woman_with_headscarf_tone2\",\n        [\"🧕🏽\"] = \"woman_with_headscarf_tone3\",\n        [\"🧕🏾\"] = \"woman_with_headscarf_tone4\",\n        [\"🧕🏿\"] = \"woman_with_headscarf_tone5\",\n        [\"👮\"] = \"police_officer\",\n        [\"👮🏻\"] = \"police_officer_tone1\",\n        [\"👮🏼\"] = \"police_officer_tone2\",\n        [\"👮🏽\"] = \"police_officer_tone3\",\n        [\"👮🏾\"] = \"police_officer_tone4\",\n        [\"👮🏿\"] = \"police_officer_tone5\",\n        [\"👮‍♀️\"] = \"woman_police_officer\",\n        [\"👮🏻‍♀️\"] = \"woman_police_officer_tone1\",\n        [\"👮🏼‍♀️\"] = \"woman_police_officer_tone2\",\n        [\"👮🏽‍♀️\"] = \"woman_police_officer_tone3\",\n        [\"👮🏾‍♀️\"] = \"woman_police_officer_tone4\",\n        [\"👮🏿‍♀️\"] = \"woman_police_officer_tone5\",\n        [\"👮‍♂️\"] = \"man_police_officer\",\n        [\"👮🏻‍♂️\"] = \"man_police_officer_tone1\",\n        [\"👮🏼‍♂️\"] = \"man_police_officer_tone2\",\n        [\"👮🏽‍♂️\"] = \"man_police_officer_tone3\",\n        [\"👮🏾‍♂️\"] = \"man_police_officer_tone4\",\n        [\"👮🏿‍♂️\"] = \"man_police_officer_tone5\",\n        [\"👷\"] = \"construction_worker\",\n        [\"👷🏻\"] = \"construction_worker_tone1\",\n        [\"👷🏼\"] = \"construction_worker_tone2\",\n        [\"👷🏽\"] = \"construction_worker_tone3\",\n        [\"👷🏾\"] = \"construction_worker_tone4\",\n        [\"👷🏿\"] = \"construction_worker_tone5\",\n        [\"👷‍♀️\"] = \"woman_construction_worker\",\n        [\"👷🏻‍♀️\"] = \"woman_construction_worker_tone1\",\n        [\"👷🏼‍♀️\"] = \"woman_construction_worker_tone2\",\n        [\"👷🏽‍♀️\"] = \"woman_construction_worker_tone3\",\n        [\"👷🏾‍♀️\"] = \"woman_construction_worker_tone4\",\n        [\"👷🏿‍♀️\"] = \"woman_construction_worker_tone5\",\n        [\"👷‍♂️\"] = \"man_construction_worker\",\n        [\"👷🏻‍♂️\"] = \"man_construction_worker_tone1\",\n        [\"👷🏼‍♂️\"] = \"man_construction_worker_tone2\",\n        [\"👷🏽‍♂️\"] = \"man_construction_worker_tone3\",\n        [\"👷🏾‍♂️\"] = \"man_construction_worker_tone4\",\n        [\"👷🏿‍♂️\"] = \"man_construction_worker_tone5\",\n        [\"💂\"] = \"guard\",\n        [\"💂🏻\"] = \"guard_tone1\",\n        [\"💂🏼\"] = \"guard_tone2\",\n        [\"💂🏽\"] = \"guard_tone3\",\n        [\"💂🏾\"] = \"guard_tone4\",\n        [\"💂🏿\"] = \"guard_tone5\",\n        [\"💂‍♀️\"] = \"woman_guard\",\n        [\"💂🏻‍♀️\"] = \"woman_guard_tone1\",\n        [\"💂🏼‍♀️\"] = \"woman_guard_tone2\",\n        [\"💂🏽‍♀️\"] = \"woman_guard_tone3\",\n        [\"💂🏾‍♀️\"] = \"woman_guard_tone4\",\n        [\"💂🏿‍♀️\"] = \"woman_guard_tone5\",\n        [\"💂‍♂️\"] = \"man_guard\",\n        [\"💂🏻‍♂️\"] = \"man_guard_tone1\",\n        [\"💂🏼‍♂️\"] = \"man_guard_tone2\",\n        [\"💂🏽‍♂️\"] = \"man_guard_tone3\",\n        [\"💂🏾‍♂️\"] = \"man_guard_tone4\",\n        [\"💂🏿‍♂️\"] = \"man_guard_tone5\",\n        [\"🕵️\"] = \"detective\",\n        [\"🕵🏻\"] = \"detective_tone1\",\n        [\"🕵🏼\"] = \"detective_tone2\",\n        [\"🕵🏽\"] = \"detective_tone3\",\n        [\"🕵🏾\"] = \"detective_tone4\",\n        [\"🕵🏿\"] = \"detective_tone5\",\n        [\"🕵️‍♀️\"] = \"woman_detective\",\n        [\"🕵🏻‍♀️\"] = \"woman_detective_tone1\",\n        [\"🕵🏼‍♀️\"] = \"woman_detective_tone2\",\n        [\"🕵🏽‍♀️\"] = \"woman_detective_tone3\",\n        [\"🕵🏾‍♀️\"] = \"woman_detective_tone4\",\n        [\"🕵🏿‍♀️\"] = \"woman_detective_tone5\",\n        [\"🕵️‍♂️\"] = \"man_detective\",\n        [\"🕵🏻‍♂️\"] = \"man_detective_tone1\",\n        [\"🕵🏼‍♂️\"] = \"man_detective_tone2\",\n        [\"🕵🏽‍♂️\"] = \"man_detective_tone3\",\n        [\"🕵🏾‍♂️\"] = \"man_detective_tone4\",\n        [\"🕵🏿‍♂️\"] = \"man_detective_tone5\",\n        [\"🧑‍⚕️\"] = \"health_worker\",\n        [\"🧑🏻‍⚕️\"] = \"health_worker_tone1\",\n        [\"🧑🏼‍⚕️\"] = \"health_worker_tone2\",\n        [\"🧑🏽‍⚕️\"] = \"health_worker_tone3\",\n        [\"🧑🏾‍⚕️\"] = \"health_worker_tone4\",\n        [\"🧑🏿‍⚕️\"] = \"health_worker_tone5\",\n        [\"👩‍⚕️\"] = \"woman_health_worker\",\n        [\"👩🏻‍⚕️\"] = \"woman_health_worker_tone1\",\n        [\"👩🏼‍⚕️\"] = \"woman_health_worker_tone2\",\n        [\"👩🏽‍⚕️\"] = \"woman_health_worker_tone3\",\n        [\"👩🏾‍⚕️\"] = \"woman_health_worker_tone4\",\n        [\"👩🏿‍⚕️\"] = \"woman_health_worker_tone5\",\n        [\"👨‍⚕️\"] = \"man_health_worker\",\n        [\"👨🏻‍⚕️\"] = \"man_health_worker_tone1\",\n        [\"👨🏼‍⚕️\"] = \"man_health_worker_tone2\",\n        [\"👨🏽‍⚕️\"] = \"man_health_worker_tone3\",\n        [\"👨🏾‍⚕️\"] = \"man_health_worker_tone4\",\n        [\"👨🏿‍⚕️\"] = \"man_health_worker_tone5\",\n        [\"🧑‍🌾\"] = \"farmer\",\n        [\"🧑🏻‍🌾\"] = \"farmer_tone1\",\n        [\"🧑🏼‍🌾\"] = \"farmer_tone2\",\n        [\"🧑🏽‍🌾\"] = \"farmer_tone3\",\n        [\"🧑🏾‍🌾\"] = \"farmer_tone4\",\n        [\"🧑🏿‍🌾\"] = \"farmer_tone5\",\n        [\"👩‍🌾\"] = \"woman_farmer\",\n        [\"👩🏻‍🌾\"] = \"woman_farmer_tone1\",\n        [\"👩🏼‍🌾\"] = \"woman_farmer_tone2\",\n        [\"👩🏽‍🌾\"] = \"woman_farmer_tone3\",\n        [\"👩🏾‍🌾\"] = \"woman_farmer_tone4\",\n        [\"👩🏿‍🌾\"] = \"woman_farmer_tone5\",\n        [\"👨‍🌾\"] = \"man_farmer\",\n        [\"👨🏻‍🌾\"] = \"man_farmer_tone1\",\n        [\"👨🏼‍🌾\"] = \"man_farmer_tone2\",\n        [\"👨🏽‍🌾\"] = \"man_farmer_tone3\",\n        [\"👨🏾‍🌾\"] = \"man_farmer_tone4\",\n        [\"👨🏿‍🌾\"] = \"man_farmer_tone5\",\n        [\"🧑‍🍳\"] = \"cook\",\n        [\"🧑🏻‍🍳\"] = \"cook_tone1\",\n        [\"🧑🏼‍🍳\"] = \"cook_tone2\",\n        [\"🧑🏽‍🍳\"] = \"cook_tone3\",\n        [\"🧑🏾‍🍳\"] = \"cook_tone4\",\n        [\"🧑🏿‍🍳\"] = \"cook_tone5\",\n        [\"👩‍🍳\"] = \"woman_cook\",\n        [\"👩🏻‍🍳\"] = \"woman_cook_tone1\",\n        [\"👩🏼‍🍳\"] = \"woman_cook_tone2\",\n        [\"👩🏽‍🍳\"] = \"woman_cook_tone3\",\n        [\"👩🏾‍🍳\"] = \"woman_cook_tone4\",\n        [\"👩🏿‍🍳\"] = \"woman_cook_tone5\",\n        [\"👨‍🍳\"] = \"man_cook\",\n        [\"👨🏻‍🍳\"] = \"man_cook_tone1\",\n        [\"👨🏼‍🍳\"] = \"man_cook_tone2\",\n        [\"👨🏽‍🍳\"] = \"man_cook_tone3\",\n        [\"👨🏾‍🍳\"] = \"man_cook_tone4\",\n        [\"👨🏿‍🍳\"] = \"man_cook_tone5\",\n        [\"🧑‍🎓\"] = \"student\",\n        [\"🧑🏻‍🎓\"] = \"student_tone1\",\n        [\"🧑🏼‍🎓\"] = \"student_tone2\",\n        [\"🧑🏽‍🎓\"] = \"student_tone3\",\n        [\"🧑🏾‍🎓\"] = \"student_tone4\",\n        [\"🧑🏿‍🎓\"] = \"student_tone5\",\n        [\"👩‍🎓\"] = \"woman_student\",\n        [\"👩🏻‍🎓\"] = \"woman_student_tone1\",\n        [\"👩🏼‍🎓\"] = \"woman_student_tone2\",\n        [\"👩🏽‍🎓\"] = \"woman_student_tone3\",\n        [\"👩🏾‍🎓\"] = \"woman_student_tone4\",\n        [\"👩🏿‍🎓\"] = \"woman_student_tone5\",\n        [\"👨‍🎓\"] = \"man_student\",\n        [\"👨🏻‍🎓\"] = \"man_student_tone1\",\n        [\"👨🏼‍🎓\"] = \"man_student_tone2\",\n        [\"👨🏽‍🎓\"] = \"man_student_tone3\",\n        [\"👨🏾‍🎓\"] = \"man_student_tone4\",\n        [\"👨🏿‍🎓\"] = \"man_student_tone5\",\n        [\"🧑‍🎤\"] = \"singer\",\n        [\"🧑🏻‍🎤\"] = \"singer_tone1\",\n        [\"🧑🏼‍🎤\"] = \"singer_tone2\",\n        [\"🧑🏽‍🎤\"] = \"singer_tone3\",\n        [\"🧑🏾‍🎤\"] = \"singer_tone4\",\n        [\"🧑🏿‍🎤\"] = \"singer_tone5\",\n        [\"👩‍🎤\"] = \"woman_singer\",\n        [\"👩🏻‍🎤\"] = \"woman_singer_tone1\",\n        [\"👩🏼‍🎤\"] = \"woman_singer_tone2\",\n        [\"👩🏽‍🎤\"] = \"woman_singer_tone3\",\n        [\"👩🏾‍🎤\"] = \"woman_singer_tone4\",\n        [\"👩🏿‍🎤\"] = \"woman_singer_tone5\",\n        [\"👨‍🎤\"] = \"man_singer\",\n        [\"👨🏻‍🎤\"] = \"man_singer_tone1\",\n        [\"👨🏼‍🎤\"] = \"man_singer_tone2\",\n        [\"👨🏽‍🎤\"] = \"man_singer_tone3\",\n        [\"👨🏾‍🎤\"] = \"man_singer_tone4\",\n        [\"👨🏿‍🎤\"] = \"man_singer_tone5\",\n        [\"🧑‍🏫\"] = \"teacher\",\n        [\"🧑🏻‍🏫\"] = \"teacher_tone1\",\n        [\"🧑🏼‍🏫\"] = \"teacher_tone2\",\n        [\"🧑🏽‍🏫\"] = \"teacher_tone3\",\n        [\"🧑🏾‍🏫\"] = \"teacher_tone4\",\n        [\"🧑🏿‍🏫\"] = \"teacher_tone5\",\n        [\"👩‍🏫\"] = \"woman_teacher\",\n        [\"👩🏻‍🏫\"] = \"woman_teacher_tone1\",\n        [\"👩🏼‍🏫\"] = \"woman_teacher_tone2\",\n        [\"👩🏽‍🏫\"] = \"woman_teacher_tone3\",\n        [\"👩🏾‍🏫\"] = \"woman_teacher_tone4\",\n        [\"👩🏿‍🏫\"] = \"woman_teacher_tone5\",\n        [\"👨‍🏫\"] = \"man_teacher\",\n        [\"👨🏻‍🏫\"] = \"man_teacher_tone1\",\n        [\"👨🏼‍🏫\"] = \"man_teacher_tone2\",\n        [\"👨🏽‍🏫\"] = \"man_teacher_tone3\",\n        [\"👨🏾‍🏫\"] = \"man_teacher_tone4\",\n        [\"👨🏿‍🏫\"] = \"man_teacher_tone5\",\n        [\"🧑‍🏭\"] = \"factory_worker\",\n        [\"🧑🏻‍🏭\"] = \"factory_worker_tone1\",\n        [\"🧑🏼‍🏭\"] = \"factory_worker_tone2\",\n        [\"🧑🏽‍🏭\"] = \"factory_worker_tone3\",\n        [\"🧑🏾‍🏭\"] = \"factory_worker_tone4\",\n        [\"🧑🏿‍🏭\"] = \"factory_worker_tone5\",\n        [\"👩‍🏭\"] = \"woman_factory_worker\",\n        [\"👩🏻‍🏭\"] = \"woman_factory_worker_tone1\",\n        [\"👩🏼‍🏭\"] = \"woman_factory_worker_tone2\",\n        [\"👩🏽‍🏭\"] = \"woman_factory_worker_tone3\",\n        [\"👩🏾‍🏭\"] = \"woman_factory_worker_tone4\",\n        [\"👩🏿‍🏭\"] = \"woman_factory_worker_tone5\",\n        [\"👨‍🏭\"] = \"man_factory_worker\",\n        [\"👨🏻‍🏭\"] = \"man_factory_worker_tone1\",\n        [\"👨🏼‍🏭\"] = \"man_factory_worker_tone2\",\n        [\"👨🏽‍🏭\"] = \"man_factory_worker_tone3\",\n        [\"👨🏾‍🏭\"] = \"man_factory_worker_tone4\",\n        [\"👨🏿‍🏭\"] = \"man_factory_worker_tone5\",\n        [\"🧑‍💻\"] = \"technologist\",\n        [\"🧑🏻‍💻\"] = \"technologist_tone1\",\n        [\"🧑🏼‍💻\"] = \"technologist_tone2\",\n        [\"🧑🏽‍💻\"] = \"technologist_tone3\",\n        [\"🧑🏾‍💻\"] = \"technologist_tone4\",\n        [\"🧑🏿‍💻\"] = \"technologist_tone5\",\n        [\"👩‍💻\"] = \"woman_technologist\",\n        [\"👩🏻‍💻\"] = \"woman_technologist_tone1\",\n        [\"👩🏼‍💻\"] = \"woman_technologist_tone2\",\n        [\"👩🏽‍💻\"] = \"woman_technologist_tone3\",\n        [\"👩🏾‍💻\"] = \"woman_technologist_tone4\",\n        [\"👩🏿‍💻\"] = \"woman_technologist_tone5\",\n        [\"👨‍💻\"] = \"man_technologist\",\n        [\"👨🏻‍💻\"] = \"man_technologist_tone1\",\n        [\"👨🏼‍💻\"] = \"man_technologist_tone2\",\n        [\"👨🏽‍💻\"] = \"man_technologist_tone3\",\n        [\"👨🏾‍💻\"] = \"man_technologist_tone4\",\n        [\"👨🏿‍💻\"] = \"man_technologist_tone5\",\n        [\"🧑‍💼\"] = \"office_worker\",\n        [\"🧑🏻‍💼\"] = \"office_worker_tone1\",\n        [\"🧑🏼‍💼\"] = \"office_worker_tone2\",\n        [\"🧑🏽‍💼\"] = \"office_worker_tone3\",\n        [\"🧑🏾‍💼\"] = \"office_worker_tone4\",\n        [\"🧑🏿‍💼\"] = \"office_worker_tone5\",\n        [\"👩‍💼\"] = \"woman_office_worker\",\n        [\"👩🏻‍💼\"] = \"woman_office_worker_tone1\",\n        [\"👩🏼‍💼\"] = \"woman_office_worker_tone2\",\n        [\"👩🏽‍💼\"] = \"woman_office_worker_tone3\",\n        [\"👩🏾‍💼\"] = \"woman_office_worker_tone4\",\n        [\"👩🏿‍💼\"] = \"woman_office_worker_tone5\",\n        [\"👨‍💼\"] = \"man_office_worker\",\n        [\"👨🏻‍💼\"] = \"man_office_worker_tone1\",\n        [\"👨🏼‍💼\"] = \"man_office_worker_tone2\",\n        [\"👨🏽‍💼\"] = \"man_office_worker_tone3\",\n        [\"👨🏾‍💼\"] = \"man_office_worker_tone4\",\n        [\"👨🏿‍💼\"] = \"man_office_worker_tone5\",\n        [\"🧑‍🔧\"] = \"mechanic\",\n        [\"🧑🏻‍🔧\"] = \"mechanic_tone1\",\n        [\"🧑🏼‍🔧\"] = \"mechanic_tone2\",\n        [\"🧑🏽‍🔧\"] = \"mechanic_tone3\",\n        [\"🧑🏾‍🔧\"] = \"mechanic_tone4\",\n        [\"🧑🏿‍🔧\"] = \"mechanic_tone5\",\n        [\"👩‍🔧\"] = \"woman_mechanic\",\n        [\"👩🏻‍🔧\"] = \"woman_mechanic_tone1\",\n        [\"👩🏼‍🔧\"] = \"woman_mechanic_tone2\",\n        [\"👩🏽‍🔧\"] = \"woman_mechanic_tone3\",\n        [\"👩🏾‍🔧\"] = \"woman_mechanic_tone4\",\n        [\"👩🏿‍🔧\"] = \"woman_mechanic_tone5\",\n        [\"👨‍🔧\"] = \"man_mechanic\",\n        [\"👨🏻‍🔧\"] = \"man_mechanic_tone1\",\n        [\"👨🏼‍🔧\"] = \"man_mechanic_tone2\",\n        [\"👨🏽‍🔧\"] = \"man_mechanic_tone3\",\n        [\"👨🏾‍🔧\"] = \"man_mechanic_tone4\",\n        [\"👨🏿‍🔧\"] = \"man_mechanic_tone5\",\n        [\"🧑‍🔬\"] = \"scientist\",\n        [\"🧑🏻‍🔬\"] = \"scientist_tone1\",\n        [\"🧑🏼‍🔬\"] = \"scientist_tone2\",\n        [\"🧑🏽‍🔬\"] = \"scientist_tone3\",\n        [\"🧑🏾‍🔬\"] = \"scientist_tone4\",\n        [\"🧑🏿‍🔬\"] = \"scientist_tone5\",\n        [\"👩‍🔬\"] = \"woman_scientist\",\n        [\"👩🏻‍🔬\"] = \"woman_scientist_tone1\",\n        [\"👩🏼‍🔬\"] = \"woman_scientist_tone2\",\n        [\"👩🏽‍🔬\"] = \"woman_scientist_tone3\",\n        [\"👩🏾‍🔬\"] = \"woman_scientist_tone4\",\n        [\"👩🏿‍🔬\"] = \"woman_scientist_tone5\",\n        [\"👨‍🔬\"] = \"man_scientist\",\n        [\"👨🏻‍🔬\"] = \"man_scientist_tone1\",\n        [\"👨🏼‍🔬\"] = \"man_scientist_tone2\",\n        [\"👨🏽‍🔬\"] = \"man_scientist_tone3\",\n        [\"👨🏾‍🔬\"] = \"man_scientist_tone4\",\n        [\"👨🏿‍🔬\"] = \"man_scientist_tone5\",\n        [\"🧑‍🎨\"] = \"artist\",\n        [\"🧑🏻‍🎨\"] = \"artist_tone1\",\n        [\"🧑🏼‍🎨\"] = \"artist_tone2\",\n        [\"🧑🏽‍🎨\"] = \"artist_tone3\",\n        [\"🧑🏾‍🎨\"] = \"artist_tone4\",\n        [\"🧑🏿‍🎨\"] = \"artist_tone5\",\n        [\"👩‍🎨\"] = \"woman_artist\",\n        [\"👩🏻‍🎨\"] = \"woman_artist_tone1\",\n        [\"👩🏼‍🎨\"] = \"woman_artist_tone2\",\n        [\"👩🏽‍🎨\"] = \"woman_artist_tone3\",\n        [\"👩🏾‍🎨\"] = \"woman_artist_tone4\",\n        [\"👩🏿‍🎨\"] = \"woman_artist_tone5\",\n        [\"👨‍🎨\"] = \"man_artist\",\n        [\"👨🏻‍🎨\"] = \"man_artist_tone1\",\n        [\"👨🏼‍🎨\"] = \"man_artist_tone2\",\n        [\"👨🏽‍🎨\"] = \"man_artist_tone3\",\n        [\"👨🏾‍🎨\"] = \"man_artist_tone4\",\n        [\"👨🏿‍🎨\"] = \"man_artist_tone5\",\n        [\"🧑‍🚒\"] = \"firefighter\",\n        [\"🧑🏻‍🚒\"] = \"firefighter_tone1\",\n        [\"🧑🏼‍🚒\"] = \"firefighter_tone2\",\n        [\"🧑🏽‍🚒\"] = \"firefighter_tone3\",\n        [\"🧑🏾‍🚒\"] = \"firefighter_tone4\",\n        [\"🧑🏿‍🚒\"] = \"firefighter_tone5\",\n        [\"👩‍🚒\"] = \"woman_firefighter\",\n        [\"👩🏻‍🚒\"] = \"woman_firefighter_tone1\",\n        [\"👩🏼‍🚒\"] = \"woman_firefighter_tone2\",\n        [\"👩🏽‍🚒\"] = \"woman_firefighter_tone3\",\n        [\"👩🏾‍🚒\"] = \"woman_firefighter_tone4\",\n        [\"👩🏿‍🚒\"] = \"woman_firefighter_tone5\",\n        [\"👨‍🚒\"] = \"man_firefighter\",\n        [\"👨🏻‍🚒\"] = \"man_firefighter_tone1\",\n        [\"👨🏼‍🚒\"] = \"man_firefighter_tone2\",\n        [\"👨🏽‍🚒\"] = \"man_firefighter_tone3\",\n        [\"👨🏾‍🚒\"] = \"man_firefighter_tone4\",\n        [\"👨🏿‍🚒\"] = \"man_firefighter_tone5\",\n        [\"🧑‍✈️\"] = \"pilot\",\n        [\"🧑🏻‍✈️\"] = \"pilot_tone1\",\n        [\"🧑🏼‍✈️\"] = \"pilot_tone2\",\n        [\"🧑🏽‍✈️\"] = \"pilot_tone3\",\n        [\"🧑🏾‍✈️\"] = \"pilot_tone4\",\n        [\"🧑🏿‍✈️\"] = \"pilot_tone5\",\n        [\"👩‍✈️\"] = \"woman_pilot\",\n        [\"👩🏻‍✈️\"] = \"woman_pilot_tone1\",\n        [\"👩🏼‍✈️\"] = \"woman_pilot_tone2\",\n        [\"👩🏽‍✈️\"] = \"woman_pilot_tone3\",\n        [\"👩🏾‍✈️\"] = \"woman_pilot_tone4\",\n        [\"👩🏿‍✈️\"] = \"woman_pilot_tone5\",\n        [\"👨‍✈️\"] = \"man_pilot\",\n        [\"👨🏻‍✈️\"] = \"man_pilot_tone1\",\n        [\"👨🏼‍✈️\"] = \"man_pilot_tone2\",\n        [\"👨🏽‍✈️\"] = \"man_pilot_tone3\",\n        [\"👨🏾‍✈️\"] = \"man_pilot_tone4\",\n        [\"👨🏿‍✈️\"] = \"man_pilot_tone5\",\n        [\"🧑‍🚀\"] = \"astronaut\",\n        [\"🧑🏻‍🚀\"] = \"astronaut_tone1\",\n        [\"🧑🏼‍🚀\"] = \"astronaut_tone2\",\n        [\"🧑🏽‍🚀\"] = \"astronaut_tone3\",\n        [\"🧑🏾‍🚀\"] = \"astronaut_tone4\",\n        [\"🧑🏿‍🚀\"] = \"astronaut_tone5\",\n        [\"👩‍🚀\"] = \"woman_astronaut\",\n        [\"👩🏻‍🚀\"] = \"woman_astronaut_tone1\",\n        [\"👩🏼‍🚀\"] = \"woman_astronaut_tone2\",\n        [\"👩🏽‍🚀\"] = \"woman_astronaut_tone3\",\n        [\"👩🏾‍🚀\"] = \"woman_astronaut_tone4\",\n        [\"👩🏿‍🚀\"] = \"woman_astronaut_tone5\",\n        [\"👨‍🚀\"] = \"man_astronaut\",\n        [\"👨🏻‍🚀\"] = \"man_astronaut_tone1\",\n        [\"👨🏼‍🚀\"] = \"man_astronaut_tone2\",\n        [\"👨🏽‍🚀\"] = \"man_astronaut_tone3\",\n        [\"👨🏾‍🚀\"] = \"man_astronaut_tone4\",\n        [\"👨🏿‍🚀\"] = \"man_astronaut_tone5\",\n        [\"🧑‍⚖️\"] = \"judge\",\n        [\"🧑🏻‍⚖️\"] = \"judge_tone1\",\n        [\"🧑🏼‍⚖️\"] = \"judge_tone2\",\n        [\"🧑🏽‍⚖️\"] = \"judge_tone3\",\n        [\"🧑🏾‍⚖️\"] = \"judge_tone4\",\n        [\"🧑🏿‍⚖️\"] = \"judge_tone5\",\n        [\"👩‍⚖️\"] = \"woman_judge\",\n        [\"👩🏻‍⚖️\"] = \"woman_judge_tone1\",\n        [\"👩🏼‍⚖️\"] = \"woman_judge_tone2\",\n        [\"👩🏽‍⚖️\"] = \"woman_judge_tone3\",\n        [\"👩🏾‍⚖️\"] = \"woman_judge_tone4\",\n        [\"👩🏿‍⚖️\"] = \"woman_judge_tone5\",\n        [\"👨‍⚖️\"] = \"man_judge\",\n        [\"👨🏻‍⚖️\"] = \"man_judge_tone1\",\n        [\"👨🏼‍⚖️\"] = \"man_judge_tone2\",\n        [\"👨🏽‍⚖️\"] = \"man_judge_tone3\",\n        [\"👨🏾‍⚖️\"] = \"man_judge_tone4\",\n        [\"👨🏿‍⚖️\"] = \"man_judge_tone5\",\n        [\"👰\"] = \"person_with_veil\",\n        [\"👰🏻\"] = \"person_with_veil_tone1\",\n        [\"👰🏼\"] = \"person_with_veil_tone2\",\n        [\"👰🏽\"] = \"person_with_veil_tone3\",\n        [\"👰🏾\"] = \"person_with_veil_tone4\",\n        [\"👰🏿\"] = \"person_with_veil_tone5\",\n        [\"👰‍♀️\"] = \"woman_with_veil\",\n        [\"👰🏻‍♀️\"] = \"woman_with_veil_tone1\",\n        [\"👰🏼‍♀️\"] = \"woman_with_veil_tone2\",\n        [\"👰🏽‍♀️\"] = \"woman_with_veil_tone3\",\n        [\"👰🏾‍♀️\"] = \"woman_with_veil_tone4\",\n        [\"👰🏿‍♀️\"] = \"woman_with_veil_tone5\",\n        [\"👰‍♂️\"] = \"man_with_veil\",\n        [\"👰🏻‍♂️\"] = \"man_with_veil_tone1\",\n        [\"👰🏼‍♂️\"] = \"man_with_veil_tone2\",\n        [\"👰🏽‍♂️\"] = \"man_with_veil_tone3\",\n        [\"👰🏾‍♂️\"] = \"man_with_veil_tone4\",\n        [\"👰🏿‍♂️\"] = \"man_with_veil_tone5\",\n        [\"🤵\"] = \"person_in_tuxedo\",\n        [\"🤵🏻\"] = \"person_in_tuxedo_tone1\",\n        [\"🤵🏼\"] = \"person_in_tuxedo_tone2\",\n        [\"🤵🏽\"] = \"person_in_tuxedo_tone3\",\n        [\"🤵🏾\"] = \"person_in_tuxedo_tone4\",\n        [\"🤵🏿\"] = \"person_in_tuxedo_tone5\",\n        [\"🤵‍♀️\"] = \"woman_in_tuxedo\",\n        [\"🤵🏻‍♀️\"] = \"woman_in_tuxedo_tone1\",\n        [\"🤵🏼‍♀️\"] = \"woman_in_tuxedo_tone2\",\n        [\"🤵🏽‍♀️\"] = \"woman_in_tuxedo_tone3\",\n        [\"🤵🏾‍♀️\"] = \"woman_in_tuxedo_tone4\",\n        [\"🤵🏿‍♀️\"] = \"woman_in_tuxedo_tone5\",\n        [\"🤵‍♂️\"] = \"man_in_tuxedo\",\n        [\"🤵🏻‍♂️\"] = \"man_in_tuxedo_tone1\",\n        [\"🤵🏼‍♂️\"] = \"man_in_tuxedo_tone2\",\n        [\"🤵🏽‍♂️\"] = \"man_in_tuxedo_tone3\",\n        [\"🤵🏾‍♂️\"] = \"man_in_tuxedo_tone4\",\n        [\"🤵🏿‍♂️\"] = \"man_in_tuxedo_tone5\",\n        [\"👸\"] = \"princess\",\n        [\"👸🏻\"] = \"princess_tone1\",\n        [\"👸🏼\"] = \"princess_tone2\",\n        [\"👸🏽\"] = \"princess_tone3\",\n        [\"👸🏾\"] = \"princess_tone4\",\n        [\"👸🏿\"] = \"princess_tone5\",\n        [\"🤴\"] = \"prince\",\n        [\"🤴🏻\"] = \"prince_tone1\",\n        [\"🤴🏼\"] = \"prince_tone2\",\n        [\"🤴🏽\"] = \"prince_tone3\",\n        [\"🤴🏾\"] = \"prince_tone4\",\n        [\"🤴🏿\"] = \"prince_tone5\",\n        [\"🦸\"] = \"superhero\",\n        [\"🦸🏻\"] = \"superhero_tone1\",\n        [\"🦸🏼\"] = \"superhero_tone2\",\n        [\"🦸🏽\"] = \"superhero_tone3\",\n        [\"🦸🏾\"] = \"superhero_tone4\",\n        [\"🦸🏿\"] = \"superhero_tone5\",\n        [\"🦸‍♀️\"] = \"woman_superhero\",\n        [\"🦸🏻‍♀️\"] = \"woman_superhero_tone1\",\n        [\"🦸🏼‍♀️\"] = \"woman_superhero_tone2\",\n        [\"🦸🏽‍♀️\"] = \"woman_superhero_tone3\",\n        [\"🦸🏾‍♀️\"] = \"woman_superhero_tone4\",\n        [\"🦸🏿‍♀️\"] = \"woman_superhero_tone5\",\n        [\"🦸‍♂️\"] = \"man_superhero\",\n        [\"🦸🏻‍♂️\"] = \"man_superhero_tone1\",\n        [\"🦸🏼‍♂️\"] = \"man_superhero_tone2\",\n        [\"🦸🏽‍♂️\"] = \"man_superhero_tone3\",\n        [\"🦸🏾‍♂️\"] = \"man_superhero_tone4\",\n        [\"🦸🏿‍♂️\"] = \"man_superhero_tone5\",\n        [\"🦹\"] = \"supervillain\",\n        [\"🦹🏻\"] = \"supervillain_tone1\",\n        [\"🦹🏼\"] = \"supervillain_tone2\",\n        [\"🦹🏽\"] = \"supervillain_tone3\",\n        [\"🦹🏾\"] = \"supervillain_tone4\",\n        [\"🦹🏿\"] = \"supervillain_tone5\",\n        [\"🦹‍♀️\"] = \"woman_supervillain\",\n        [\"🦹🏻‍♀️\"] = \"woman_supervillain_tone1\",\n        [\"🦹🏼‍♀️\"] = \"woman_supervillain_tone2\",\n        [\"🦹🏽‍♀️\"] = \"woman_supervillain_tone3\",\n        [\"🦹🏾‍♀️\"] = \"woman_supervillain_tone4\",\n        [\"🦹🏿‍♀️\"] = \"woman_supervillain_tone5\",\n        [\"🦹‍♂️\"] = \"man_supervillain\",\n        [\"🦹🏻‍♂️\"] = \"man_supervillain_tone1\",\n        [\"🦹🏼‍♂️\"] = \"man_supervillain_tone2\",\n        [\"🦹🏽‍♂️\"] = \"man_supervillain_tone3\",\n        [\"🦹🏾‍♂️\"] = \"man_supervillain_tone4\",\n        [\"🦹🏿‍♂️\"] = \"man_supervillain_tone5\",\n        [\"🥷\"] = \"ninja\",\n        [\"🥷🏻\"] = \"ninja_tone1\",\n        [\"🥷🏼\"] = \"ninja_tone2\",\n        [\"🥷🏽\"] = \"ninja_tone3\",\n        [\"🥷🏾\"] = \"ninja_tone4\",\n        [\"🥷🏿\"] = \"ninja_tone5\",\n        [\"🧑‍🎄\"] = \"mx_claus\",\n        [\"🧑🏻‍🎄\"] = \"mx_claus_tone1\",\n        [\"🧑🏼‍🎄\"] = \"mx_claus_tone2\",\n        [\"🧑🏽‍🎄\"] = \"mx_claus_tone3\",\n        [\"🧑🏾‍🎄\"] = \"mx_claus_tone4\",\n        [\"🧑🏿‍🎄\"] = \"mx_claus_tone5\",\n        [\"🤶\"] = \"mrs_claus\",\n        [\"🤶🏻\"] = \"mrs_claus_tone1\",\n        [\"🤶🏼\"] = \"mrs_claus_tone2\",\n        [\"🤶🏽\"] = \"mrs_claus_tone3\",\n        [\"🤶🏾\"] = \"mrs_claus_tone4\",\n        [\"🤶🏿\"] = \"mrs_claus_tone5\",\n        [\"🎅\"] = \"santa\",\n        [\"🎅🏻\"] = \"santa_tone1\",\n        [\"🎅🏼\"] = \"santa_tone2\",\n        [\"🎅🏽\"] = \"santa_tone3\",\n        [\"🎅🏾\"] = \"santa_tone4\",\n        [\"🎅🏿\"] = \"santa_tone5\",\n        [\"🧙\"] = \"mage\",\n        [\"🧙🏻\"] = \"mage_tone1\",\n        [\"🧙🏼\"] = \"mage_tone2\",\n        [\"🧙🏽\"] = \"mage_tone3\",\n        [\"🧙🏾\"] = \"mage_tone4\",\n        [\"🧙🏿\"] = \"mage_tone5\",\n        [\"🧙‍♀️\"] = \"woman_mage\",\n        [\"🧙🏻‍♀️\"] = \"woman_mage_tone1\",\n        [\"🧙🏼‍♀️\"] = \"woman_mage_tone2\",\n        [\"🧙🏽‍♀️\"] = \"woman_mage_tone3\",\n        [\"🧙🏾‍♀️\"] = \"woman_mage_tone4\",\n        [\"🧙🏿‍♀️\"] = \"woman_mage_tone5\",\n        [\"🧙‍♂️\"] = \"man_mage\",\n        [\"🧙🏻‍♂️\"] = \"man_mage_tone1\",\n        [\"🧙🏼‍♂️\"] = \"man_mage_tone2\",\n        [\"🧙🏽‍♂️\"] = \"man_mage_tone3\",\n        [\"🧙🏾‍♂️\"] = \"man_mage_tone4\",\n        [\"🧙🏿‍♂️\"] = \"man_mage_tone5\",\n        [\"🧝\"] = \"elf\",\n        [\"🧝🏻\"] = \"elf_tone1\",\n        [\"🧝🏼\"] = \"elf_tone2\",\n        [\"🧝🏽\"] = \"elf_tone3\",\n        [\"🧝🏾\"] = \"elf_tone4\",\n        [\"🧝🏿\"] = \"elf_tone5\",\n        [\"🧝‍♀️\"] = \"woman_elf\",\n        [\"🧝🏻‍♀️\"] = \"woman_elf_tone1\",\n        [\"🧝🏼‍♀️\"] = \"woman_elf_tone2\",\n        [\"🧝🏽‍♀️\"] = \"woman_elf_tone3\",\n        [\"🧝🏾‍♀️\"] = \"woman_elf_tone4\",\n        [\"🧝🏿‍♀️\"] = \"woman_elf_tone5\",\n        [\"🧝‍♂️\"] = \"man_elf\",\n        [\"🧝🏻‍♂️\"] = \"man_elf_tone1\",\n        [\"🧝🏼‍♂️\"] = \"man_elf_tone2\",\n        [\"🧝🏽‍♂️\"] = \"man_elf_tone3\",\n        [\"🧝🏾‍♂️\"] = \"man_elf_tone4\",\n        [\"🧝🏿‍♂️\"] = \"man_elf_tone5\",\n        [\"🧛\"] = \"vampire\",\n        [\"🧛🏻\"] = \"vampire_tone1\",\n        [\"🧛🏼\"] = \"vampire_tone2\",\n        [\"🧛🏽\"] = \"vampire_tone3\",\n        [\"🧛🏾\"] = \"vampire_tone4\",\n        [\"🧛🏿\"] = \"vampire_tone5\",\n        [\"🧛‍♀️\"] = \"woman_vampire\",\n        [\"🧛🏻‍♀️\"] = \"woman_vampire_tone1\",\n        [\"🧛🏼‍♀️\"] = \"woman_vampire_tone2\",\n        [\"🧛🏽‍♀️\"] = \"woman_vampire_tone3\",\n        [\"🧛🏾‍♀️\"] = \"woman_vampire_tone4\",\n        [\"🧛🏿‍♀️\"] = \"woman_vampire_tone5\",\n        [\"🧛‍♂️\"] = \"man_vampire\",\n        [\"🧛🏻‍♂️\"] = \"man_vampire_tone1\",\n        [\"🧛🏼‍♂️\"] = \"man_vampire_tone2\",\n        [\"🧛🏽‍♂️\"] = \"man_vampire_tone3\",\n        [\"🧛🏾‍♂️\"] = \"man_vampire_tone4\",\n        [\"🧛🏿‍♂️\"] = \"man_vampire_tone5\",\n        [\"🧟\"] = \"zombie\",\n        [\"🧟‍♀️\"] = \"woman_zombie\",\n        [\"🧟‍♂️\"] = \"man_zombie\",\n        [\"🧞\"] = \"genie\",\n        [\"🧞‍♀️\"] = \"woman_genie\",\n        [\"🧞‍♂️\"] = \"man_genie\",\n        [\"🧜\"] = \"merperson\",\n        [\"🧜🏻\"] = \"merperson_tone1\",\n        [\"🧜🏼\"] = \"merperson_tone2\",\n        [\"🧜🏽\"] = \"merperson_tone3\",\n        [\"🧜🏾\"] = \"merperson_tone4\",\n        [\"🧜🏿\"] = \"merperson_tone5\",\n        [\"🧜‍♀️\"] = \"mermaid\",\n        [\"🧜🏻‍♀️\"] = \"mermaid_tone1\",\n        [\"🧜🏼‍♀️\"] = \"mermaid_tone2\",\n        [\"🧜🏽‍♀️\"] = \"mermaid_tone3\",\n        [\"🧜🏾‍♀️\"] = \"mermaid_tone4\",\n        [\"🧜🏿‍♀️\"] = \"mermaid_tone5\",\n        [\"🧜‍♂️\"] = \"merman\",\n        [\"🧜🏻‍♂️\"] = \"merman_tone1\",\n        [\"🧜🏼‍♂️\"] = \"merman_tone2\",\n        [\"🧜🏽‍♂️\"] = \"merman_tone3\",\n        [\"🧜🏾‍♂️\"] = \"merman_tone4\",\n        [\"🧜🏿‍♂️\"] = \"merman_tone5\",\n        [\"🧚\"] = \"fairy\",\n        [\"🧚🏻\"] = \"fairy_tone1\",\n        [\"🧚🏼\"] = \"fairy_tone2\",\n        [\"🧚🏽\"] = \"fairy_tone3\",\n        [\"🧚🏾\"] = \"fairy_tone4\",\n        [\"🧚🏿\"] = \"fairy_tone5\",\n        [\"🧚‍♀️\"] = \"woman_fairy\",\n        [\"🧚🏻‍♀️\"] = \"woman_fairy_tone1\",\n        [\"🧚🏼‍♀️\"] = \"woman_fairy_tone2\",\n        [\"🧚🏽‍♀️\"] = \"woman_fairy_tone3\",\n        [\"🧚🏾‍♀️\"] = \"woman_fairy_tone4\",\n        [\"🧚🏿‍♀️\"] = \"woman_fairy_tone5\",\n        [\"🧚‍♂️\"] = \"man_fairy\",\n        [\"🧚🏻‍♂️\"] = \"man_fairy_tone1\",\n        [\"🧚🏼‍♂️\"] = \"man_fairy_tone2\",\n        [\"🧚🏽‍♂️\"] = \"man_fairy_tone3\",\n        [\"🧚🏾‍♂️\"] = \"man_fairy_tone4\",\n        [\"🧚🏿‍♂️\"] = \"man_fairy_tone5\",\n        [\"👼\"] = \"angel\",\n        [\"👼🏻\"] = \"angel_tone1\",\n        [\"👼🏼\"] = \"angel_tone2\",\n        [\"👼🏽\"] = \"angel_tone3\",\n        [\"👼🏾\"] = \"angel_tone4\",\n        [\"👼🏿\"] = \"angel_tone5\",\n        [\"🤰\"] = \"pregnant_woman\",\n        [\"🤰🏻\"] = \"pregnant_woman_tone1\",\n        [\"🤰🏼\"] = \"pregnant_woman_tone2\",\n        [\"🤰🏽\"] = \"pregnant_woman_tone3\",\n        [\"🤰🏾\"] = \"pregnant_woman_tone4\",\n        [\"🤰🏿\"] = \"pregnant_woman_tone5\",\n        [\"🤱\"] = \"breast_feeding\",\n        [\"🤱🏻\"] = \"breast_feeding_tone1\",\n        [\"🤱🏼\"] = \"breast_feeding_tone2\",\n        [\"🤱🏽\"] = \"breast_feeding_tone3\",\n        [\"🤱🏾\"] = \"breast_feeding_tone4\",\n        [\"🤱🏿\"] = \"breast_feeding_tone5\",\n        [\"🧑‍🍼\"] = \"person_feeding_baby\",\n        [\"🧑🏻‍🍼\"] = \"person_feeding_baby_tone1\",\n        [\"🧑🏼‍🍼\"] = \"person_feeding_baby_tone2\",\n        [\"🧑🏽‍🍼\"] = \"person_feeding_baby_tone3\",\n        [\"🧑🏾‍🍼\"] = \"person_feeding_baby_tone4\",\n        [\"🧑🏿‍🍼\"] = \"person_feeding_baby_tone5\",\n        [\"👩‍🍼\"] = \"woman_feeding_baby\",\n        [\"👩🏻‍🍼\"] = \"woman_feeding_baby_tone1\",\n        [\"👩🏼‍🍼\"] = \"woman_feeding_baby_tone2\",\n        [\"👩🏽‍🍼\"] = \"woman_feeding_baby_tone3\",\n        [\"👩🏾‍🍼\"] = \"woman_feeding_baby_tone4\",\n        [\"👩🏿‍🍼\"] = \"woman_feeding_baby_tone5\",\n        [\"👨‍🍼\"] = \"man_feeding_baby\",\n        [\"👨🏻‍🍼\"] = \"man_feeding_baby_tone1\",\n        [\"👨🏼‍🍼\"] = \"man_feeding_baby_tone2\",\n        [\"👨🏽‍🍼\"] = \"man_feeding_baby_tone3\",\n        [\"👨🏾‍🍼\"] = \"man_feeding_baby_tone4\",\n        [\"👨🏿‍🍼\"] = \"man_feeding_baby_tone5\",\n        [\"🙇\"] = \"person_bowing\",\n        [\"🙇🏻\"] = \"person_bowing_tone1\",\n        [\"🙇🏼\"] = \"person_bowing_tone2\",\n        [\"🙇🏽\"] = \"person_bowing_tone3\",\n        [\"🙇🏾\"] = \"person_bowing_tone4\",\n        [\"🙇🏿\"] = \"person_bowing_tone5\",\n        [\"🙇‍♀️\"] = \"woman_bowing\",\n        [\"🙇🏻‍♀️\"] = \"woman_bowing_tone1\",\n        [\"🙇🏼‍♀️\"] = \"woman_bowing_tone2\",\n        [\"🙇🏽‍♀️\"] = \"woman_bowing_tone3\",\n        [\"🙇🏾‍♀️\"] = \"woman_bowing_tone4\",\n        [\"🙇🏿‍♀️\"] = \"woman_bowing_tone5\",\n        [\"🙇‍♂️\"] = \"man_bowing\",\n        [\"🙇🏻‍♂️\"] = \"man_bowing_tone1\",\n        [\"🙇🏼‍♂️\"] = \"man_bowing_tone2\",\n        [\"🙇🏽‍♂️\"] = \"man_bowing_tone3\",\n        [\"🙇🏾‍♂️\"] = \"man_bowing_tone4\",\n        [\"🙇🏿‍♂️\"] = \"man_bowing_tone5\",\n        [\"💁\"] = \"person_tipping_hand\",\n        [\"💁🏻\"] = \"person_tipping_hand_tone1\",\n        [\"💁🏼\"] = \"person_tipping_hand_tone2\",\n        [\"💁🏽\"] = \"person_tipping_hand_tone3\",\n        [\"💁🏾\"] = \"person_tipping_hand_tone4\",\n        [\"💁🏿\"] = \"person_tipping_hand_tone5\",\n        [\"💁‍♀️\"] = \"woman_tipping_hand\",\n        [\"💁🏻‍♀️\"] = \"woman_tipping_hand_tone1\",\n        [\"💁🏼‍♀️\"] = \"woman_tipping_hand_tone2\",\n        [\"💁🏽‍♀️\"] = \"woman_tipping_hand_tone3\",\n        [\"💁🏾‍♀️\"] = \"woman_tipping_hand_tone4\",\n        [\"💁🏿‍♀️\"] = \"woman_tipping_hand_tone5\",\n        [\"💁‍♂️\"] = \"man_tipping_hand\",\n        [\"💁🏻‍♂️\"] = \"man_tipping_hand_tone1\",\n        [\"💁🏼‍♂️\"] = \"man_tipping_hand_tone2\",\n        [\"💁🏽‍♂️\"] = \"man_tipping_hand_tone3\",\n        [\"💁🏾‍♂️\"] = \"man_tipping_hand_tone4\",\n        [\"💁🏿‍♂️\"] = \"man_tipping_hand_tone5\",\n        [\"🙅\"] = \"person_gesturing_no\",\n        [\"🙅🏻\"] = \"person_gesturing_no_tone1\",\n        [\"🙅🏼\"] = \"person_gesturing_no_tone2\",\n        [\"🙅🏽\"] = \"person_gesturing_no_tone3\",\n        [\"🙅🏾\"] = \"person_gesturing_no_tone4\",\n        [\"🙅🏿\"] = \"person_gesturing_no_tone5\",\n        [\"🙅‍♀️\"] = \"woman_gesturing_no\",\n        [\"🙅🏻‍♀️\"] = \"woman_gesturing_no_tone1\",\n        [\"🙅🏼‍♀️\"] = \"woman_gesturing_no_tone2\",\n        [\"🙅🏽‍♀️\"] = \"woman_gesturing_no_tone3\",\n        [\"🙅🏾‍♀️\"] = \"woman_gesturing_no_tone4\",\n        [\"🙅🏿‍♀️\"] = \"woman_gesturing_no_tone5\",\n        [\"🙅‍♂️\"] = \"man_gesturing_no\",\n        [\"🙅🏻‍♂️\"] = \"man_gesturing_no_tone1\",\n        [\"🙅🏼‍♂️\"] = \"man_gesturing_no_tone2\",\n        [\"🙅🏽‍♂️\"] = \"man_gesturing_no_tone3\",\n        [\"🙅🏾‍♂️\"] = \"man_gesturing_no_tone4\",\n        [\"🙅🏿‍♂️\"] = \"man_gesturing_no_tone5\",\n        [\"🙆\"] = \"person_gesturing_ok\",\n        [\"🙆🏻\"] = \"person_gesturing_ok_tone1\",\n        [\"🙆🏼\"] = \"person_gesturing_ok_tone2\",\n        [\"🙆🏽\"] = \"person_gesturing_ok_tone3\",\n        [\"🙆🏾\"] = \"person_gesturing_ok_tone4\",\n        [\"🙆🏿\"] = \"person_gesturing_ok_tone5\",\n        [\"🙆‍♀️\"] = \"woman_gesturing_ok\",\n        [\"🙆🏻‍♀️\"] = \"woman_gesturing_ok_tone1\",\n        [\"🙆🏼‍♀️\"] = \"woman_gesturing_ok_tone2\",\n        [\"🙆🏽‍♀️\"] = \"woman_gesturing_ok_tone3\",\n        [\"🙆🏾‍♀️\"] = \"woman_gesturing_ok_tone4\",\n        [\"🙆🏿‍♀️\"] = \"woman_gesturing_ok_tone5\",\n        [\"🙆‍♂️\"] = \"man_gesturing_ok\",\n        [\"🙆🏻‍♂️\"] = \"man_gesturing_ok_tone1\",\n        [\"🙆🏼‍♂️\"] = \"man_gesturing_ok_tone2\",\n        [\"🙆🏽‍♂️\"] = \"man_gesturing_ok_tone3\",\n        [\"🙆🏾‍♂️\"] = \"man_gesturing_ok_tone4\",\n        [\"🙆🏿‍♂️\"] = \"man_gesturing_ok_tone5\",\n        [\"🙋\"] = \"person_raising_hand\",\n        [\"🙋🏻\"] = \"person_raising_hand_tone1\",\n        [\"🙋🏼\"] = \"person_raising_hand_tone2\",\n        [\"🙋🏽\"] = \"person_raising_hand_tone3\",\n        [\"🙋🏾\"] = \"person_raising_hand_tone4\",\n        [\"🙋🏿\"] = \"person_raising_hand_tone5\",\n        [\"🙋‍♀️\"] = \"woman_raising_hand\",\n        [\"🙋🏻‍♀️\"] = \"woman_raising_hand_tone1\",\n        [\"🙋🏼‍♀️\"] = \"woman_raising_hand_tone2\",\n        [\"🙋🏽‍♀️\"] = \"woman_raising_hand_tone3\",\n        [\"🙋🏾‍♀️\"] = \"woman_raising_hand_tone4\",\n        [\"🙋🏿‍♀️\"] = \"woman_raising_hand_tone5\",\n        [\"🙋‍♂️\"] = \"man_raising_hand\",\n        [\"🙋🏻‍♂️\"] = \"man_raising_hand_tone1\",\n        [\"🙋🏼‍♂️\"] = \"man_raising_hand_tone2\",\n        [\"🙋🏽‍♂️\"] = \"man_raising_hand_tone3\",\n        [\"🙋🏾‍♂️\"] = \"man_raising_hand_tone4\",\n        [\"🙋🏿‍♂️\"] = \"man_raising_hand_tone5\",\n        [\"🧏\"] = \"deaf_person\",\n        [\"🧏🏻\"] = \"deaf_person_tone1\",\n        [\"🧏🏼\"] = \"deaf_person_tone2\",\n        [\"🧏🏽\"] = \"deaf_person_tone3\",\n        [\"🧏🏾\"] = \"deaf_person_tone4\",\n        [\"🧏🏿\"] = \"deaf_person_tone5\",\n        [\"🧏‍♀️\"] = \"deaf_woman\",\n        [\"🧏🏻‍♀️\"] = \"deaf_woman_tone1\",\n        [\"🧏🏼‍♀️\"] = \"deaf_woman_tone2\",\n        [\"🧏🏽‍♀️\"] = \"deaf_woman_tone3\",\n        [\"🧏🏾‍♀️\"] = \"deaf_woman_tone4\",\n        [\"🧏🏿‍♀️\"] = \"deaf_woman_tone5\",\n        [\"🧏‍♂️\"] = \"deaf_man\",\n        [\"🧏🏻‍♂️\"] = \"deaf_man_tone1\",\n        [\"🧏🏼‍♂️\"] = \"deaf_man_tone2\",\n        [\"🧏🏽‍♂️\"] = \"deaf_man_tone3\",\n        [\"🧏🏾‍♂️\"] = \"deaf_man_tone4\",\n        [\"🧏🏿‍♂️\"] = \"deaf_man_tone5\",\n        [\"🤦\"] = \"person_facepalming\",\n        [\"🤦🏻\"] = \"person_facepalming_tone1\",\n        [\"🤦🏼\"] = \"person_facepalming_tone2\",\n        [\"🤦🏽\"] = \"person_facepalming_tone3\",\n        [\"🤦🏾\"] = \"person_facepalming_tone4\",\n        [\"🤦🏿\"] = \"person_facepalming_tone5\",\n        [\"🤦‍♀️\"] = \"woman_facepalming\",\n        [\"🤦🏻‍♀️\"] = \"woman_facepalming_tone1\",\n        [\"🤦🏼‍♀️\"] = \"woman_facepalming_tone2\",\n        [\"🤦🏽‍♀️\"] = \"woman_facepalming_tone3\",\n        [\"🤦🏾‍♀️\"] = \"woman_facepalming_tone4\",\n        [\"🤦🏿‍♀️\"] = \"woman_facepalming_tone5\",\n        [\"🤦‍♂️\"] = \"man_facepalming\",\n        [\"🤦🏻‍♂️\"] = \"man_facepalming_tone1\",\n        [\"🤦🏼‍♂️\"] = \"man_facepalming_tone2\",\n        [\"🤦🏽‍♂️\"] = \"man_facepalming_tone3\",\n        [\"🤦🏾‍♂️\"] = \"man_facepalming_tone4\",\n        [\"🤦🏿‍♂️\"] = \"man_facepalming_tone5\",\n        [\"🤷\"] = \"person_shrugging\",\n        [\"🤷🏻\"] = \"person_shrugging_tone1\",\n        [\"🤷🏼\"] = \"person_shrugging_tone2\",\n        [\"🤷🏽\"] = \"person_shrugging_tone3\",\n        [\"🤷🏾\"] = \"person_shrugging_tone4\",\n        [\"🤷🏿\"] = \"person_shrugging_tone5\",\n        [\"🤷‍♀️\"] = \"woman_shrugging\",\n        [\"🤷🏻‍♀️\"] = \"woman_shrugging_tone1\",\n        [\"🤷🏼‍♀️\"] = \"woman_shrugging_tone2\",\n        [\"🤷🏽‍♀️\"] = \"woman_shrugging_tone3\",\n        [\"🤷🏾‍♀️\"] = \"woman_shrugging_tone4\",\n        [\"🤷🏿‍♀️\"] = \"woman_shrugging_tone5\",\n        [\"🤷‍♂️\"] = \"man_shrugging\",\n        [\"🤷🏻‍♂️\"] = \"man_shrugging_tone1\",\n        [\"🤷🏼‍♂️\"] = \"man_shrugging_tone2\",\n        [\"🤷🏽‍♂️\"] = \"man_shrugging_tone3\",\n        [\"🤷🏾‍♂️\"] = \"man_shrugging_tone4\",\n        [\"🤷🏿‍♂️\"] = \"man_shrugging_tone5\",\n        [\"🙎\"] = \"person_pouting\",\n        [\"🙎🏻\"] = \"person_pouting_tone1\",\n        [\"🙎🏼\"] = \"person_pouting_tone2\",\n        [\"🙎🏽\"] = \"person_pouting_tone3\",\n        [\"🙎🏾\"] = \"person_pouting_tone4\",\n        [\"🙎🏿\"] = \"person_pouting_tone5\",\n        [\"🙎‍♀️\"] = \"woman_pouting\",\n        [\"🙎🏻‍♀️\"] = \"woman_pouting_tone1\",\n        [\"🙎🏼‍♀️\"] = \"woman_pouting_tone2\",\n        [\"🙎🏽‍♀️\"] = \"woman_pouting_tone3\",\n        [\"🙎🏾‍♀️\"] = \"woman_pouting_tone4\",\n        [\"🙎🏿‍♀️\"] = \"woman_pouting_tone5\",\n        [\"🙎‍♂️\"] = \"man_pouting\",\n        [\"🙎🏻‍♂️\"] = \"man_pouting_tone1\",\n        [\"🙎🏼‍♂️\"] = \"man_pouting_tone2\",\n        [\"🙎🏽‍♂️\"] = \"man_pouting_tone3\",\n        [\"🙎🏾‍♂️\"] = \"man_pouting_tone4\",\n        [\"🙎🏿‍♂️\"] = \"man_pouting_tone5\",\n        [\"🙍\"] = \"person_frowning\",\n        [\"🙍🏻\"] = \"person_frowning_tone1\",\n        [\"🙍🏼\"] = \"person_frowning_tone2\",\n        [\"🙍🏽\"] = \"person_frowning_tone3\",\n        [\"🙍🏾\"] = \"person_frowning_tone4\",\n        [\"🙍🏿\"] = \"person_frowning_tone5\",\n        [\"🙍‍♀️\"] = \"woman_frowning\",\n        [\"🙍🏻‍♀️\"] = \"woman_frowning_tone1\",\n        [\"🙍🏼‍♀️\"] = \"woman_frowning_tone2\",\n        [\"🙍🏽‍♀️\"] = \"woman_frowning_tone3\",\n        [\"🙍🏾‍♀️\"] = \"woman_frowning_tone4\",\n        [\"🙍🏿‍♀️\"] = \"woman_frowning_tone5\",\n        [\"🙍‍♂️\"] = \"man_frowning\",\n        [\"🙍🏻‍♂️\"] = \"man_frowning_tone1\",\n        [\"🙍🏼‍♂️\"] = \"man_frowning_tone2\",\n        [\"🙍🏽‍♂️\"] = \"man_frowning_tone3\",\n        [\"🙍🏾‍♂️\"] = \"man_frowning_tone4\",\n        [\"🙍🏿‍♂️\"] = \"man_frowning_tone5\",\n        [\"💇\"] = \"person_getting_haircut\",\n        [\"💇🏻\"] = \"person_getting_haircut_tone1\",\n        [\"💇🏼\"] = \"person_getting_haircut_tone2\",\n        [\"💇🏽\"] = \"person_getting_haircut_tone3\",\n        [\"💇🏾\"] = \"person_getting_haircut_tone4\",\n        [\"💇🏿\"] = \"person_getting_haircut_tone5\",\n        [\"💇‍♀️\"] = \"woman_getting_haircut\",\n        [\"💇🏻‍♀️\"] = \"woman_getting_haircut_tone1\",\n        [\"💇🏼‍♀️\"] = \"woman_getting_haircut_tone2\",\n        [\"💇🏽‍♀️\"] = \"woman_getting_haircut_tone3\",\n        [\"💇🏾‍♀️\"] = \"woman_getting_haircut_tone4\",\n        [\"💇🏿‍♀️\"] = \"woman_getting_haircut_tone5\",\n        [\"💇‍♂️\"] = \"man_getting_haircut\",\n        [\"💇🏻‍♂️\"] = \"man_getting_haircut_tone1\",\n        [\"💇🏼‍♂️\"] = \"man_getting_haircut_tone2\",\n        [\"💇🏽‍♂️\"] = \"man_getting_haircut_tone3\",\n        [\"💇🏾‍♂️\"] = \"man_getting_haircut_tone4\",\n        [\"💇🏿‍♂️\"] = \"man_getting_haircut_tone5\",\n        [\"💆\"] = \"person_getting_massage\",\n        [\"💆🏻\"] = \"person_getting_massage_tone1\",\n        [\"💆🏼\"] = \"person_getting_massage_tone2\",\n        [\"💆🏽\"] = \"person_getting_massage_tone3\",\n        [\"💆🏾\"] = \"person_getting_massage_tone4\",\n        [\"💆🏿\"] = \"person_getting_massage_tone5\",\n        [\"💆‍♀️\"] = \"woman_getting_face_massage\",\n        [\"💆🏻‍♀️\"] = \"woman_getting_face_massage_tone1\",\n        [\"💆🏼‍♀️\"] = \"woman_getting_face_massage_tone2\",\n        [\"💆🏽‍♀️\"] = \"woman_getting_face_massage_tone3\",\n        [\"💆🏾‍♀️\"] = \"woman_getting_face_massage_tone4\",\n        [\"💆🏿‍♀️\"] = \"woman_getting_face_massage_tone5\",\n        [\"💆‍♂️\"] = \"man_getting_face_massage\",\n        [\"💆🏻‍♂️\"] = \"man_getting_face_massage_tone1\",\n        [\"💆🏼‍♂️\"] = \"man_getting_face_massage_tone2\",\n        [\"💆🏽‍♂️\"] = \"man_getting_face_massage_tone3\",\n        [\"💆🏾‍♂️\"] = \"man_getting_face_massage_tone4\",\n        [\"💆🏿‍♂️\"] = \"man_getting_face_massage_tone5\",\n        [\"🧖\"] = \"person_in_steamy_room\",\n        [\"🧖🏻\"] = \"person_in_steamy_room_tone1\",\n        [\"🧖🏼\"] = \"person_in_steamy_room_tone2\",\n        [\"🧖🏽\"] = \"person_in_steamy_room_tone3\",\n        [\"🧖🏾\"] = \"person_in_steamy_room_tone4\",\n        [\"🧖🏿\"] = \"person_in_steamy_room_tone5\",\n        [\"🧖‍♀️\"] = \"woman_in_steamy_room\",\n        [\"🧖🏻‍♀️\"] = \"woman_in_steamy_room_tone1\",\n        [\"🧖🏼‍♀️\"] = \"woman_in_steamy_room_tone2\",\n        [\"🧖🏽‍♀️\"] = \"woman_in_steamy_room_tone3\",\n        [\"🧖🏾‍♀️\"] = \"woman_in_steamy_room_tone4\",\n        [\"🧖🏿‍♀️\"] = \"woman_in_steamy_room_tone5\",\n        [\"🧖‍♂️\"] = \"man_in_steamy_room\",\n        [\"🧖🏻‍♂️\"] = \"man_in_steamy_room_tone1\",\n        [\"🧖🏼‍♂️\"] = \"man_in_steamy_room_tone2\",\n        [\"🧖🏽‍♂️\"] = \"man_in_steamy_room_tone3\",\n        [\"🧖🏾‍♂️\"] = \"man_in_steamy_room_tone4\",\n        [\"🧖🏿‍♂️\"] = \"man_in_steamy_room_tone5\",\n        [\"💅\"] = \"nail_care\",\n        [\"💅🏻\"] = \"nail_care_tone1\",\n        [\"💅🏼\"] = \"nail_care_tone2\",\n        [\"💅🏽\"] = \"nail_care_tone3\",\n        [\"💅🏾\"] = \"nail_care_tone4\",\n        [\"💅🏿\"] = \"nail_care_tone5\",\n        [\"🤳\"] = \"selfie\",\n        [\"🤳🏻\"] = \"selfie_tone1\",\n        [\"🤳🏼\"] = \"selfie_tone2\",\n        [\"🤳🏽\"] = \"selfie_tone3\",\n        [\"🤳🏾\"] = \"selfie_tone4\",\n        [\"🤳🏿\"] = \"selfie_tone5\",\n        [\"💃\"] = \"dancer\",\n        [\"💃🏻\"] = \"dancer_tone1\",\n        [\"💃🏼\"] = \"dancer_tone2\",\n        [\"💃🏽\"] = \"dancer_tone3\",\n        [\"💃🏾\"] = \"dancer_tone4\",\n        [\"💃🏿\"] = \"dancer_tone5\",\n        [\"🕺\"] = \"man_dancing\",\n        [\"🕺🏻\"] = \"man_dancing_tone1\",\n        [\"🕺🏼\"] = \"man_dancing_tone2\",\n        [\"🕺🏽\"] = \"man_dancing_tone3\",\n        [\"🕺🏿\"] = \"man_dancing_tone5\",\n        [\"🕺🏾\"] = \"man_dancing_tone4\",\n        [\"👯\"] = \"people_with_bunny_ears_partying\",\n        [\"👯‍♀️\"] = \"women_with_bunny_ears_partying\",\n        [\"👯‍♂️\"] = \"men_with_bunny_ears_partying\",\n        [\"🕴️\"] = \"levitate\",\n        [\"🕴🏻\"] = \"levitate_tone1\",\n        [\"🕴🏼\"] = \"levitate_tone2\",\n        [\"🕴🏽\"] = \"levitate_tone3\",\n        [\"🕴🏾\"] = \"levitate_tone4\",\n        [\"🕴🏿\"] = \"levitate_tone5\",\n        [\"🧑‍🦽\"] = \"person_in_manual_wheelchair\",\n        [\"🧑🏻‍🦽\"] = \"person_in_manual_wheelchair_tone1\",\n        [\"🧑🏼‍🦽\"] = \"person_in_manual_wheelchair_tone2\",\n        [\"🧑🏽‍🦽\"] = \"person_in_manual_wheelchair_tone3\",\n        [\"🧑🏾‍🦽\"] = \"person_in_manual_wheelchair_tone4\",\n        [\"🧑🏿‍🦽\"] = \"person_in_manual_wheelchair_tone5\",\n        [\"👩‍🦽\"] = \"woman_in_manual_wheelchair\",\n        [\"👩🏻‍🦽\"] = \"woman_in_manual_wheelchair_tone1\",\n        [\"👩🏼‍🦽\"] = \"woman_in_manual_wheelchair_tone2\",\n        [\"👩🏽‍🦽\"] = \"woman_in_manual_wheelchair_tone3\",\n        [\"👩🏾‍🦽\"] = \"woman_in_manual_wheelchair_tone4\",\n        [\"👩🏿‍🦽\"] = \"woman_in_manual_wheelchair_tone5\",\n        [\"👨‍🦽\"] = \"man_in_manual_wheelchair\",\n        [\"👨🏻‍🦽\"] = \"man_in_manual_wheelchair_tone1\",\n        [\"👨🏼‍🦽\"] = \"man_in_manual_wheelchair_tone2\",\n        [\"👨🏽‍🦽\"] = \"man_in_manual_wheelchair_tone3\",\n        [\"👨🏾‍🦽\"] = \"man_in_manual_wheelchair_tone4\",\n        [\"👨🏿‍🦽\"] = \"man_in_manual_wheelchair_tone5\",\n        [\"🧑‍🦼\"] = \"person_in_motorized_wheelchair\",\n        [\"🧑🏻‍🦼\"] = \"person_in_motorized_wheelchair_tone1\",\n        [\"🧑🏼‍🦼\"] = \"person_in_motorized_wheelchair_tone2\",\n        [\"🧑🏽‍🦼\"] = \"person_in_motorized_wheelchair_tone3\",\n        [\"🧑🏾‍🦼\"] = \"person_in_motorized_wheelchair_tone4\",\n        [\"🧑🏿‍🦼\"] = \"person_in_motorized_wheelchair_tone5\",\n        [\"👩‍🦼\"] = \"woman_in_motorized_wheelchair\",\n        [\"👩🏻‍🦼\"] = \"woman_in_motorized_wheelchair_tone1\",\n        [\"👩🏼‍🦼\"] = \"woman_in_motorized_wheelchair_tone2\",\n        [\"👩🏽‍🦼\"] = \"woman_in_motorized_wheelchair_tone3\",\n        [\"👩🏾‍🦼\"] = \"woman_in_motorized_wheelchair_tone4\",\n        [\"👩🏿‍🦼\"] = \"woman_in_motorized_wheelchair_tone5\",\n        [\"👨‍🦼\"] = \"man_in_motorized_wheelchair\",\n        [\"👨🏻‍🦼\"] = \"man_in_motorized_wheelchair_tone1\",\n        [\"👨🏼‍🦼\"] = \"man_in_motorized_wheelchair_tone2\",\n        [\"👨🏽‍🦼\"] = \"man_in_motorized_wheelchair_tone3\",\n        [\"👨🏾‍🦼\"] = \"man_in_motorized_wheelchair_tone4\",\n        [\"👨🏿‍🦼\"] = \"man_in_motorized_wheelchair_tone5\",\n        [\"🚶\"] = \"person_walking\",\n        [\"🚶🏻\"] = \"person_walking_tone1\",\n        [\"🚶🏼\"] = \"person_walking_tone2\",\n        [\"🚶🏽\"] = \"person_walking_tone3\",\n        [\"🚶🏾\"] = \"person_walking_tone4\",\n        [\"🚶🏿\"] = \"person_walking_tone5\",\n        [\"🚶‍♀️\"] = \"woman_walking\",\n        [\"🚶🏻‍♀️\"] = \"woman_walking_tone1\",\n        [\"🚶🏼‍♀️\"] = \"woman_walking_tone2\",\n        [\"🚶🏽‍♀️\"] = \"woman_walking_tone3\",\n        [\"🚶🏾‍♀️\"] = \"woman_walking_tone4\",\n        [\"🚶🏿‍♀️\"] = \"woman_walking_tone5\",\n        [\"🚶‍♂️\"] = \"man_walking\",\n        [\"🚶🏻‍♂️\"] = \"man_walking_tone1\",\n        [\"🚶🏼‍♂️\"] = \"man_walking_tone2\",\n        [\"🚶🏽‍♂️\"] = \"man_walking_tone3\",\n        [\"🚶🏾‍♂️\"] = \"man_walking_tone4\",\n        [\"🚶🏿‍♂️\"] = \"man_walking_tone5\",\n        [\"🧑‍🦯\"] = \"person_with_probing_cane\",\n        [\"🧑🏻‍🦯\"] = \"person_with_probing_cane_tone1\",\n        [\"🧑🏼‍🦯\"] = \"person_with_probing_cane_tone2\",\n        [\"🧑🏽‍🦯\"] = \"person_with_probing_cane_tone3\",\n        [\"🧑🏾‍🦯\"] = \"person_with_probing_cane_tone4\",\n        [\"🧑🏿‍🦯\"] = \"person_with_probing_cane_tone5\",\n        [\"👩‍🦯\"] = \"woman_with_probing_cane\",\n        [\"👩🏻‍🦯\"] = \"woman_with_probing_cane_tone1\",\n        [\"👩🏼‍🦯\"] = \"woman_with_probing_cane_tone2\",\n        [\"👩🏽‍🦯\"] = \"woman_with_probing_cane_tone3\",\n        [\"👩🏾‍🦯\"] = \"woman_with_probing_cane_tone4\",\n        [\"👩🏿‍🦯\"] = \"woman_with_probing_cane_tone5\",\n        [\"👨‍🦯\"] = \"man_with_probing_cane\",\n        [\"👨🏻‍🦯\"] = \"man_with_probing_cane_tone1\",\n        [\"👨🏽‍🦯\"] = \"man_with_probing_cane_tone3\",\n        [\"👨🏼‍🦯\"] = \"man_with_probing_cane_tone2\",\n        [\"👨🏾‍🦯\"] = \"man_with_probing_cane_tone4\",\n        [\"👨🏿‍🦯\"] = \"man_with_probing_cane_tone5\",\n        [\"🧎\"] = \"person_kneeling\",\n        [\"🧎🏻\"] = \"person_kneeling_tone1\",\n        [\"🧎🏼\"] = \"person_kneeling_tone2\",\n        [\"🧎🏽\"] = \"person_kneeling_tone3\",\n        [\"🧎🏾\"] = \"person_kneeling_tone4\",\n        [\"🧎🏿\"] = \"person_kneeling_tone5\",\n        [\"🧎‍♀️\"] = \"woman_kneeling\",\n        [\"🧎🏻‍♀️\"] = \"woman_kneeling_tone1\",\n        [\"🧎🏼‍♀️\"] = \"woman_kneeling_tone2\",\n        [\"🧎🏽‍♀️\"] = \"woman_kneeling_tone3\",\n        [\"🧎🏾‍♀️\"] = \"woman_kneeling_tone4\",\n        [\"🧎🏿‍♀️\"] = \"woman_kneeling_tone5\",\n        [\"🧎‍♂️\"] = \"man_kneeling\",\n        [\"🧎🏻‍♂️\"] = \"man_kneeling_tone1\",\n        [\"🧎🏼‍♂️\"] = \"man_kneeling_tone2\",\n        [\"🧎🏽‍♂️\"] = \"man_kneeling_tone3\",\n        [\"🧎🏾‍♂️\"] = \"man_kneeling_tone4\",\n        [\"🧎🏿‍♂️\"] = \"man_kneeling_tone5\",\n        [\"🏃\"] = \"person_running\",\n        [\"🏃🏻\"] = \"person_running_tone1\",\n        [\"🏃🏼\"] = \"person_running_tone2\",\n        [\"🏃🏽\"] = \"person_running_tone3\",\n        [\"🏃🏾\"] = \"person_running_tone4\",\n        [\"🏃🏿\"] = \"person_running_tone5\",\n        [\"🏃‍♀️\"] = \"woman_running\",\n        [\"🏃🏻‍♀️\"] = \"woman_running_tone1\",\n        [\"🏃🏼‍♀️\"] = \"woman_running_tone2\",\n        [\"🏃🏽‍♀️\"] = \"woman_running_tone3\",\n        [\"🏃🏾‍♀️\"] = \"woman_running_tone4\",\n        [\"🏃🏿‍♀️\"] = \"woman_running_tone5\",\n        [\"🏃‍♂️\"] = \"man_running\",\n        [\"🏃🏻‍♂️\"] = \"man_running_tone1\",\n        [\"🏃🏼‍♂️\"] = \"man_running_tone2\",\n        [\"🏃🏽‍♂️\"] = \"man_running_tone3\",\n        [\"🏃🏾‍♂️\"] = \"man_running_tone4\",\n        [\"🏃🏿‍♂️\"] = \"man_running_tone5\",\n        [\"🧍\"] = \"person_standing\",\n        [\"🧍🏻\"] = \"person_standing_tone1\",\n        [\"🧍🏼\"] = \"person_standing_tone2\",\n        [\"🧍🏽\"] = \"person_standing_tone3\",\n        [\"🧍🏾\"] = \"person_standing_tone4\",\n        [\"🧍🏿\"] = \"person_standing_tone5\",\n        [\"🧍‍♀️\"] = \"woman_standing\",\n        [\"🧍🏻‍♀️\"] = \"woman_standing_tone1\",\n        [\"🧍🏼‍♀️\"] = \"woman_standing_tone2\",\n        [\"🧍🏽‍♀️\"] = \"woman_standing_tone3\",\n        [\"🧍🏾‍♀️\"] = \"woman_standing_tone4\",\n        [\"🧍🏿‍♀️\"] = \"woman_standing_tone5\",\n        [\"🧍‍♂️\"] = \"man_standing\",\n        [\"🧍🏻‍♂️\"] = \"man_standing_tone1\",\n        [\"🧍🏼‍♂️\"] = \"man_standing_tone2\",\n        [\"🧍🏽‍♂️\"] = \"man_standing_tone3\",\n        [\"🧍🏾‍♂️\"] = \"man_standing_tone4\",\n        [\"🧍🏿‍♂️\"] = \"man_standing_tone5\",\n        [\"🧑‍🤝‍🧑\"] = \"people_holding_hands\",\n        [\"🧑🏻‍🤝‍🧑🏻\"] = \"people_holding_hands_tone1\",\n        [\"🧑🏻‍🤝‍🧑🏼\"] = \"people_holding_hands_tone1_tone2\",\n        [\"🧑🏻‍🤝‍🧑🏽\"] = \"people_holding_hands_tone1_tone3\",\n        [\"🧑🏻‍🤝‍🧑🏾\"] = \"people_holding_hands_tone1_tone4\",\n        [\"🧑🏻‍🤝‍🧑🏿\"] = \"people_holding_hands_tone1_tone5\",\n        [\"🧑🏼‍🤝‍🧑🏻\"] = \"people_holding_hands_tone2_tone1\",\n        [\"🧑🏼‍🤝‍🧑🏼\"] = \"people_holding_hands_tone2\",\n        [\"🧑🏼‍🤝‍🧑🏽\"] = \"people_holding_hands_tone2_tone3\",\n        [\"🧑🏼‍🤝‍🧑🏾\"] = \"people_holding_hands_tone2_tone4\",\n        [\"🧑🏼‍🤝‍🧑🏿\"] = \"people_holding_hands_tone2_tone5\",\n        [\"🧑🏽‍🤝‍🧑🏻\"] = \"people_holding_hands_tone3_tone1\",\n        [\"🧑🏽‍🤝‍🧑🏼\"] = \"people_holding_hands_tone3_tone2\",\n        [\"🧑🏽‍🤝‍🧑🏽\"] = \"people_holding_hands_tone3\",\n        [\"🧑🏽‍🤝‍🧑🏾\"] = \"people_holding_hands_tone3_tone4\",\n        [\"🧑🏽‍🤝‍🧑🏿\"] = \"people_holding_hands_tone3_tone5\",\n        [\"🧑🏾‍🤝‍🧑🏻\"] = \"people_holding_hands_tone4_tone1\",\n        [\"🧑🏾‍🤝‍🧑🏼\"] = \"people_holding_hands_tone4_tone2\",\n        [\"🧑🏾‍🤝‍🧑🏽\"] = \"people_holding_hands_tone4_tone3\",\n        [\"🧑🏾‍🤝‍🧑🏾\"] = \"people_holding_hands_tone4\",\n        [\"🧑🏾‍🤝‍🧑🏿\"] = \"people_holding_hands_tone4_tone5\",\n        [\"🧑🏿‍🤝‍🧑🏻\"] = \"people_holding_hands_tone5_tone1\",\n        [\"🧑🏿‍🤝‍🧑🏼\"] = \"people_holding_hands_tone5_tone2\",\n        [\"🧑🏿‍🤝‍🧑🏽\"] = \"people_holding_hands_tone5_tone3\",\n        [\"🧑🏿‍🤝‍🧑🏾\"] = \"people_holding_hands_tone5_tone4\",\n        [\"🧑🏿‍🤝‍🧑🏿\"] = \"people_holding_hands_tone5\",\n        [\"👫\"] = \"couple\",\n        [\"👫🏻\"] = \"woman_and_man_holding_hands_tone1\",\n        [\"👩🏻‍🤝‍👨🏼\"] = \"woman_and_man_holding_hands_tone1_tone2\",\n        [\"👩🏻‍🤝‍👨🏽\"] = \"woman_and_man_holding_hands_tone1_tone3\",\n        [\"👩🏻‍🤝‍👨🏾\"] = \"woman_and_man_holding_hands_tone1_tone4\",\n        [\"👩🏻‍🤝‍👨🏿\"] = \"woman_and_man_holding_hands_tone1_tone5\",\n        [\"👩🏼‍🤝‍👨🏻\"] = \"woman_and_man_holding_hands_tone2_tone1\",\n        [\"👫🏼\"] = \"woman_and_man_holding_hands_tone2\",\n        [\"👩🏼‍🤝‍👨🏽\"] = \"woman_and_man_holding_hands_tone2_tone3\",\n        [\"👩🏼‍🤝‍👨🏾\"] = \"woman_and_man_holding_hands_tone2_tone4\",\n        [\"👩🏼‍🤝‍👨🏿\"] = \"woman_and_man_holding_hands_tone2_tone5\",\n        [\"👩🏽‍🤝‍👨🏻\"] = \"woman_and_man_holding_hands_tone3_tone1\",\n        [\"👩🏽‍🤝‍👨🏼\"] = \"woman_and_man_holding_hands_tone3_tone2\",\n        [\"👫🏽\"] = \"woman_and_man_holding_hands_tone3\",\n        [\"👩🏽‍🤝‍👨🏾\"] = \"woman_and_man_holding_hands_tone3_tone4\",\n        [\"👩🏽‍🤝‍👨🏿\"] = \"woman_and_man_holding_hands_tone3_tone5\",\n        [\"👩🏾‍🤝‍👨🏻\"] = \"woman_and_man_holding_hands_tone4_tone1\",\n        [\"👩🏾‍🤝‍👨🏼\"] = \"woman_and_man_holding_hands_tone4_tone2\",\n        [\"👩🏾‍🤝‍👨🏽\"] = \"woman_and_man_holding_hands_tone4_tone3\",\n        [\"👫🏾\"] = \"woman_and_man_holding_hands_tone4\",\n        [\"👩🏾‍🤝‍👨🏿\"] = \"woman_and_man_holding_hands_tone4_tone5\",\n        [\"👩🏿‍🤝‍👨🏻\"] = \"woman_and_man_holding_hands_tone5_tone1\",\n        [\"👩🏿‍🤝‍👨🏼\"] = \"woman_and_man_holding_hands_tone5_tone2\",\n        [\"👩🏿‍🤝‍👨🏽\"] = \"woman_and_man_holding_hands_tone5_tone3\",\n        [\"👩🏿‍🤝‍👨🏾\"] = \"woman_and_man_holding_hands_tone5_tone4\",\n        [\"👫🏿\"] = \"woman_and_man_holding_hands_tone5\",\n        [\"👭\"] = \"two_women_holding_hands\",\n        [\"👭🏻\"] = \"women_holding_hands_tone1\",\n        [\"👩🏻‍🤝‍👩🏼\"] = \"women_holding_hands_tone1_tone2\",\n        [\"👩🏻‍🤝‍👩🏽\"] = \"women_holding_hands_tone1_tone3\",\n        [\"👩🏻‍🤝‍👩🏾\"] = \"women_holding_hands_tone1_tone4\",\n        [\"👩🏻‍🤝‍👩🏿\"] = \"women_holding_hands_tone1_tone5\",\n        [\"👩🏼‍🤝‍👩🏻\"] = \"women_holding_hands_tone2_tone1\",\n        [\"👭🏼\"] = \"women_holding_hands_tone2\",\n        [\"👩🏼‍🤝‍👩🏽\"] = \"women_holding_hands_tone2_tone3\",\n        [\"👩🏼‍🤝‍👩🏾\"] = \"women_holding_hands_tone2_tone4\",\n        [\"👩🏼‍🤝‍👩🏿\"] = \"women_holding_hands_tone2_tone5\",\n        [\"👩🏽‍🤝‍👩🏻\"] = \"women_holding_hands_tone3_tone1\",\n        [\"👩🏽‍🤝‍👩🏼\"] = \"women_holding_hands_tone3_tone2\",\n        [\"👭🏽\"] = \"women_holding_hands_tone3\",\n        [\"👩🏽‍🤝‍👩🏾\"] = \"women_holding_hands_tone3_tone4\",\n        [\"👩🏽‍🤝‍👩🏿\"] = \"women_holding_hands_tone3_tone5\",\n        [\"👩🏾‍🤝‍👩🏻\"] = \"women_holding_hands_tone4_tone1\",\n        [\"👩🏾‍🤝‍👩🏼\"] = \"women_holding_hands_tone4_tone2\",\n        [\"👩🏾‍🤝‍👩🏽\"] = \"women_holding_hands_tone4_tone3\",\n        [\"👭🏾\"] = \"women_holding_hands_tone4\",\n        [\"👩🏾‍🤝‍👩🏿\"] = \"women_holding_hands_tone4_tone5\",\n        [\"👩🏿‍🤝‍👩🏻\"] = \"women_holding_hands_tone5_tone1\",\n        [\"👩🏿‍🤝‍👩🏼\"] = \"women_holding_hands_tone5_tone2\",\n        [\"👩🏿‍🤝‍👩🏽\"] = \"women_holding_hands_tone5_tone3\",\n        [\"👩🏿‍🤝‍👩🏾\"] = \"women_holding_hands_tone5_tone4\",\n        [\"👭🏿\"] = \"women_holding_hands_tone5\",\n        [\"👬\"] = \"two_men_holding_hands\",\n        [\"👬🏻\"] = \"men_holding_hands_tone1\",\n        [\"👨🏻‍🤝‍👨🏼\"] = \"men_holding_hands_tone1_tone2\",\n        [\"👨🏻‍🤝‍👨🏽\"] = \"men_holding_hands_tone1_tone3\",\n        [\"👨🏻‍🤝‍👨🏾\"] = \"men_holding_hands_tone1_tone4\",\n        [\"👨🏻‍🤝‍👨🏿\"] = \"men_holding_hands_tone1_tone5\",\n        [\"👨🏼‍🤝‍👨🏻\"] = \"men_holding_hands_tone2_tone1\",\n        [\"👬🏼\"] = \"men_holding_hands_tone2\",\n        [\"👨🏼‍🤝‍👨🏽\"] = \"men_holding_hands_tone2_tone3\",\n        [\"👨🏼‍🤝‍👨🏾\"] = \"men_holding_hands_tone2_tone4\",\n        [\"👨🏼‍🤝‍👨🏿\"] = \"men_holding_hands_tone2_tone5\",\n        [\"👨🏽‍🤝‍👨🏻\"] = \"men_holding_hands_tone3_tone1\",\n        [\"👨🏽‍🤝‍👨🏼\"] = \"men_holding_hands_tone3_tone2\",\n        [\"👬🏽\"] = \"men_holding_hands_tone3\",\n        [\"👨🏽‍🤝‍👨🏾\"] = \"men_holding_hands_tone3_tone4\",\n        [\"👨🏽‍🤝‍👨🏿\"] = \"men_holding_hands_tone3_tone5\",\n        [\"👨🏾‍🤝‍👨🏻\"] = \"men_holding_hands_tone4_tone1\",\n        [\"👨🏾‍🤝‍👨🏼\"] = \"men_holding_hands_tone4_tone2\",\n        [\"👨🏾‍🤝‍👨🏽\"] = \"men_holding_hands_tone4_tone3\",\n        [\"👬🏾\"] = \"men_holding_hands_tone4\",\n        [\"👨🏾‍🤝‍👨🏿\"] = \"men_holding_hands_tone4_tone5\",\n        [\"👨🏿‍🤝‍👨🏻\"] = \"men_holding_hands_tone5_tone1\",\n        [\"👨🏿‍🤝‍👨🏼\"] = \"men_holding_hands_tone5_tone2\",\n        [\"👨🏿‍🤝‍👨🏽\"] = \"men_holding_hands_tone5_tone3\",\n        [\"👨🏿‍🤝‍👨🏾\"] = \"men_holding_hands_tone5_tone4\",\n        [\"👬🏿\"] = \"men_holding_hands_tone5\",\n        [\"💑\"] = \"couple_with_heart\",\n        [\"💑🏻\"] = \"couple_with_heart_tone1\",\n        [\"🧑🏻‍❤️‍🧑🏼\"] = \"couple_with_heart_person_person_tone1_tone2\",\n        [\"🧑🏻‍❤️‍🧑🏽\"] = \"couple_with_heart_person_person_tone1_tone3\",\n        [\"🧑🏻‍❤️‍🧑🏾\"] = \"couple_with_heart_person_person_tone1_tone4\",\n        [\"🧑🏻‍❤️‍🧑🏿\"] = \"couple_with_heart_person_person_tone1_tone5\",\n        [\"🧑🏼‍❤️‍🧑🏻\"] = \"couple_with_heart_person_person_tone2_tone1\",\n        [\"💑🏼\"] = \"couple_with_heart_tone2\",\n        [\"🧑🏼‍❤️‍🧑🏽\"] = \"couple_with_heart_person_person_tone2_tone3\",\n        [\"🧑🏼‍❤️‍🧑🏾\"] = \"couple_with_heart_person_person_tone2_tone4\",\n        [\"🧑🏼‍❤️‍🧑🏿\"] = \"couple_with_heart_person_person_tone2_tone5\",\n        [\"🧑🏽‍❤️‍🧑🏻\"] = \"couple_with_heart_person_person_tone3_tone1\",\n        [\"🧑🏽‍❤️‍🧑🏼\"] = \"couple_with_heart_person_person_tone3_tone2\",\n        [\"💑🏽\"] = \"couple_with_heart_tone3\",\n        [\"🧑🏽‍❤️‍🧑🏾\"] = \"couple_with_heart_person_person_tone3_tone4\",\n        [\"🧑🏽‍❤️‍🧑🏿\"] = \"couple_with_heart_person_person_tone3_tone5\",\n        [\"🧑🏾‍❤️‍🧑🏻\"] = \"couple_with_heart_person_person_tone4_tone1\",\n        [\"🧑🏾‍❤️‍🧑🏼\"] = \"couple_with_heart_person_person_tone4_tone2\",\n        [\"🧑🏾‍❤️‍🧑🏽\"] = \"couple_with_heart_person_person_tone4_tone3\",\n        [\"💑🏾\"] = \"couple_with_heart_tone4\",\n        [\"🧑🏾‍❤️‍🧑🏿\"] = \"couple_with_heart_person_person_tone4_tone5\",\n        [\"🧑🏿‍❤️‍🧑🏻\"] = \"couple_with_heart_person_person_tone5_tone1\",\n        [\"🧑🏿‍❤️‍🧑🏼\"] = \"couple_with_heart_person_person_tone5_tone2\",\n        [\"🧑🏿‍❤️‍🧑🏽\"] = \"couple_with_heart_person_person_tone5_tone3\",\n        [\"🧑🏿‍❤️‍🧑🏾\"] = \"couple_with_heart_person_person_tone5_tone4\",\n        [\"💑🏿\"] = \"couple_with_heart_tone5\",\n        [\"👩‍❤️‍👨\"] = \"couple_with_heart_woman_man\",\n        [\"👩🏻‍❤️‍👨🏻\"] = \"couple_with_heart_woman_man_tone1\",\n        [\"👩🏻‍❤️‍👨🏼\"] = \"couple_with_heart_woman_man_tone1_tone2\",\n        [\"👩🏻‍❤️‍👨🏽\"] = \"couple_with_heart_woman_man_tone1_tone3\",\n        [\"👩🏻‍❤️‍👨🏾\"] = \"couple_with_heart_woman_man_tone1_tone4\",\n        [\"👩🏻‍❤️‍👨🏿\"] = \"couple_with_heart_woman_man_tone1_tone5\",\n        [\"👩🏼‍❤️‍👨🏻\"] = \"couple_with_heart_woman_man_tone2_tone1\",\n        [\"👩🏼‍❤️‍👨🏼\"] = \"couple_with_heart_woman_man_tone2\",\n        [\"👩🏼‍❤️‍👨🏽\"] = \"couple_with_heart_woman_man_tone2_tone3\",\n        [\"👩🏼‍❤️‍👨🏾\"] = \"couple_with_heart_woman_man_tone2_tone4\",\n        [\"👩🏼‍❤️‍👨🏿\"] = \"couple_with_heart_woman_man_tone2_tone5\",\n        [\"👩🏽‍❤️‍👨🏻\"] = \"couple_with_heart_woman_man_tone3_tone1\",\n        [\"👩🏽‍❤️‍👨🏼\"] = \"couple_with_heart_woman_man_tone3_tone2\",\n        [\"👩🏽‍❤️‍👨🏽\"] = \"couple_with_heart_woman_man_tone3\",\n        [\"👩🏽‍❤️‍👨🏾\"] = \"couple_with_heart_woman_man_tone3_tone4\",\n        [\"👩🏽‍❤️‍👨🏿\"] = \"couple_with_heart_woman_man_tone3_tone5\",\n        [\"👩🏾‍❤️‍👨🏻\"] = \"couple_with_heart_woman_man_tone4_tone1\",\n        [\"👩🏾‍❤️‍👨🏼\"] = \"couple_with_heart_woman_man_tone4_tone2\",\n        [\"👩🏾‍❤️‍👨🏽\"] = \"couple_with_heart_woman_man_tone4_tone3\",\n        [\"👩🏾‍❤️‍👨🏾\"] = \"couple_with_heart_woman_man_tone4\",\n        [\"👩🏾‍❤️‍👨🏿\"] = \"couple_with_heart_woman_man_tone4_tone5\",\n        [\"👩🏿‍❤️‍👨🏻\"] = \"couple_with_heart_woman_man_tone5_tone1\",\n        [\"👩🏿‍❤️‍👨🏼\"] = \"couple_with_heart_woman_man_tone5_tone2\",\n        [\"👩🏿‍❤️‍👨🏽\"] = \"couple_with_heart_woman_man_tone5_tone3\",\n        [\"👩🏿‍❤️‍👨🏾\"] = \"couple_with_heart_woman_man_tone5_tone4\",\n        [\"👩🏿‍❤️‍👨🏿\"] = \"couple_with_heart_woman_man_tone5\",\n        [\"👩‍❤️‍👩\"] = \"couple_ww\",\n        [\"👩🏻‍❤️‍👩🏻\"] = \"couple_with_heart_woman_woman_tone1\",\n        [\"👩🏻‍❤️‍👩🏼\"] = \"couple_with_heart_woman_woman_tone1_tone2\",\n        [\"👩🏻‍❤️‍👩🏽\"] = \"couple_with_heart_woman_woman_tone1_tone3\",\n        [\"👩🏻‍❤️‍👩🏾\"] = \"couple_with_heart_woman_woman_tone1_tone4\",\n        [\"👩🏻‍❤️‍👩🏿\"] = \"couple_with_heart_woman_woman_tone1_tone5\",\n        [\"👩🏼‍❤️‍👩🏻\"] = \"couple_with_heart_woman_woman_tone2_tone1\",\n        [\"👩🏼‍❤️‍👩🏼\"] = \"couple_with_heart_woman_woman_tone2\",\n        [\"👩🏼‍❤️‍👩🏽\"] = \"couple_with_heart_woman_woman_tone2_tone3\",\n        [\"👩🏼‍❤️‍👩🏾\"] = \"couple_with_heart_woman_woman_tone2_tone4\",\n        [\"👩🏼‍❤️‍👩🏿\"] = \"couple_with_heart_woman_woman_tone2_tone5\",\n        [\"👩🏽‍❤️‍👩🏻\"] = \"couple_with_heart_woman_woman_tone3_tone1\",\n        [\"👩🏽‍❤️‍👩🏼\"] = \"couple_with_heart_woman_woman_tone3_tone2\",\n        [\"👩🏽‍❤️‍👩🏽\"] = \"couple_with_heart_woman_woman_tone3\",\n        [\"👩🏽‍❤️‍👩🏾\"] = \"couple_with_heart_woman_woman_tone3_tone4\",\n        [\"👩🏽‍❤️‍👩🏿\"] = \"couple_with_heart_woman_woman_tone3_tone5\",\n        [\"👩🏾‍❤️‍👩🏻\"] = \"couple_with_heart_woman_woman_tone4_tone1\",\n        [\"👩🏾‍❤️‍👩🏼\"] = \"couple_with_heart_woman_woman_tone4_tone2\",\n        [\"👩🏾‍❤️‍👩🏽\"] = \"couple_with_heart_woman_woman_tone4_tone3\",\n        [\"👩🏾‍❤️‍👩🏾\"] = \"couple_with_heart_woman_woman_tone4\",\n        [\"👩🏾‍❤️‍👩🏿\"] = \"couple_with_heart_woman_woman_tone4_tone5\",\n        [\"👩🏿‍❤️‍👩🏻\"] = \"couple_with_heart_woman_woman_tone5_tone1\",\n        [\"👩🏿‍❤️‍👩🏼\"] = \"couple_with_heart_woman_woman_tone5_tone2\",\n        [\"👩🏿‍❤️‍👩🏽\"] = \"couple_with_heart_woman_woman_tone5_tone3\",\n        [\"👩🏿‍❤️‍👩🏾\"] = \"couple_with_heart_woman_woman_tone5_tone4\",\n        [\"👩🏿‍❤️‍👩🏿\"] = \"couple_with_heart_woman_woman_tone5\",\n        [\"👨‍❤️‍👨\"] = \"couple_mm\",\n        [\"👨🏻‍❤️‍👨🏻\"] = \"couple_with_heart_man_man_tone1\",\n        [\"👨🏻‍❤️‍👨🏼\"] = \"couple_with_heart_man_man_tone1_tone2\",\n        [\"👨🏻‍❤️‍👨🏽\"] = \"couple_with_heart_man_man_tone1_tone3\",\n        [\"👨🏻‍❤️‍👨🏾\"] = \"couple_with_heart_man_man_tone1_tone4\",\n        [\"👨🏻‍❤️‍👨🏿\"] = \"couple_with_heart_man_man_tone1_tone5\",\n        [\"👨🏼‍❤️‍👨🏻\"] = \"couple_with_heart_man_man_tone2_tone1\",\n        [\"👨🏼‍❤️‍👨🏼\"] = \"couple_with_heart_man_man_tone2\",\n        [\"👨🏼‍❤️‍👨🏽\"] = \"couple_with_heart_man_man_tone2_tone3\",\n        [\"👨🏼‍❤️‍👨🏾\"] = \"couple_with_heart_man_man_tone2_tone4\",\n        [\"👨🏼‍❤️‍👨🏿\"] = \"couple_with_heart_man_man_tone2_tone5\",\n        [\"👨🏽‍❤️‍👨🏻\"] = \"couple_with_heart_man_man_tone3_tone1\",\n        [\"👨🏽‍❤️‍👨🏼\"] = \"couple_with_heart_man_man_tone3_tone2\",\n        [\"👨🏽‍❤️‍👨🏽\"] = \"couple_with_heart_man_man_tone3\",\n        [\"👨🏽‍❤️‍👨🏾\"] = \"couple_with_heart_man_man_tone3_tone4\",\n        [\"👨🏽‍❤️‍👨🏿\"] = \"couple_with_heart_man_man_tone3_tone5\",\n        [\"👨🏾‍❤️‍👨🏻\"] = \"couple_with_heart_man_man_tone4_tone1\",\n        [\"👨🏾‍❤️‍👨🏼\"] = \"couple_with_heart_man_man_tone4_tone2\",\n        [\"👨🏾‍❤️‍👨🏽\"] = \"couple_with_heart_man_man_tone4_tone3\",\n        [\"👨🏾‍❤️‍👨🏾\"] = \"couple_with_heart_man_man_tone4\",\n        [\"👨🏾‍❤️‍👨🏿\"] = \"couple_with_heart_man_man_tone4_tone5\",\n        [\"👨🏿‍❤️‍👨🏻\"] = \"couple_with_heart_man_man_tone5_tone1\",\n        [\"👨🏿‍❤️‍👨🏼\"] = \"couple_with_heart_man_man_tone5_tone2\",\n        [\"👨🏿‍❤️‍👨🏽\"] = \"couple_with_heart_man_man_tone5_tone3\",\n        [\"👨🏿‍❤️‍👨🏾\"] = \"couple_with_heart_man_man_tone5_tone4\",\n        [\"👨🏿‍❤️‍👨🏿\"] = \"couple_with_heart_man_man_tone5\",\n        [\"💏\"] = \"couplekiss\",\n        [\"🧑🏿‍❤️‍💋‍🧑🏾\"] = \"kiss_person_person_tone5_tone4\",\n        [\"💏🏻\"] = \"kiss_tone1\",\n        [\"🧑🏻‍❤️‍💋‍🧑🏼\"] = \"kiss_person_person_tone1_tone2\",\n        [\"🧑🏻‍❤️‍💋‍🧑🏽\"] = \"kiss_person_person_tone1_tone3\",\n        [\"🧑🏻‍❤️‍💋‍🧑🏾\"] = \"kiss_person_person_tone1_tone4\",\n        [\"🧑🏻‍❤️‍💋‍🧑🏿\"] = \"kiss_person_person_tone1_tone5\",\n        [\"🧑🏼‍❤️‍💋‍🧑🏻\"] = \"kiss_person_person_tone2_tone1\",\n        [\"💏🏼\"] = \"kiss_tone2\",\n        [\"🧑🏼‍❤️‍💋‍🧑🏽\"] = \"kiss_person_person_tone2_tone3\",\n        [\"🧑🏼‍❤️‍💋‍🧑🏾\"] = \"kiss_person_person_tone2_tone4\",\n        [\"🧑🏼‍❤️‍💋‍🧑🏿\"] = \"kiss_person_person_tone2_tone5\",\n        [\"🧑🏽‍❤️‍💋‍🧑🏻\"] = \"kiss_person_person_tone3_tone1\",\n        [\"🧑🏽‍❤️‍💋‍🧑🏼\"] = \"kiss_person_person_tone3_tone2\",\n        [\"💏🏽\"] = \"kiss_tone3\",\n        [\"🧑🏽‍❤️‍💋‍🧑🏾\"] = \"kiss_person_person_tone3_tone4\",\n        [\"🧑🏽‍❤️‍💋‍🧑🏿\"] = \"kiss_person_person_tone3_tone5\",\n        [\"🧑🏾‍❤️‍💋‍🧑🏻\"] = \"kiss_person_person_tone4_tone1\",\n        [\"🧑🏾‍❤️‍💋‍🧑🏼\"] = \"kiss_person_person_tone4_tone2\",\n        [\"🧑🏾‍❤️‍💋‍🧑🏽\"] = \"kiss_person_person_tone4_tone3\",\n        [\"💏🏾\"] = \"kiss_tone4\",\n        [\"🧑🏾‍❤️‍💋‍🧑🏿\"] = \"kiss_person_person_tone4_tone5\",\n        [\"🧑🏿‍❤️‍💋‍🧑🏻\"] = \"kiss_person_person_tone5_tone1\",\n        [\"🧑🏿‍❤️‍💋‍🧑🏼\"] = \"kiss_person_person_tone5_tone2\",\n        [\"🧑🏿‍❤️‍💋‍🧑🏽\"] = \"kiss_person_person_tone5_tone3\",\n        [\"💏🏿\"] = \"kiss_tone5\",\n        [\"👩‍❤️‍💋‍👨\"] = \"kiss_woman_man\",\n        [\"👩🏻‍❤️‍💋‍👨🏻\"] = \"kiss_woman_man_tone1\",\n        [\"👩🏻‍❤️‍💋‍👨🏼\"] = \"kiss_woman_man_tone1_tone2\",\n        [\"👩🏻‍❤️‍💋‍👨🏽\"] = \"kiss_woman_man_tone1_tone3\",\n        [\"👩🏻‍❤️‍💋‍👨🏾\"] = \"kiss_woman_man_tone1_tone4\",\n        [\"👩🏻‍❤️‍💋‍👨🏿\"] = \"kiss_woman_man_tone1_tone5\",\n        [\"👩🏼‍❤️‍💋‍👨🏻\"] = \"kiss_woman_man_tone2_tone1\",\n        [\"👩🏼‍❤️‍💋‍👨🏼\"] = \"kiss_woman_man_tone2\",\n        [\"👩🏼‍❤️‍💋‍👨🏽\"] = \"kiss_woman_man_tone2_tone3\",\n        [\"👩🏼‍❤️‍💋‍👨🏾\"] = \"kiss_woman_man_tone2_tone4\",\n        [\"👩🏼‍❤️‍💋‍👨🏿\"] = \"kiss_woman_man_tone2_tone5\",\n        [\"👩🏽‍❤️‍💋‍👨🏻\"] = \"kiss_woman_man_tone3_tone1\",\n        [\"👩🏽‍❤️‍💋‍👨🏼\"] = \"kiss_woman_man_tone3_tone2\",\n        [\"👩🏽‍❤️‍💋‍👨🏽\"] = \"kiss_woman_man_tone3\",\n        [\"👩🏽‍❤️‍💋‍👨🏾\"] = \"kiss_woman_man_tone3_tone4\",\n        [\"👩🏽‍❤️‍💋‍👨🏿\"] = \"kiss_woman_man_tone3_tone5\",\n        [\"👩🏾‍❤️‍💋‍👨🏻\"] = \"kiss_woman_man_tone4_tone1\",\n        [\"👩🏾‍❤️‍💋‍👨🏼\"] = \"kiss_woman_man_tone4_tone2\",\n        [\"👩🏾‍❤️‍💋‍👨🏽\"] = \"kiss_woman_man_tone4_tone3\",\n        [\"👩🏾‍❤️‍💋‍👨🏾\"] = \"kiss_woman_man_tone4\",\n        [\"👩🏾‍❤️‍💋‍👨🏿\"] = \"kiss_woman_man_tone4_tone5\",\n        [\"👩🏿‍❤️‍💋‍👨🏻\"] = \"kiss_woman_man_tone5_tone1\",\n        [\"👩🏿‍❤️‍💋‍👨🏼\"] = \"kiss_woman_man_tone5_tone2\",\n        [\"👩🏿‍❤️‍💋‍👨🏽\"] = \"kiss_woman_man_tone5_tone3\",\n        [\"👩🏿‍❤️‍💋‍👨🏾\"] = \"kiss_woman_man_tone5_tone4\",\n        [\"👩🏿‍❤️‍💋‍👨🏿\"] = \"kiss_woman_man_tone5\",\n        [\"👩‍❤️‍💋‍👩\"] = \"kiss_ww\",\n        [\"👩🏻‍❤️‍💋‍👩🏻\"] = \"kiss_woman_woman_tone1\",\n        [\"👩🏻‍❤️‍💋‍👩🏼\"] = \"kiss_woman_woman_tone1_tone2\",\n        [\"👩🏻‍❤️‍💋‍👩🏽\"] = \"kiss_woman_woman_tone1_tone3\",\n        [\"👩🏻‍❤️‍💋‍👩🏾\"] = \"kiss_woman_woman_tone1_tone4\",\n        [\"👩🏻‍❤️‍💋‍👩🏿\"] = \"kiss_woman_woman_tone1_tone5\",\n        [\"👩🏼‍❤️‍💋‍👩🏻\"] = \"kiss_woman_woman_tone2_tone1\",\n        [\"👩🏼‍❤️‍💋‍👩🏼\"] = \"kiss_woman_woman_tone2\",\n        [\"👩🏼‍❤️‍💋‍👩🏽\"] = \"kiss_woman_woman_tone2_tone3\",\n        [\"👩🏼‍❤️‍💋‍👩🏾\"] = \"kiss_woman_woman_tone2_tone4\",\n        [\"👩🏼‍❤️‍💋‍👩🏿\"] = \"kiss_woman_woman_tone2_tone5\",\n        [\"👩🏽‍❤️‍💋‍👩🏻\"] = \"kiss_woman_woman_tone3_tone1\",\n        [\"👩🏽‍❤️‍💋‍👩🏼\"] = \"kiss_woman_woman_tone3_tone2\",\n        [\"👩🏽‍❤️‍💋‍👩🏽\"] = \"kiss_woman_woman_tone3\",\n        [\"👩🏽‍❤️‍💋‍👩🏾\"] = \"kiss_woman_woman_tone3_tone4\",\n        [\"👩🏽‍❤️‍💋‍👩🏿\"] = \"kiss_woman_woman_tone3_tone5\",\n        [\"👩🏾‍❤️‍💋‍👩🏻\"] = \"kiss_woman_woman_tone4_tone1\",\n        [\"👩🏾‍❤️‍💋‍👩🏼\"] = \"kiss_woman_woman_tone4_tone2\",\n        [\"👩🏾‍❤️‍💋‍👩🏽\"] = \"kiss_woman_woman_tone4_tone3\",\n        [\"👩🏾‍❤️‍💋‍👩🏾\"] = \"kiss_woman_woman_tone4\",\n        [\"👩🏾‍❤️‍💋‍👩🏿\"] = \"kiss_woman_woman_tone4_tone5\",\n        [\"👩🏿‍❤️‍💋‍👩🏻\"] = \"kiss_woman_woman_tone5_tone1\",\n        [\"👩🏿‍❤️‍💋‍👩🏼\"] = \"kiss_woman_woman_tone5_tone2\",\n        [\"👩🏿‍❤️‍💋‍👩🏽\"] = \"kiss_woman_woman_tone5_tone3\",\n        [\"👩🏿‍❤️‍💋‍👩🏾\"] = \"kiss_woman_woman_tone5_tone4\",\n        [\"👩🏿‍❤️‍💋‍👩🏿\"] = \"kiss_woman_woman_tone5\",\n        [\"👨‍❤️‍💋‍👨\"] = \"kiss_mm\",\n        [\"👨🏻‍❤️‍💋‍👨🏻\"] = \"kiss_man_man_tone1\",\n        [\"👨🏻‍❤️‍💋‍👨🏼\"] = \"kiss_man_man_tone1_tone2\",\n        [\"👨🏻‍❤️‍💋‍👨🏽\"] = \"kiss_man_man_tone1_tone3\",\n        [\"👨🏻‍❤️‍💋‍👨🏾\"] = \"kiss_man_man_tone1_tone4\",\n        [\"👨🏻‍❤️‍💋‍👨🏿\"] = \"kiss_man_man_tone1_tone5\",\n        [\"👨🏼‍❤️‍💋‍👨🏻\"] = \"kiss_man_man_tone2_tone1\",\n        [\"👨🏼‍❤️‍💋‍👨🏼\"] = \"kiss_man_man_tone2\",\n        [\"👨🏼‍❤️‍💋‍👨🏽\"] = \"kiss_man_man_tone2_tone3\",\n        [\"👨🏼‍❤️‍💋‍👨🏾\"] = \"kiss_man_man_tone2_tone4\",\n        [\"👨🏼‍❤️‍💋‍👨🏿\"] = \"kiss_man_man_tone2_tone5\",\n        [\"👨🏽‍❤️‍💋‍👨🏻\"] = \"kiss_man_man_tone3_tone1\",\n        [\"👨🏽‍❤️‍💋‍👨🏼\"] = \"kiss_man_man_tone3_tone2\",\n        [\"👨🏽‍❤️‍💋‍👨🏽\"] = \"kiss_man_man_tone3\",\n        [\"👨🏽‍❤️‍💋‍👨🏾\"] = \"kiss_man_man_tone3_tone4\",\n        [\"👨🏽‍❤️‍💋‍👨🏿\"] = \"kiss_man_man_tone3_tone5\",\n        [\"👨🏾‍❤️‍💋‍👨🏻\"] = \"kiss_man_man_tone4_tone1\",\n        [\"👨🏾‍❤️‍💋‍👨🏼\"] = \"kiss_man_man_tone4_tone2\",\n        [\"👨🏾‍❤️‍💋‍👨🏽\"] = \"kiss_man_man_tone4_tone3\",\n        [\"👨🏾‍❤️‍💋‍👨🏾\"] = \"kiss_man_man_tone4\",\n        [\"👨🏾‍❤️‍💋‍👨🏿\"] = \"kiss_man_man_tone4_tone5\",\n        [\"👨🏿‍❤️‍💋‍👨🏻\"] = \"kiss_man_man_tone5_tone1\",\n        [\"👨🏿‍❤️‍💋‍👨🏼\"] = \"kiss_man_man_tone5_tone2\",\n        [\"👨🏿‍❤️‍💋‍👨🏽\"] = \"kiss_man_man_tone5_tone3\",\n        [\"👨🏿‍❤️‍💋‍👨🏾\"] = \"kiss_man_man_tone5_tone4\",\n        [\"👨🏿‍❤️‍💋‍👨🏿\"] = \"kiss_man_man_tone5\",\n        [\"👪\"] = \"family\",\n        [\"👨‍👩‍👦\"] = \"family_man_woman_boy\",\n        [\"👨‍👩‍👧\"] = \"family_mwg\",\n        [\"👨‍👩‍👧‍👦\"] = \"family_mwgb\",\n        [\"👨‍👩‍👦‍👦\"] = \"family_mwbb\",\n        [\"👨‍👩‍👧‍👧\"] = \"family_mwgg\",\n        [\"👩‍👩‍👦\"] = \"family_wwb\",\n        [\"👩‍👩‍👧\"] = \"family_wwg\",\n        [\"👩‍👩‍👧‍👦\"] = \"family_wwgb\",\n        [\"👩‍👩‍👦‍👦\"] = \"family_wwbb\",\n        [\"👩‍👩‍👧‍👧\"] = \"family_wwgg\",\n        [\"👨‍👨‍👦\"] = \"family_mmb\",\n        [\"👨‍👨‍👧\"] = \"family_mmg\",\n        [\"👨‍👨‍👧‍👦\"] = \"family_mmgb\",\n        [\"👨‍👨‍👦‍👦\"] = \"family_mmbb\",\n        [\"👨‍👨‍👧‍👧\"] = \"family_mmgg\",\n        [\"👩‍👦\"] = \"family_woman_boy\",\n        [\"👩‍👧\"] = \"family_woman_girl\",\n        [\"👩‍👧‍👦\"] = \"family_woman_girl_boy\",\n        [\"👩‍👦‍👦\"] = \"family_woman_boy_boy\",\n        [\"👩‍👧‍👧\"] = \"family_woman_girl_girl\",\n        [\"👨‍👦\"] = \"family_man_boy\",\n        [\"👨‍👧\"] = \"family_man_girl\",\n        [\"👨‍👧‍👦\"] = \"family_man_girl_boy\",\n        [\"👨‍👦‍👦\"] = \"family_man_boy_boy\",\n        [\"👨‍👧‍👧\"] = \"family_man_girl_girl\",\n        [\"🧶\"] = \"yarn\",\n        [\"🧵\"] = \"thread\",\n        [\"🧥\"] = \"coat\",\n        [\"🥼\"] = \"lab_coat\",\n        [\"🦺\"] = \"safety_vest\",\n        [\"👚\"] = \"womans_clothes\",\n        [\"👕\"] = \"shirt\",\n        [\"👖\"] = \"jeans\",\n        [\"🩲\"] = \"briefs\",\n        [\"🩳\"] = \"shorts\",\n        [\"👔\"] = \"necktie\",\n        [\"👗\"] = \"dress\",\n        [\"👙\"] = \"bikini\",\n        [\"🩱\"] = \"one_piece_swimsuit\",\n        [\"👘\"] = \"kimono\",\n        [\"🥻\"] = \"sari\",\n        [\"🥿\"] = \"womans_flat_shoe\",\n        [\"👠\"] = \"high_heel\",\n        [\"👡\"] = \"sandal\",\n        [\"👢\"] = \"boot\",\n        [\"👞\"] = \"mans_shoe\",\n        [\"👟\"] = \"athletic_shoe\",\n        [\"🥾\"] = \"hiking_boot\",\n        [\"🩴\"] = \"thong_sandal\",\n        [\"🧦\"] = \"socks\",\n        [\"🧤\"] = \"gloves\",\n        [\"🧣\"] = \"scarf\",\n        [\"🎩\"] = \"tophat\",\n        [\"🧢\"] = \"billed_cap\",\n        [\"👒\"] = \"womans_hat\",\n        [\"🎓\"] = \"mortar_board\",\n        [\"⛑️\"] = \"helmet_with_cross\",\n        [\"🪖\"] = \"military_helmet\",\n        [\"👑\"] = \"crown\",\n        [\"💍\"] = \"ring\",\n        [\"👝\"] = \"pouch\",\n        [\"👛\"] = \"purse\",\n        [\"👜\"] = \"handbag\",\n        [\"💼\"] = \"briefcase\",\n        [\"🎒\"] = \"school_satchel\",\n        [\"🧳\"] = \"luggage\",\n        [\"👓\"] = \"eyeglasses\",\n        [\"🕶️\"] = \"dark_sunglasses\",\n        [\"🥽\"] = \"goggles\",\n        [\"🌂\"] = \"closed_umbrella\",\n        [\"🐶\"] = \"dog\",\n        [\"🐱\"] = \"cat\",\n        [\"🐭\"] = \"mouse\",\n        [\"🐹\"] = \"hamster\",\n        [\"🐰\"] = \"rabbit\",\n        [\"🦊\"] = \"fox\",\n        [\"🐻\"] = \"bear\",\n        [\"🐼\"] = \"panda_face\",\n        [\"🐻‍❄️\"] = \"polar_bear\",\n        [\"🐨\"] = \"koala\",\n        [\"🐯\"] = \"tiger\",\n        [\"🦁\"] = \"lion_face\",\n        [\"🐮\"] = \"cow\",\n        [\"🐷\"] = \"pig\",\n        [\"🐽\"] = \"pig_nose\",\n        [\"🐸\"] = \"frog\",\n        [\"🐵\"] = \"monkey_face\",\n        [\"🙈\"] = \"see_no_evil\",\n        [\"🙉\"] = \"hear_no_evil\",\n        [\"🙊\"] = \"speak_no_evil\",\n        [\"🐒\"] = \"monkey\",\n        [\"🐔\"] = \"chicken\",\n        [\"🐧\"] = \"penguin\",\n        [\"🐦\"] = \"bird\",\n        [\"🐤\"] = \"baby_chick\",\n        [\"🐣\"] = \"hatching_chick\",\n        [\"🐥\"] = \"hatched_chick\",\n        [\"🦆\"] = \"duck\",\n        [\"🦤\"] = \"dodo\",\n        [\"🦅\"] = \"eagle\",\n        [\"🦉\"] = \"owl\",\n        [\"🦇\"] = \"bat\",\n        [\"🐺\"] = \"wolf\",\n        [\"🐗\"] = \"boar\",\n        [\"🐴\"] = \"horse\",\n        [\"🦄\"] = \"unicorn\",\n        [\"🐝\"] = \"bee\",\n        [\"🐛\"] = \"bug\",\n        [\"🦋\"] = \"butterfly\",\n        [\"🐌\"] = \"snail\",\n        [\"🪱\"] = \"worm\",\n        [\"🐞\"] = \"lady_beetle\",\n        [\"🐜\"] = \"ant\",\n        [\"🪰\"] = \"fly\",\n        [\"🦟\"] = \"mosquito\",\n        [\"🪳\"] = \"cockroach\",\n        [\"🪲\"] = \"beetle\",\n        [\"🦗\"] = \"cricket\",\n        [\"🕷️\"] = \"spider\",\n        [\"🕸️\"] = \"spider_web\",\n        [\"🦂\"] = \"scorpion\",\n        [\"🐢\"] = \"turtle\",\n        [\"🐍\"] = \"snake\",\n        [\"🦎\"] = \"lizard\",\n        [\"🦖\"] = \"t_rex\",\n        [\"🦕\"] = \"sauropod\",\n        [\"🐙\"] = \"octopus\",\n        [\"🦑\"] = \"squid\",\n        [\"🦐\"] = \"shrimp\",\n        [\"🦞\"] = \"lobster\",\n        [\"🦀\"] = \"crab\",\n        [\"🐡\"] = \"blowfish\",\n        [\"🐠\"] = \"tropical_fish\",\n        [\"🐟\"] = \"fish\",\n        [\"🦭\"] = \"seal\",\n        [\"🐬\"] = \"dolphin\",\n        [\"🐳\"] = \"whale\",\n        [\"🐋\"] = \"whale2\",\n        [\"🦈\"] = \"shark\",\n        [\"🐊\"] = \"crocodile\",\n        [\"🐅\"] = \"tiger2\",\n        [\"🐆\"] = \"leopard\",\n        [\"🦓\"] = \"zebra\",\n        [\"🦍\"] = \"gorilla\",\n        [\"🦧\"] = \"orangutan\",\n        [\"🐘\"] = \"elephant\",\n        [\"🦣\"] = \"mammoth\",\n        [\"🦬\"] = \"bison\",\n        [\"🦛\"] = \"hippopotamus\",\n        [\"🦏\"] = \"rhino\",\n        [\"🐪\"] = \"dromedary_camel\",\n        [\"🐫\"] = \"camel\",\n        [\"🦒\"] = \"giraffe\",\n        [\"🦘\"] = \"kangaroo\",\n        [\"🐃\"] = \"water_buffalo\",\n        [\"🐂\"] = \"ox\",\n        [\"🐄\"] = \"cow2\",\n        [\"🐎\"] = \"racehorse\",\n        [\"🐖\"] = \"pig2\",\n        [\"🐏\"] = \"ram\",\n        [\"🐑\"] = \"sheep\",\n        [\"🦙\"] = \"llama\",\n        [\"🐐\"] = \"goat\",\n        [\"🦌\"] = \"deer\",\n        [\"🐕\"] = \"dog2\",\n        [\"🐩\"] = \"poodle\",\n        [\"🦮\"] = \"guide_dog\",\n        [\"🐕‍🦺\"] = \"service_dog\",\n        [\"🐈\"] = \"cat2\",\n        [\"🐈‍⬛\"] = \"black_cat\",\n        [\"🐓\"] = \"rooster\",\n        [\"🦃\"] = \"turkey\",\n        [\"🦚\"] = \"peacock\",\n        [\"🦜\"] = \"parrot\",\n        [\"🦢\"] = \"swan\",\n        [\"🦩\"] = \"flamingo\",\n        [\"🕊️\"] = \"dove\",\n        [\"🐇\"] = \"rabbit2\",\n        [\"🦝\"] = \"raccoon\",\n        [\"🦨\"] = \"skunk\",\n        [\"🦡\"] = \"badger\",\n        [\"🦫\"] = \"beaver\",\n        [\"🦦\"] = \"otter\",\n        [\"🦥\"] = \"sloth\",\n        [\"🐁\"] = \"mouse2\",\n        [\"🐀\"] = \"rat\",\n        [\"🐿️\"] = \"chipmunk\",\n        [\"🦔\"] = \"hedgehog\",\n        [\"🐾\"] = \"feet\",\n        [\"🐉\"] = \"dragon\",\n        [\"🐲\"] = \"dragon_face\",\n        [\"🌵\"] = \"cactus\",\n        [\"🎄\"] = \"christmas_tree\",\n        [\"🌲\"] = \"evergreen_tree\",\n        [\"🌳\"] = \"deciduous_tree\",\n        [\"🌴\"] = \"palm_tree\",\n        [\"🌱\"] = \"seedling\",\n        [\"🌿\"] = \"herb\",\n        [\"☘️\"] = \"shamrock\",\n        [\"🍀\"] = \"four_leaf_clover\",\n        [\"🎍\"] = \"bamboo\",\n        [\"🎋\"] = \"tanabata_tree\",\n        [\"🍃\"] = \"leaves\",\n        [\"🍂\"] = \"fallen_leaf\",\n        [\"🍁\"] = \"maple_leaf\",\n        [\"🪶\"] = \"feather\",\n        [\"🍄\"] = \"mushroom\",\n        [\"🐚\"] = \"shell\",\n        [\"🪨\"] = \"rock\",\n        [\"🪵\"] = \"wood\",\n        [\"🌾\"] = \"ear_of_rice\",\n        [\"🪴\"] = \"potted_plant\",\n        [\"💐\"] = \"bouquet\",\n        [\"🌷\"] = \"tulip\",\n        [\"🌹\"] = \"rose\",\n        [\"🥀\"] = \"wilted_rose\",\n        [\"🌺\"] = \"hibiscus\",\n        [\"🌸\"] = \"cherry_blossom\",\n        [\"🌼\"] = \"blossom\",\n        [\"🌻\"] = \"sunflower\",\n        [\"🌞\"] = \"sun_with_face\",\n        [\"🌝\"] = \"full_moon_with_face\",\n        [\"🌛\"] = \"first_quarter_moon_with_face\",\n        [\"🌜\"] = \"last_quarter_moon_with_face\",\n        [\"🌚\"] = \"new_moon_with_face\",\n        [\"🌕\"] = \"full_moon\",\n        [\"🌖\"] = \"waning_gibbous_moon\",\n        [\"🌗\"] = \"last_quarter_moon\",\n        [\"🌘\"] = \"waning_crescent_moon\",\n        [\"🌑\"] = \"new_moon\",\n        [\"🌒\"] = \"waxing_crescent_moon\",\n        [\"🌓\"] = \"first_quarter_moon\",\n        [\"🌔\"] = \"waxing_gibbous_moon\",\n        [\"🌙\"] = \"crescent_moon\",\n        [\"🌎\"] = \"earth_americas\",\n        [\"🌍\"] = \"earth_africa\",\n        [\"🌏\"] = \"earth_asia\",\n        [\"🪐\"] = \"ringed_planet\",\n        [\"💫\"] = \"dizzy\",\n        [\"⭐\"] = \"star\",\n        [\"🌟\"] = \"star2\",\n        [\"✨\"] = \"sparkles\",\n        [\"⚡\"] = \"zap\",\n        [\"☄️\"] = \"comet\",\n        [\"💥\"] = \"boom\",\n        [\"🔥\"] = \"fire\",\n        [\"🌪️\"] = \"cloud_tornado\",\n        [\"🌈\"] = \"rainbow\",\n        [\"☀️\"] = \"sunny\",\n        [\"🌤️\"] = \"white_sun_small_cloud\",\n        [\"⛅\"] = \"partly_sunny\",\n        [\"🌥️\"] = \"white_sun_cloud\",\n        [\"☁️\"] = \"cloud\",\n        [\"🌦️\"] = \"white_sun_rain_cloud\",\n        [\"🌧️\"] = \"cloud_rain\",\n        [\"⛈️\"] = \"thunder_cloud_rain\",\n        [\"🌩️\"] = \"cloud_lightning\",\n        [\"🌨️\"] = \"cloud_snow\",\n        [\"❄️\"] = \"snowflake\",\n        [\"☃️\"] = \"snowman2\",\n        [\"⛄\"] = \"snowman\",\n        [\"🌬️\"] = \"wind_blowing_face\",\n        [\"💨\"] = \"dash\",\n        [\"💧\"] = \"droplet\",\n        [\"💦\"] = \"sweat_drops\",\n        [\"☔\"] = \"umbrella\",\n        [\"☂️\"] = \"umbrella2\",\n        [\"🌊\"] = \"ocean\",\n        [\"🌫️\"] = \"fog\",\n        [\"🍏\"] = \"green_apple\",\n        [\"🍎\"] = \"apple\",\n        [\"🍐\"] = \"pear\",\n        [\"🍊\"] = \"tangerine\",\n        [\"🍋\"] = \"lemon\",\n        [\"🍌\"] = \"banana\",\n        [\"🍉\"] = \"watermelon\",\n        [\"🍇\"] = \"grapes\",\n        [\"🫐\"] = \"blueberries\",\n        [\"🍓\"] = \"strawberry\",\n        [\"🍈\"] = \"melon\",\n        [\"🍒\"] = \"cherries\",\n        [\"🍑\"] = \"peach\",\n        [\"🥭\"] = \"mango\",\n        [\"🍍\"] = \"pineapple\",\n        [\"🥥\"] = \"coconut\",\n        [\"🥝\"] = \"kiwi\",\n        [\"🍅\"] = \"tomato\",\n        [\"🍆\"] = \"eggplant\",\n        [\"🥑\"] = \"avocado\",\n        [\"🫒\"] = \"olive\",\n        [\"🥦\"] = \"broccoli\",\n        [\"🥬\"] = \"leafy_green\",\n        [\"🫑\"] = \"bell_pepper\",\n        [\"🥒\"] = \"cucumber\",\n        [\"🌶️\"] = \"hot_pepper\",\n        [\"🌽\"] = \"corn\",\n        [\"🥕\"] = \"carrot\",\n        [\"🧄\"] = \"garlic\",\n        [\"🧅\"] = \"onion\",\n        [\"🥔\"] = \"potato\",\n        [\"🍠\"] = \"sweet_potato\",\n        [\"🥐\"] = \"croissant\",\n        [\"🥯\"] = \"bagel\",\n        [\"🍞\"] = \"bread\",\n        [\"🥖\"] = \"french_bread\",\n        [\"🫓\"] = \"flatbread\",\n        [\"🥨\"] = \"pretzel\",\n        [\"🧀\"] = \"cheese\",\n        [\"🥚\"] = \"egg\",\n        [\"🍳\"] = \"cooking\",\n        [\"🧈\"] = \"butter\",\n        [\"🥞\"] = \"pancakes\",\n        [\"🧇\"] = \"waffle\",\n        [\"🥓\"] = \"bacon\",\n        [\"🥩\"] = \"cut_of_meat\",\n        [\"🍗\"] = \"poultry_leg\",\n        [\"🍖\"] = \"meat_on_bone\",\n        [\"🌭\"] = \"hotdog\",\n        [\"🍔\"] = \"hamburger\",\n        [\"🍟\"] = \"fries\",\n        [\"🍕\"] = \"pizza\",\n        [\"🥪\"] = \"sandwich\",\n        [\"🥙\"] = \"stuffed_flatbread\",\n        [\"🧆\"] = \"falafel\",\n        [\"🌮\"] = \"taco\",\n        [\"🌯\"] = \"burrito\",\n        [\"🫔\"] = \"tamale\",\n        [\"🥗\"] = \"salad\",\n        [\"🥘\"] = \"shallow_pan_of_food\",\n        [\"🫕\"] = \"fondue\",\n        [\"🥫\"] = \"canned_food\",\n        [\"🍝\"] = \"spaghetti\",\n        [\"🍜\"] = \"ramen\",\n        [\"🍲\"] = \"stew\",\n        [\"🍛\"] = \"curry\",\n        [\"🍣\"] = \"sushi\",\n        [\"🍱\"] = \"bento\",\n        [\"🥟\"] = \"dumpling\",\n        [\"🦪\"] = \"oyster\",\n        [\"🍤\"] = \"fried_shrimp\",\n        [\"🍙\"] = \"rice_ball\",\n        [\"🍚\"] = \"rice\",\n        [\"🍘\"] = \"rice_cracker\",\n        [\"🍥\"] = \"fish_cake\",\n        [\"🥠\"] = \"fortune_cookie\",\n        [\"🥮\"] = \"moon_cake\",\n        [\"🍢\"] = \"oden\",\n        [\"🍡\"] = \"dango\",\n        [\"🍧\"] = \"shaved_ice\",\n        [\"🍨\"] = \"ice_cream\",\n        [\"🍦\"] = \"icecream\",\n        [\"🥧\"] = \"pie\",\n        [\"🧁\"] = \"cupcake\",\n        [\"🍰\"] = \"cake\",\n        [\"🎂\"] = \"birthday\",\n        [\"🍮\"] = \"custard\",\n        [\"🍭\"] = \"lollipop\",\n        [\"🍬\"] = \"candy\",\n        [\"🍫\"] = \"chocolate_bar\",\n        [\"🍿\"] = \"popcorn\",\n        [\"🍩\"] = \"doughnut\",\n        [\"🍪\"] = \"cookie\",\n        [\"🌰\"] = \"chestnut\",\n        [\"🥜\"] = \"peanuts\",\n        [\"🍯\"] = \"honey_pot\",\n        [\"🥛\"] = \"milk\",\n        [\"🍼\"] = \"baby_bottle\",\n        [\"☕\"] = \"coffee\",\n        [\"🍵\"] = \"tea\",\n        [\"🫖\"] = \"teapot\",\n        [\"🧉\"] = \"mate\",\n        [\"🧋\"] = \"bubble_tea\",\n        [\"🧃\"] = \"beverage_box\",\n        [\"🥤\"] = \"cup_with_straw\",\n        [\"🍶\"] = \"sake\",\n        [\"🍺\"] = \"beer\",\n        [\"🍻\"] = \"beers\",\n        [\"🥂\"] = \"champagne_glass\",\n        [\"🍷\"] = \"wine_glass\",\n        [\"🥃\"] = \"tumbler_glass\",\n        [\"🍸\"] = \"cocktail\",\n        [\"🍹\"] = \"tropical_drink\",\n        [\"🍾\"] = \"champagne\",\n        [\"🧊\"] = \"ice_cube\",\n        [\"🥄\"] = \"spoon\",\n        [\"🍴\"] = \"fork_and_knife\",\n        [\"🍽️\"] = \"fork_knife_plate\",\n        [\"🥣\"] = \"bowl_with_spoon\",\n        [\"🥡\"] = \"takeout_box\",\n        [\"🥢\"] = \"chopsticks\",\n        [\"🧂\"] = \"salt\",\n        [\"⚽\"] = \"soccer\",\n        [\"🏀\"] = \"basketball\",\n        [\"🏈\"] = \"football\",\n        [\"⚾\"] = \"baseball\",\n        [\"🥎\"] = \"softball\",\n        [\"🎾\"] = \"tennis\",\n        [\"🏐\"] = \"volleyball\",\n        [\"🏉\"] = \"rugby_football\",\n        [\"🥏\"] = \"flying_disc\",\n        [\"🪃\"] = \"boomerang\",\n        [\"🎱\"] = \"8ball\",\n        [\"🪀\"] = \"yo_yo\",\n        [\"🏓\"] = \"ping_pong\",\n        [\"🏸\"] = \"badminton\",\n        [\"🏒\"] = \"hockey\",\n        [\"🏑\"] = \"field_hockey\",\n        [\"🥍\"] = \"lacrosse\",\n        [\"🏏\"] = \"cricket_game\",\n        [\"🥅\"] = \"goal\",\n        [\"⛳\"] = \"golf\",\n        [\"🪁\"] = \"kite\",\n        [\"🏹\"] = \"bow_and_arrow\",\n        [\"🎣\"] = \"fishing_pole_and_fish\",\n        [\"🤿\"] = \"diving_mask\",\n        [\"🥊\"] = \"boxing_glove\",\n        [\"🥋\"] = \"martial_arts_uniform\",\n        [\"🎽\"] = \"running_shirt_with_sash\",\n        [\"🛹\"] = \"skateboard\",\n        [\"🛼\"] = \"roller_skate\",\n        [\"🛷\"] = \"sled\",\n        [\"⛸️\"] = \"ice_skate\",\n        [\"🥌\"] = \"curling_stone\",\n        [\"🎿\"] = \"ski\",\n        [\"⛷️\"] = \"skier\",\n        [\"🏂\"] = \"snowboarder\",\n        [\"🏂🏻\"] = \"snowboarder_tone1\",\n        [\"🏂🏼\"] = \"snowboarder_tone2\",\n        [\"🏂🏽\"] = \"snowboarder_tone3\",\n        [\"🏂🏾\"] = \"snowboarder_tone4\",\n        [\"🏂🏿\"] = \"snowboarder_tone5\",\n        [\"🪂\"] = \"parachute\",\n        [\"🏋️\"] = \"person_lifting_weights\",\n        [\"🏋🏻\"] = \"person_lifting_weights_tone1\",\n        [\"🏋🏼\"] = \"person_lifting_weights_tone2\",\n        [\"🏋🏽\"] = \"person_lifting_weights_tone3\",\n        [\"🏋🏾\"] = \"person_lifting_weights_tone4\",\n        [\"🏋🏿\"] = \"person_lifting_weights_tone5\",\n        [\"🏋️‍♀️\"] = \"woman_lifting_weights\",\n        [\"🏋🏻‍♀️\"] = \"woman_lifting_weights_tone1\",\n        [\"🏋🏼‍♀️\"] = \"woman_lifting_weights_tone2\",\n        [\"🏋🏽‍♀️\"] = \"woman_lifting_weights_tone3\",\n        [\"🏋🏾‍♀️\"] = \"woman_lifting_weights_tone4\",\n        [\"🏋🏿‍♀️\"] = \"woman_lifting_weights_tone5\",\n        [\"🏋️‍♂️\"] = \"man_lifting_weights\",\n        [\"🏋🏻‍♂️\"] = \"man_lifting_weights_tone1\",\n        [\"🏋🏼‍♂️\"] = \"man_lifting_weights_tone2\",\n        [\"🏋🏽‍♂️\"] = \"man_lifting_weights_tone3\",\n        [\"🏋🏾‍♂️\"] = \"man_lifting_weights_tone4\",\n        [\"🏋🏿‍♂️\"] = \"man_lifting_weights_tone5\",\n        [\"🤼\"] = \"people_wrestling\",\n        [\"🤼‍♀️\"] = \"women_wrestling\",\n        [\"🤼‍♂️\"] = \"men_wrestling\",\n        [\"🤸\"] = \"person_doing_cartwheel\",\n        [\"🤸🏻\"] = \"person_doing_cartwheel_tone1\",\n        [\"🤸🏼\"] = \"person_doing_cartwheel_tone2\",\n        [\"🤸🏽\"] = \"person_doing_cartwheel_tone3\",\n        [\"🤸🏾\"] = \"person_doing_cartwheel_tone4\",\n        [\"🤸🏿\"] = \"person_doing_cartwheel_tone5\",\n        [\"🤸‍♀️\"] = \"woman_cartwheeling\",\n        [\"🤸🏻‍♀️\"] = \"woman_cartwheeling_tone1\",\n        [\"🤸🏼‍♀️\"] = \"woman_cartwheeling_tone2\",\n        [\"🤸🏽‍♀️\"] = \"woman_cartwheeling_tone3\",\n        [\"🤸🏾‍♀️\"] = \"woman_cartwheeling_tone4\",\n        [\"🤸🏿‍♀️\"] = \"woman_cartwheeling_tone5\",\n        [\"🤸‍♂️\"] = \"man_cartwheeling\",\n        [\"🤸🏻‍♂️\"] = \"man_cartwheeling_tone1\",\n        [\"🤸🏼‍♂️\"] = \"man_cartwheeling_tone2\",\n        [\"🤸🏽‍♂️\"] = \"man_cartwheeling_tone3\",\n        [\"🤸🏾‍♂️\"] = \"man_cartwheeling_tone4\",\n        [\"🤸🏿‍♂️\"] = \"man_cartwheeling_tone5\",\n        [\"⛹️\"] = \"person_bouncing_ball\",\n        [\"⛹🏻\"] = \"person_bouncing_ball_tone1\",\n        [\"⛹🏼\"] = \"person_bouncing_ball_tone2\",\n        [\"⛹🏽\"] = \"person_bouncing_ball_tone3\",\n        [\"⛹🏾\"] = \"person_bouncing_ball_tone4\",\n        [\"⛹🏿\"] = \"person_bouncing_ball_tone5\",\n        [\"⛹️‍♀️\"] = \"woman_bouncing_ball\",\n        [\"⛹🏻‍♀️\"] = \"woman_bouncing_ball_tone1\",\n        [\"⛹🏼‍♀️\"] = \"woman_bouncing_ball_tone2\",\n        [\"⛹🏽‍♀️\"] = \"woman_bouncing_ball_tone3\",\n        [\"⛹🏾‍♀️\"] = \"woman_bouncing_ball_tone4\",\n        [\"⛹🏿‍♀️\"] = \"woman_bouncing_ball_tone5\",\n        [\"⛹️‍♂️\"] = \"man_bouncing_ball\",\n        [\"⛹🏻‍♂️\"] = \"man_bouncing_ball_tone1\",\n        [\"⛹🏼‍♂️\"] = \"man_bouncing_ball_tone2\",\n        [\"⛹🏽‍♂️\"] = \"man_bouncing_ball_tone3\",\n        [\"⛹🏾‍♂️\"] = \"man_bouncing_ball_tone4\",\n        [\"⛹🏿‍♂️\"] = \"man_bouncing_ball_tone5\",\n        [\"🤺\"] = \"person_fencing\",\n        [\"🤾\"] = \"person_playing_handball\",\n        [\"🤾🏻\"] = \"person_playing_handball_tone1\",\n        [\"🤾🏼\"] = \"person_playing_handball_tone2\",\n        [\"🤾🏽\"] = \"person_playing_handball_tone3\",\n        [\"🤾🏾\"] = \"person_playing_handball_tone4\",\n        [\"🤾🏿\"] = \"person_playing_handball_tone5\",\n        [\"🤾‍♀️\"] = \"woman_playing_handball\",\n        [\"🤾🏻‍♀️\"] = \"woman_playing_handball_tone1\",\n        [\"🤾🏼‍♀️\"] = \"woman_playing_handball_tone2\",\n        [\"🤾🏽‍♀️\"] = \"woman_playing_handball_tone3\",\n        [\"🤾🏾‍♀️\"] = \"woman_playing_handball_tone4\",\n        [\"🤾🏿‍♀️\"] = \"woman_playing_handball_tone5\",\n        [\"🤾‍♂️\"] = \"man_playing_handball\",\n        [\"🤾🏻‍♂️\"] = \"man_playing_handball_tone1\",\n        [\"🤾🏼‍♂️\"] = \"man_playing_handball_tone2\",\n        [\"🤾🏽‍♂️\"] = \"man_playing_handball_tone3\",\n        [\"🤾🏾‍♂️\"] = \"man_playing_handball_tone4\",\n        [\"🤾🏿‍♂️\"] = \"man_playing_handball_tone5\",\n        [\"🏌️\"] = \"person_golfing\",\n        [\"🏌🏻\"] = \"person_golfing_tone1\",\n        [\"🏌🏼\"] = \"person_golfing_tone2\",\n        [\"🏌🏽\"] = \"person_golfing_tone3\",\n        [\"🏌🏾\"] = \"person_golfing_tone4\",\n        [\"🏌🏿\"] = \"person_golfing_tone5\",\n        [\"🏌️‍♀️\"] = \"woman_golfing\",\n        [\"🏌🏻‍♀️\"] = \"woman_golfing_tone1\",\n        [\"🏌🏼‍♀️\"] = \"woman_golfing_tone2\",\n        [\"🏌🏽‍♀️\"] = \"woman_golfing_tone3\",\n        [\"🏌🏾‍♀️\"] = \"woman_golfing_tone4\",\n        [\"🏌🏿‍♀️\"] = \"woman_golfing_tone5\",\n        [\"🏌️‍♂️\"] = \"man_golfing\",\n        [\"🏌🏻‍♂️\"] = \"man_golfing_tone1\",\n        [\"🏌🏼‍♂️\"] = \"man_golfing_tone2\",\n        [\"🏌🏽‍♂️\"] = \"man_golfing_tone3\",\n        [\"🏌🏾‍♂️\"] = \"man_golfing_tone4\",\n        [\"🏌🏿‍♂️\"] = \"man_golfing_tone5\",\n        [\"🏇\"] = \"horse_racing\",\n        [\"🏇🏻\"] = \"horse_racing_tone1\",\n        [\"🏇🏼\"] = \"horse_racing_tone2\",\n        [\"🏇🏽\"] = \"horse_racing_tone3\",\n        [\"🏇🏾\"] = \"horse_racing_tone4\",\n        [\"🏇🏿\"] = \"horse_racing_tone5\",\n        [\"🧘\"] = \"person_in_lotus_position\",\n        [\"🧘🏻\"] = \"person_in_lotus_position_tone1\",\n        [\"🧘🏼\"] = \"person_in_lotus_position_tone2\",\n        [\"🧘🏽\"] = \"person_in_lotus_position_tone3\",\n        [\"🧘🏾\"] = \"person_in_lotus_position_tone4\",\n        [\"🧘🏿\"] = \"person_in_lotus_position_tone5\",\n        [\"🧘‍♀️\"] = \"woman_in_lotus_position\",\n        [\"🧘🏻‍♀️\"] = \"woman_in_lotus_position_tone1\",\n        [\"🧘🏼‍♀️\"] = \"woman_in_lotus_position_tone2\",\n        [\"🧘🏽‍♀️\"] = \"woman_in_lotus_position_tone3\",\n        [\"🧘🏾‍♀️\"] = \"woman_in_lotus_position_tone4\",\n        [\"🧘🏿‍♀️\"] = \"woman_in_lotus_position_tone5\",\n        [\"🧘‍♂️\"] = \"man_in_lotus_position\",\n        [\"🧘🏻‍♂️\"] = \"man_in_lotus_position_tone1\",\n        [\"🧘🏼‍♂️\"] = \"man_in_lotus_position_tone2\",\n        [\"🧘🏽‍♂️\"] = \"man_in_lotus_position_tone3\",\n        [\"🧘🏾‍♂️\"] = \"man_in_lotus_position_tone4\",\n        [\"🧘🏿‍♂️\"] = \"man_in_lotus_position_tone5\",\n        [\"🏄\"] = \"person_surfing\",\n        [\"🏄🏻\"] = \"person_surfing_tone1\",\n        [\"🏄🏼\"] = \"person_surfing_tone2\",\n        [\"🏄🏽\"] = \"person_surfing_tone3\",\n        [\"🏄🏾\"] = \"person_surfing_tone4\",\n        [\"🏄🏿\"] = \"person_surfing_tone5\",\n        [\"🏄‍♀️\"] = \"woman_surfing\",\n        [\"🏄🏻‍♀️\"] = \"woman_surfing_tone1\",\n        [\"🏄🏼‍♀️\"] = \"woman_surfing_tone2\",\n        [\"🏄🏽‍♀️\"] = \"woman_surfing_tone3\",\n        [\"🏄🏾‍♀️\"] = \"woman_surfing_tone4\",\n        [\"🏄🏿‍♀️\"] = \"woman_surfing_tone5\",\n        [\"🏄‍♂️\"] = \"man_surfing\",\n        [\"🏄🏻‍♂️\"] = \"man_surfing_tone1\",\n        [\"🏄🏼‍♂️\"] = \"man_surfing_tone2\",\n        [\"🏄🏽‍♂️\"] = \"man_surfing_tone3\",\n        [\"🏄🏾‍♂️\"] = \"man_surfing_tone4\",\n        [\"🏄🏿‍♂️\"] = \"man_surfing_tone5\",\n        [\"🏊\"] = \"person_swimming\",\n        [\"🏊🏻\"] = \"person_swimming_tone1\",\n        [\"🏊🏼\"] = \"person_swimming_tone2\",\n        [\"🏊🏽\"] = \"person_swimming_tone3\",\n        [\"🏊🏾\"] = \"person_swimming_tone4\",\n        [\"🏊🏿\"] = \"person_swimming_tone5\",\n        [\"🏊‍♀️\"] = \"woman_swimming\",\n        [\"🏊🏻‍♀️\"] = \"woman_swimming_tone1\",\n        [\"🏊🏼‍♀️\"] = \"woman_swimming_tone2\",\n        [\"🏊🏽‍♀️\"] = \"woman_swimming_tone3\",\n        [\"🏊🏾‍♀️\"] = \"woman_swimming_tone4\",\n        [\"🏊🏿‍♀️\"] = \"woman_swimming_tone5\",\n        [\"🏊‍♂️\"] = \"man_swimming\",\n        [\"🏊🏻‍♂️\"] = \"man_swimming_tone1\",\n        [\"🏊🏼‍♂️\"] = \"man_swimming_tone2\",\n        [\"🏊🏽‍♂️\"] = \"man_swimming_tone3\",\n        [\"🏊🏾‍♂️\"] = \"man_swimming_tone4\",\n        [\"🏊🏿‍♂️\"] = \"man_swimming_tone5\",\n        [\"🤽\"] = \"person_playing_water_polo\",\n        [\"🤽🏻\"] = \"person_playing_water_polo_tone1\",\n        [\"🤽🏼\"] = \"person_playing_water_polo_tone2\",\n        [\"🤽🏽\"] = \"person_playing_water_polo_tone3\",\n        [\"🤽🏾\"] = \"person_playing_water_polo_tone4\",\n        [\"🤽🏿\"] = \"person_playing_water_polo_tone5\",\n        [\"🤽‍♀️\"] = \"woman_playing_water_polo\",\n        [\"🤽🏻‍♀️\"] = \"woman_playing_water_polo_tone1\",\n        [\"🤽🏼‍♀️\"] = \"woman_playing_water_polo_tone2\",\n        [\"🤽🏽‍♀️\"] = \"woman_playing_water_polo_tone3\",\n        [\"🤽🏾‍♀️\"] = \"woman_playing_water_polo_tone4\",\n        [\"🤽🏿‍♀️\"] = \"woman_playing_water_polo_tone5\",\n        [\"🤽‍♂️\"] = \"man_playing_water_polo\",\n        [\"🤽🏻‍♂️\"] = \"man_playing_water_polo_tone1\",\n        [\"🤽🏼‍♂️\"] = \"man_playing_water_polo_tone2\",\n        [\"🤽🏽‍♂️\"] = \"man_playing_water_polo_tone3\",\n        [\"🤽🏾‍♂️\"] = \"man_playing_water_polo_tone4\",\n        [\"🤽🏿‍♂️\"] = \"man_playing_water_polo_tone5\",\n        [\"🚣\"] = \"person_rowing_boat\",\n        [\"🚣🏻\"] = \"person_rowing_boat_tone1\",\n        [\"🚣🏼\"] = \"person_rowing_boat_tone2\",\n        [\"🚣🏽\"] = \"person_rowing_boat_tone3\",\n        [\"🚣🏾\"] = \"person_rowing_boat_tone4\",\n        [\"🚣🏿\"] = \"person_rowing_boat_tone5\",\n        [\"🚣‍♀️\"] = \"woman_rowing_boat\",\n        [\"🚣🏻‍♀️\"] = \"woman_rowing_boat_tone1\",\n        [\"🚣🏼‍♀️\"] = \"woman_rowing_boat_tone2\",\n        [\"🚣🏽‍♀️\"] = \"woman_rowing_boat_tone3\",\n        [\"🚣🏾‍♀️\"] = \"woman_rowing_boat_tone4\",\n        [\"🚣🏿‍♀️\"] = \"woman_rowing_boat_tone5\",\n        [\"🚣‍♂️\"] = \"man_rowing_boat\",\n        [\"🚣🏻‍♂️\"] = \"man_rowing_boat_tone1\",\n        [\"🚣🏼‍♂️\"] = \"man_rowing_boat_tone2\",\n        [\"🚣🏽‍♂️\"] = \"man_rowing_boat_tone3\",\n        [\"🚣🏾‍♂️\"] = \"man_rowing_boat_tone4\",\n        [\"🚣🏿‍♂️\"] = \"man_rowing_boat_tone5\",\n        [\"🧗\"] = \"person_climbing\",\n        [\"🧗🏻\"] = \"person_climbing_tone1\",\n        [\"🧗🏼\"] = \"person_climbing_tone2\",\n        [\"🧗🏽\"] = \"person_climbing_tone3\",\n        [\"🧗🏾\"] = \"person_climbing_tone4\",\n        [\"🧗🏿\"] = \"person_climbing_tone5\",\n        [\"🧗‍♀️\"] = \"woman_climbing\",\n        [\"🧗🏻‍♀️\"] = \"woman_climbing_tone1\",\n        [\"🧗🏼‍♀️\"] = \"woman_climbing_tone2\",\n        [\"🧗🏽‍♀️\"] = \"woman_climbing_tone3\",\n        [\"🧗🏾‍♀️\"] = \"woman_climbing_tone4\",\n        [\"🧗🏿‍♀️\"] = \"woman_climbing_tone5\",\n        [\"🧗‍♂️\"] = \"man_climbing\",\n        [\"🧗🏻‍♂️\"] = \"man_climbing_tone1\",\n        [\"🧗🏼‍♂️\"] = \"man_climbing_tone2\",\n        [\"🧗🏽‍♂️\"] = \"man_climbing_tone3\",\n        [\"🧗🏾‍♂️\"] = \"man_climbing_tone4\",\n        [\"🧗🏿‍♂️\"] = \"man_climbing_tone5\",\n        [\"🚵\"] = \"person_mountain_biking\",\n        [\"🚵🏻\"] = \"person_mountain_biking_tone1\",\n        [\"🚵🏼\"] = \"person_mountain_biking_tone2\",\n        [\"🚵🏽\"] = \"person_mountain_biking_tone3\",\n        [\"🚵🏾\"] = \"person_mountain_biking_tone4\",\n        [\"🚵🏿\"] = \"person_mountain_biking_tone5\",\n        [\"🚵‍♀️\"] = \"woman_mountain_biking\",\n        [\"🚵🏻‍♀️\"] = \"woman_mountain_biking_tone1\",\n        [\"🚵🏼‍♀️\"] = \"woman_mountain_biking_tone2\",\n        [\"🚵🏽‍♀️\"] = \"woman_mountain_biking_tone3\",\n        [\"🚵🏾‍♀️\"] = \"woman_mountain_biking_tone4\",\n        [\"🚵🏿‍♀️\"] = \"woman_mountain_biking_tone5\",\n        [\"🚵‍♂️\"] = \"man_mountain_biking\",\n        [\"🚵🏻‍♂️\"] = \"man_mountain_biking_tone1\",\n        [\"🚵🏼‍♂️\"] = \"man_mountain_biking_tone2\",\n        [\"🚵🏽‍♂️\"] = \"man_mountain_biking_tone3\",\n        [\"🚵🏾‍♂️\"] = \"man_mountain_biking_tone4\",\n        [\"🚵🏿‍♂️\"] = \"man_mountain_biking_tone5\",\n        [\"🚴\"] = \"person_biking\",\n        [\"🚴🏻\"] = \"person_biking_tone1\",\n        [\"🚴🏼\"] = \"person_biking_tone2\",\n        [\"🚴🏽\"] = \"person_biking_tone3\",\n        [\"🚴🏾\"] = \"person_biking_tone4\",\n        [\"🚴🏿\"] = \"person_biking_tone5\",\n        [\"🚴‍♀️\"] = \"woman_biking\",\n        [\"🚴🏻‍♀️\"] = \"woman_biking_tone1\",\n        [\"🚴🏼‍♀️\"] = \"woman_biking_tone2\",\n        [\"🚴🏽‍♀️\"] = \"woman_biking_tone3\",\n        [\"🚴🏾‍♀️\"] = \"woman_biking_tone4\",\n        [\"🚴🏿‍♀️\"] = \"woman_biking_tone5\",\n        [\"🚴‍♂️\"] = \"man_biking\",\n        [\"🚴🏻‍♂️\"] = \"man_biking_tone1\",\n        [\"🚴🏼‍♂️\"] = \"man_biking_tone2\",\n        [\"🚴🏽‍♂️\"] = \"man_biking_tone3\",\n        [\"🚴🏾‍♂️\"] = \"man_biking_tone4\",\n        [\"🚴🏿‍♂️\"] = \"man_biking_tone5\",\n        [\"🏆\"] = \"trophy\",\n        [\"🥇\"] = \"first_place\",\n        [\"🥈\"] = \"second_place\",\n        [\"🥉\"] = \"third_place\",\n        [\"🏅\"] = \"medal\",\n        [\"🎖️\"] = \"military_medal\",\n        [\"🏵️\"] = \"rosette\",\n        [\"🎗️\"] = \"reminder_ribbon\",\n        [\"🎫\"] = \"ticket\",\n        [\"🎟️\"] = \"tickets\",\n        [\"🎪\"] = \"circus_tent\",\n        [\"🤹\"] = \"person_juggling\",\n        [\"🤹🏻\"] = \"person_juggling_tone1\",\n        [\"🤹🏼\"] = \"person_juggling_tone2\",\n        [\"🤹🏽\"] = \"person_juggling_tone3\",\n        [\"🤹🏾\"] = \"person_juggling_tone4\",\n        [\"🤹🏿\"] = \"person_juggling_tone5\",\n        [\"🤹‍♀️\"] = \"woman_juggling\",\n        [\"🤹🏻‍♀️\"] = \"woman_juggling_tone1\",\n        [\"🤹🏼‍♀️\"] = \"woman_juggling_tone2\",\n        [\"🤹🏽‍♀️\"] = \"woman_juggling_tone3\",\n        [\"🤹🏾‍♀️\"] = \"woman_juggling_tone4\",\n        [\"🤹🏿‍♀️\"] = \"woman_juggling_tone5\",\n        [\"🤹‍♂️\"] = \"man_juggling\",\n        [\"🤹🏻‍♂️\"] = \"man_juggling_tone1\",\n        [\"🤹🏼‍♂️\"] = \"man_juggling_tone2\",\n        [\"🤹🏽‍♂️\"] = \"man_juggling_tone3\",\n        [\"🤹🏾‍♂️\"] = \"man_juggling_tone4\",\n        [\"🤹🏿‍♂️\"] = \"man_juggling_tone5\",\n        [\"🎭\"] = \"performing_arts\",\n        [\"🩰\"] = \"ballet_shoes\",\n        [\"🎨\"] = \"art\",\n        [\"🎬\"] = \"clapper\",\n        [\"🎤\"] = \"microphone\",\n        [\"🎧\"] = \"headphones\",\n        [\"🎼\"] = \"musical_score\",\n        [\"🎹\"] = \"musical_keyboard\",\n        [\"🥁\"] = \"drum\",\n        [\"🪘\"] = \"long_drum\",\n        [\"🎷\"] = \"saxophone\",\n        [\"🎺\"] = \"trumpet\",\n        [\"🎸\"] = \"guitar\",\n        [\"🪕\"] = \"banjo\",\n        [\"🎻\"] = \"violin\",\n        [\"🪗\"] = \"accordion\",\n        [\"🎲\"] = \"game_die\",\n        [\"♟️\"] = \"chess_pawn\",\n        [\"🎯\"] = \"dart\",\n        [\"🎳\"] = \"bowling\",\n        [\"🎮\"] = \"video_game\",\n        [\"🎰\"] = \"slot_machine\",\n        [\"🧩\"] = \"jigsaw\",\n        [\"🚗\"] = \"red_car\",\n        [\"🚕\"] = \"taxi\",\n        [\"🚙\"] = \"blue_car\",\n        [\"🛻\"] = \"pickup_truck\",\n        [\"🚌\"] = \"bus\",\n        [\"🚎\"] = \"trolleybus\",\n        [\"🏎️\"] = \"race_car\",\n        [\"🚓\"] = \"police_car\",\n        [\"🚑\"] = \"ambulance\",\n        [\"🚒\"] = \"fire_engine\",\n        [\"🚐\"] = \"minibus\",\n        [\"🚚\"] = \"truck\",\n        [\"🚛\"] = \"articulated_lorry\",\n        [\"🚜\"] = \"tractor\",\n        [\"🦯\"] = \"probing_cane\",\n        [\"🦽\"] = \"manual_wheelchair\",\n        [\"🦼\"] = \"motorized_wheelchair\",\n        [\"🛴\"] = \"scooter\",\n        [\"🚲\"] = \"bike\",\n        [\"🛵\"] = \"motor_scooter\",\n        [\"🏍️\"] = \"motorcycle\",\n        [\"🛺\"] = \"auto_rickshaw\",\n        [\"🚨\"] = \"rotating_light\",\n        [\"🚔\"] = \"oncoming_police_car\",\n        [\"🚍\"] = \"oncoming_bus\",\n        [\"🚘\"] = \"oncoming_automobile\",\n        [\"🚖\"] = \"oncoming_taxi\",\n        [\"🚡\"] = \"aerial_tramway\",\n        [\"🚠\"] = \"mountain_cableway\",\n        [\"🚟\"] = \"suspension_railway\",\n        [\"🚃\"] = \"railway_car\",\n        [\"🚋\"] = \"train\",\n        [\"🚞\"] = \"mountain_railway\",\n        [\"🚝\"] = \"monorail\",\n        [\"🚄\"] = \"bullettrain_side\",\n        [\"🚅\"] = \"bullettrain_front\",\n        [\"🚈\"] = \"light_rail\",\n        [\"🚂\"] = \"steam_locomotive\",\n        [\"🚆\"] = \"train2\",\n        [\"🚇\"] = \"metro\",\n        [\"🚊\"] = \"tram\",\n        [\"🚉\"] = \"station\",\n        [\"✈️\"] = \"airplane\",\n        [\"🛫\"] = \"airplane_departure\",\n        [\"🛬\"] = \"airplane_arriving\",\n        [\"🛩️\"] = \"airplane_small\",\n        [\"💺\"] = \"seat\",\n        [\"🛰️\"] = \"satellite_orbital\",\n        [\"🚀\"] = \"rocket\",\n        [\"🛸\"] = \"flying_saucer\",\n        [\"🚁\"] = \"helicopter\",\n        [\"🛶\"] = \"canoe\",\n        [\"⛵\"] = \"sailboat\",\n        [\"🚤\"] = \"speedboat\",\n        [\"🛥️\"] = \"motorboat\",\n        [\"🛳️\"] = \"cruise_ship\",\n        [\"⛴️\"] = \"ferry\",\n        [\"🚢\"] = \"ship\",\n        [\"⚓\"] = \"anchor\",\n        [\"⛽\"] = \"fuelpump\",\n        [\"🚧\"] = \"construction\",\n        [\"🚦\"] = \"vertical_traffic_light\",\n        [\"🚥\"] = \"traffic_light\",\n        [\"🚏\"] = \"busstop\",\n        [\"🗺️\"] = \"map\",\n        [\"🗿\"] = \"moyai\",\n        [\"🗽\"] = \"statue_of_liberty\",\n        [\"🗼\"] = \"tokyo_tower\",\n        [\"🏰\"] = \"european_castle\",\n        [\"🏯\"] = \"japanese_castle\",\n        [\"🏟️\"] = \"stadium\",\n        [\"🎡\"] = \"ferris_wheel\",\n        [\"🎢\"] = \"roller_coaster\",\n        [\"🎠\"] = \"carousel_horse\",\n        [\"⛲\"] = \"fountain\",\n        [\"⛱️\"] = \"beach_umbrella\",\n        [\"🏖️\"] = \"beach\",\n        [\"🏝️\"] = \"island\",\n        [\"🏜️\"] = \"desert\",\n        [\"🌋\"] = \"volcano\",\n        [\"⛰️\"] = \"mountain\",\n        [\"🏔️\"] = \"mountain_snow\",\n        [\"🗻\"] = \"mount_fuji\",\n        [\"🏕️\"] = \"camping\",\n        [\"⛺\"] = \"tent\",\n        [\"🏠\"] = \"house\",\n        [\"🏡\"] = \"house_with_garden\",\n        [\"🏘️\"] = \"homes\",\n        [\"🏚️\"] = \"house_abandoned\",\n        [\"🛖\"] = \"hut\",\n        [\"🏗️\"] = \"construction_site\",\n        [\"🏭\"] = \"factory\",\n        [\"🏢\"] = \"office\",\n        [\"🏬\"] = \"department_store\",\n        [\"🏣\"] = \"post_office\",\n        [\"🏤\"] = \"european_post_office\",\n        [\"🏥\"] = \"hospital\",\n        [\"🏦\"] = \"bank\",\n        [\"🏨\"] = \"hotel\",\n        [\"🏪\"] = \"convenience_store\",\n        [\"🏫\"] = \"school\",\n        [\"🏩\"] = \"love_hotel\",\n        [\"💒\"] = \"wedding\",\n        [\"🏛️\"] = \"classical_building\",\n        [\"⛪\"] = \"church\",\n        [\"🕌\"] = \"mosque\",\n        [\"🕍\"] = \"synagogue\",\n        [\"🛕\"] = \"hindu_temple\",\n        [\"🕋\"] = \"kaaba\",\n        [\"⛩️\"] = \"shinto_shrine\",\n        [\"🛤️\"] = \"railway_track\",\n        [\"🛣️\"] = \"motorway\",\n        [\"🗾\"] = \"japan\",\n        [\"🎑\"] = \"rice_scene\",\n        [\"🏞️\"] = \"park\",\n        [\"🌅\"] = \"sunrise\",\n        [\"🌄\"] = \"sunrise_over_mountains\",\n        [\"🌠\"] = \"stars\",\n        [\"🎇\"] = \"sparkler\",\n        [\"🎆\"] = \"fireworks\",\n        [\"🌇\"] = \"city_sunset\",\n        [\"🌆\"] = \"city_dusk\",\n        [\"🏙️\"] = \"cityscape\",\n        [\"🌃\"] = \"night_with_stars\",\n        [\"🌌\"] = \"milky_way\",\n        [\"🌉\"] = \"bridge_at_night\",\n        [\"🌁\"] = \"foggy\",\n        [\"⌚\"] = \"watch\",\n        [\"📱\"] = \"mobile_phone\",\n        [\"📲\"] = \"calling\",\n        [\"💻\"] = \"computer\",\n        [\"⌨️\"] = \"keyboard\",\n        [\"🖥️\"] = \"desktop\",\n        [\"🖨️\"] = \"printer\",\n        [\"🖱️\"] = \"mouse_three_button\",\n        [\"🖲️\"] = \"trackball\",\n        [\"🕹️\"] = \"joystick\",\n        [\"🗜️\"] = \"compression\",\n        [\"💽\"] = \"minidisc\",\n        [\"💾\"] = \"floppy_disk\",\n        [\"💿\"] = \"cd\",\n        [\"📀\"] = \"dvd\",\n        [\"📼\"] = \"vhs\",\n        [\"📷\"] = \"camera\",\n        [\"📸\"] = \"camera_with_flash\",\n        [\"📹\"] = \"video_camera\",\n        [\"🎥\"] = \"movie_camera\",\n        [\"📽️\"] = \"projector\",\n        [\"🎞️\"] = \"film_frames\",\n        [\"📞\"] = \"telephone_receiver\",\n        [\"☎️\"] = \"telephone\",\n        [\"📟\"] = \"pager\",\n        [\"📠\"] = \"fax\",\n        [\"📺\"] = \"tv\",\n        [\"📻\"] = \"radio\",\n        [\"🎙️\"] = \"microphone2\",\n        [\"🎚️\"] = \"level_slider\",\n        [\"🎛️\"] = \"control_knobs\",\n        [\"🧭\"] = \"compass\",\n        [\"⏱️\"] = \"stopwatch\",\n        [\"⏲️\"] = \"timer\",\n        [\"⏰\"] = \"alarm_clock\",\n        [\"🕰️\"] = \"clock\",\n        [\"⌛\"] = \"hourglass\",\n        [\"⏳\"] = \"hourglass_flowing_sand\",\n        [\"📡\"] = \"satellite\",\n        [\"🔋\"] = \"battery\",\n        [\"🔌\"] = \"electric_plug\",\n        [\"💡\"] = \"bulb\",\n        [\"🔦\"] = \"flashlight\",\n        [\"🕯️\"] = \"candle\",\n        [\"🪔\"] = \"diya_lamp\",\n        [\"🧯\"] = \"fire_extinguisher\",\n        [\"🛢️\"] = \"oil\",\n        [\"💸\"] = \"money_with_wings\",\n        [\"💵\"] = \"dollar\",\n        [\"💴\"] = \"yen\",\n        [\"💶\"] = \"euro\",\n        [\"💷\"] = \"pound\",\n        [\"🪙\"] = \"coin\",\n        [\"💰\"] = \"moneybag\",\n        [\"💳\"] = \"credit_card\",\n        [\"💎\"] = \"gem\",\n        [\"⚖️\"] = \"scales\",\n        [\"🪜\"] = \"ladder\",\n        [\"🧰\"] = \"toolbox\",\n        [\"🪛\"] = \"screwdriver\",\n        [\"🔧\"] = \"wrench\",\n        [\"🔨\"] = \"hammer\",\n        [\"⚒️\"] = \"hammer_pick\",\n        [\"🛠️\"] = \"tools\",\n        [\"⛏️\"] = \"pick\",\n        [\"🔩\"] = \"nut_and_bolt\",\n        [\"⚙️\"] = \"gear\",\n        [\"🧱\"] = \"bricks\",\n        [\"⛓️\"] = \"chains\",\n        [\"🪝\"] = \"hook\",\n        [\"🪢\"] = \"knot\",\n        [\"🧲\"] = \"magnet\",\n        [\"🔫\"] = \"gun\",\n        [\"💣\"] = \"bomb\",\n        [\"🧨\"] = \"firecracker\",\n        [\"🪓\"] = \"axe\",\n        [\"🪚\"] = \"carpentry_saw\",\n        [\"🔪\"] = \"knife\",\n        [\"🗡️\"] = \"dagger\",\n        [\"⚔️\"] = \"crossed_swords\",\n        [\"🛡️\"] = \"shield\",\n        [\"🚬\"] = \"smoking\",\n        [\"⚰️\"] = \"coffin\",\n        [\"🪦\"] = \"headstone\",\n        [\"⚱️\"] = \"urn\",\n        [\"🏺\"] = \"amphora\",\n        [\"🪄\"] = \"magic_wand\",\n        [\"🔮\"] = \"crystal_ball\",\n        [\"📿\"] = \"prayer_beads\",\n        [\"🧿\"] = \"nazar_amulet\",\n        [\"💈\"] = \"barber\",\n        [\"⚗️\"] = \"alembic\",\n        [\"🔭\"] = \"telescope\",\n        [\"🔬\"] = \"microscope\",\n        [\"🕳️\"] = \"hole\",\n        [\"🪟\"] = \"window\",\n        [\"🩹\"] = \"adhesive_bandage\",\n        [\"🩺\"] = \"stethoscope\",\n        [\"💊\"] = \"pill\",\n        [\"💉\"] = \"syringe\",\n        [\"🩸\"] = \"drop_of_blood\",\n        [\"🧬\"] = \"dna\",\n        [\"🦠\"] = \"microbe\",\n        [\"🧫\"] = \"petri_dish\",\n        [\"🧪\"] = \"test_tube\",\n        [\"🌡️\"] = \"thermometer\",\n        [\"🪤\"] = \"mouse_trap\",\n        [\"🧹\"] = \"broom\",\n        [\"🧺\"] = \"basket\",\n        [\"🪡\"] = \"sewing_needle\",\n        [\"🧻\"] = \"roll_of_paper\",\n        [\"🚽\"] = \"toilet\",\n        [\"🪠\"] = \"plunger\",\n        [\"🪣\"] = \"bucket\",\n        [\"🚰\"] = \"potable_water\",\n        [\"🚿\"] = \"shower\",\n        [\"🛁\"] = \"bathtub\",\n        [\"🛀\"] = \"bath\",\n        [\"🛀🏻\"] = \"bath_tone1\",\n        [\"🛀🏼\"] = \"bath_tone2\",\n        [\"🛀🏽\"] = \"bath_tone3\",\n        [\"🛀🏾\"] = \"bath_tone4\",\n        [\"🛀🏿\"] = \"bath_tone5\",\n        [\"🪥\"] = \"toothbrush\",\n        [\"🧼\"] = \"soap\",\n        [\"🪒\"] = \"razor\",\n        [\"🧽\"] = \"sponge\",\n        [\"🧴\"] = \"squeeze_bottle\",\n        [\"🛎️\"] = \"bellhop\",\n        [\"🔑\"] = \"key\",\n        [\"🗝️\"] = \"key2\",\n        [\"🚪\"] = \"door\",\n        [\"🪑\"] = \"chair\",\n        [\"🪞\"] = \"mirror\",\n        [\"🛋️\"] = \"couch\",\n        [\"🛏️\"] = \"bed\",\n        [\"🛌\"] = \"sleeping_accommodation\",\n        [\"🛌🏻\"] = \"person_in_bed_tone1\",\n        [\"🛌🏼\"] = \"person_in_bed_tone2\",\n        [\"🛌🏽\"] = \"person_in_bed_tone3\",\n        [\"🛌🏾\"] = \"person_in_bed_tone4\",\n        [\"🛌🏿\"] = \"person_in_bed_tone5\",\n        [\"🧸\"] = \"teddy_bear\",\n        [\"🖼️\"] = \"frame_photo\",\n        [\"🛍️\"] = \"shopping_bags\",\n        [\"🛒\"] = \"shopping_cart\",\n        [\"🎁\"] = \"gift\",\n        [\"🎈\"] = \"balloon\",\n        [\"🎏\"] = \"flags\",\n        [\"🎀\"] = \"ribbon\",\n        [\"🎊\"] = \"confetti_ball\",\n        [\"🎉\"] = \"tada\",\n        [\"🪅\"] = \"piñata\",\n        [\"🪆\"] = \"nesting_dolls\",\n        [\"🎎\"] = \"dolls\",\n        [\"🏮\"] = \"izakaya_lantern\",\n        [\"🎐\"] = \"wind_chime\",\n        [\"🧧\"] = \"red_envelope\",\n        [\"✉️\"] = \"envelope\",\n        [\"📩\"] = \"envelope_with_arrow\",\n        [\"📨\"] = \"incoming_envelope\",\n        [\"📧\"] = \"e_mail\",\n        [\"💌\"] = \"love_letter\",\n        [\"📥\"] = \"inbox_tray\",\n        [\"📤\"] = \"outbox_tray\",\n        [\"📦\"] = \"package\",\n        [\"🏷️\"] = \"label\",\n        [\"📪\"] = \"mailbox_closed\",\n        [\"📫\"] = \"mailbox\",\n        [\"📬\"] = \"mailbox_with_mail\",\n        [\"📭\"] = \"mailbox_with_no_mail\",\n        [\"📮\"] = \"postbox\",\n        [\"📯\"] = \"postal_horn\",\n        [\"🪧\"] = \"placard\",\n        [\"📜\"] = \"scroll\",\n        [\"📃\"] = \"page_with_curl\",\n        [\"📄\"] = \"page_facing_up\",\n        [\"📑\"] = \"bookmark_tabs\",\n        [\"🧾\"] = \"receipt\",\n        [\"📊\"] = \"bar_chart\",\n        [\"📈\"] = \"chart_with_upwards_trend\",\n        [\"📉\"] = \"chart_with_downwards_trend\",\n        [\"🗒️\"] = \"notepad_spiral\",\n        [\"🗓️\"] = \"calendar_spiral\",\n        [\"📆\"] = \"calendar\",\n        [\"📅\"] = \"date\",\n        [\"🗑️\"] = \"wastebasket\",\n        [\"📇\"] = \"card_index\",\n        [\"🗃️\"] = \"card_box\",\n        [\"🗳️\"] = \"ballot_box\",\n        [\"🗄️\"] = \"file_cabinet\",\n        [\"📋\"] = \"clipboard\",\n        [\"📁\"] = \"file_folder\",\n        [\"📂\"] = \"open_file_folder\",\n        [\"🗂️\"] = \"dividers\",\n        [\"🗞️\"] = \"newspaper2\",\n        [\"📰\"] = \"newspaper\",\n        [\"📓\"] = \"notebook\",\n        [\"📔\"] = \"notebook_with_decorative_cover\",\n        [\"📒\"] = \"ledger\",\n        [\"📕\"] = \"closed_book\",\n        [\"📗\"] = \"green_book\",\n        [\"📘\"] = \"blue_book\",\n        [\"📙\"] = \"orange_book\",\n        [\"📚\"] = \"books\",\n        [\"📖\"] = \"book\",\n        [\"🔖\"] = \"bookmark\",\n        [\"🧷\"] = \"safety_pin\",\n        [\"🔗\"] = \"link\",\n        [\"📎\"] = \"paperclip\",\n        [\"🖇️\"] = \"paperclips\",\n        [\"📐\"] = \"triangular_ruler\",\n        [\"📏\"] = \"straight_ruler\",\n        [\"🧮\"] = \"abacus\",\n        [\"📌\"] = \"pushpin\",\n        [\"📍\"] = \"round_pushpin\",\n        [\"✂️\"] = \"scissors\",\n        [\"🖊️\"] = \"pen_ballpoint\",\n        [\"🖋️\"] = \"pen_fountain\",\n        [\"✒️\"] = \"black_nib\",\n        [\"🖌️\"] = \"paintbrush\",\n        [\"🖍️\"] = \"crayon\",\n        [\"📝\"] = \"pencil\",\n        [\"✏️\"] = \"pencil2\",\n        [\"🔍\"] = \"mag\",\n        [\"🔎\"] = \"mag_right\",\n        [\"🔏\"] = \"lock_with_ink_pen\",\n        [\"🔐\"] = \"closed_lock_with_key\",\n        [\"🔒\"] = \"lock\",\n        [\"🔓\"] = \"unlock\",\n        [\"❤️\"] = \"heart\",\n        [\"🧡\"] = \"orange_heart\",\n        [\"💛\"] = \"yellow_heart\",\n        [\"💚\"] = \"green_heart\",\n        [\"💙\"] = \"blue_heart\",\n        [\"💜\"] = \"purple_heart\",\n        [\"🖤\"] = \"black_heart\",\n        [\"🤎\"] = \"brown_heart\",\n        [\"🤍\"] = \"white_heart\",\n        [\"💔\"] = \"broken_heart\",\n        [\"❣️\"] = \"heart_exclamation\",\n        [\"💕\"] = \"two_hearts\",\n        [\"💞\"] = \"revolving_hearts\",\n        [\"💓\"] = \"heartbeat\",\n        [\"💗\"] = \"heartpulse\",\n        [\"💖\"] = \"sparkling_heart\",\n        [\"💘\"] = \"cupid\",\n        [\"💝\"] = \"gift_heart\",\n        [\"❤️‍🩹\"] = \"mending_heart\",\n        [\"❤️‍🔥\"] = \"heart_on_fire\",\n        [\"💟\"] = \"heart_decoration\",\n        [\"☮️\"] = \"peace\",\n        [\"✝️\"] = \"cross\",\n        [\"☪️\"] = \"star_and_crescent\",\n        [\"🕉️\"] = \"om_symbol\",\n        [\"☸️\"] = \"wheel_of_dharma\",\n        [\"✡️\"] = \"star_of_david\",\n        [\"🔯\"] = \"six_pointed_star\",\n        [\"🕎\"] = \"menorah\",\n        [\"☯️\"] = \"yin_yang\",\n        [\"☦️\"] = \"orthodox_cross\",\n        [\"🛐\"] = \"place_of_worship\",\n        [\"⛎\"] = \"ophiuchus\",\n        [\"♈\"] = \"aries\",\n        [\"♉\"] = \"taurus\",\n        [\"♊\"] = \"gemini\",\n        [\"♋\"] = \"cancer\",\n        [\"♌\"] = \"leo\",\n        [\"♍\"] = \"virgo\",\n        [\"♎\"] = \"libra\",\n        [\"♏\"] = \"scorpius\",\n        [\"♐\"] = \"sagittarius\",\n        [\"♑\"] = \"capricorn\",\n        [\"♒\"] = \"aquarius\",\n        [\"♓\"] = \"pisces\",\n        [\"🆔\"] = \"id\",\n        [\"⚛️\"] = \"atom\",\n        [\"🉑\"] = \"accept\",\n        [\"☢️\"] = \"radioactive\",\n        [\"☣️\"] = \"biohazard\",\n        [\"📴\"] = \"mobile_phone_off\",\n        [\"📳\"] = \"vibration_mode\",\n        [\"🈶\"] = \"u6709\",\n        [\"🈚\"] = \"u7121\",\n        [\"🈸\"] = \"u7533\",\n        [\"🈺\"] = \"u55b6\",\n        [\"🈷️\"] = \"u6708\",\n        [\"✴️\"] = \"eight_pointed_black_star\",\n        [\"🆚\"] = \"vs\",\n        [\"💮\"] = \"white_flower\",\n        [\"🉐\"] = \"ideograph_advantage\",\n        [\"㊙️\"] = \"secret\",\n        [\"㊗️\"] = \"congratulations\",\n        [\"🈴\"] = \"u5408\",\n        [\"🈵\"] = \"u6e80\",\n        [\"🈹\"] = \"u5272\",\n        [\"🈲\"] = \"u7981\",\n        [\"🅰️\"] = \"a\",\n        [\"🅱️\"] = \"b\",\n        [\"🆎\"] = \"ab\",\n        [\"🆑\"] = \"cl\",\n        [\"🅾️\"] = \"o2\",\n        [\"🆘\"] = \"sos\",\n        [\"❌\"] = \"x\",\n        [\"⭕\"] = \"o\",\n        [\"🛑\"] = \"octagonal_sign\",\n        [\"⛔\"] = \"no_entry\",\n        [\"📛\"] = \"name_badge\",\n        [\"🚫\"] = \"no_entry_sign\",\n        [\"💯\"] = \"100\",\n        [\"💢\"] = \"anger\",\n        [\"♨️\"] = \"hotsprings\",\n        [\"🚷\"] = \"no_pedestrians\",\n        [\"🚯\"] = \"do_not_litter\",\n        [\"🚳\"] = \"no_bicycles\",\n        [\"🚱\"] = \"non_potable_water\",\n        [\"🔞\"] = \"underage\",\n        [\"📵\"] = \"no_mobile_phones\",\n        [\"🚭\"] = \"no_smoking\",\n        [\"❗\"] = \"exclamation\",\n        [\"❕\"] = \"grey_exclamation\",\n        [\"❓\"] = \"question\",\n        [\"❔\"] = \"grey_question\",\n        [\"‼️\"] = \"bangbang\",\n        [\"⁉️\"] = \"interrobang\",\n        [\"🔅\"] = \"low_brightness\",\n        [\"🔆\"] = \"high_brightness\",\n        [\"〽️\"] = \"part_alternation_mark\",\n        [\"⚠️\"] = \"warning\",\n        [\"🚸\"] = \"children_crossing\",\n        [\"🔱\"] = \"trident\",\n        [\"⚜️\"] = \"fleur_de_lis\",\n        [\"🔰\"] = \"beginner\",\n        [\"♻️\"] = \"recycle\",\n        [\"✅\"] = \"white_check_mark\",\n        [\"🈯\"] = \"u6307\",\n        [\"💹\"] = \"chart\",\n        [\"❇️\"] = \"sparkle\",\n        [\"✳️\"] = \"eight_spoked_asterisk\",\n        [\"❎\"] = \"negative_squared_cross_mark\",\n        [\"🌐\"] = \"globe_with_meridians\",\n        [\"💠\"] = \"diamond_shape_with_a_dot_inside\",\n        [\"Ⓜ️\"] = \"m\",\n        [\"🌀\"] = \"cyclone\",\n        [\"💤\"] = \"zzz\",\n        [\"🏧\"] = \"atm\",\n        [\"🚾\"] = \"wc\",\n        [\"♿\"] = \"wheelchair\",\n        [\"🅿️\"] = \"parking\",\n        [\"🈳\"] = \"u7a7a\",\n        [\"🈂️\"] = \"sa\",\n        [\"🛂\"] = \"passport_control\",\n        [\"🛃\"] = \"customs\",\n        [\"🛄\"] = \"baggage_claim\",\n        [\"🛅\"] = \"left_luggage\",\n        [\"🛗\"] = \"elevator\",\n        [\"🚹\"] = \"mens\",\n        [\"🚺\"] = \"womens\",\n        [\"🚼\"] = \"baby_symbol\",\n        [\"🚻\"] = \"restroom\",\n        [\"🚮\"] = \"put_litter_in_its_place\",\n        [\"🎦\"] = \"cinema\",\n        [\"📶\"] = \"signal_strength\",\n        [\"🈁\"] = \"koko\",\n        [\"🔣\"] = \"symbols\",\n        [\"ℹ️\"] = \"information_source\",\n        [\"🔤\"] = \"abc\",\n        [\"🔡\"] = \"abcd\",\n        [\"🔠\"] = \"capital_abcd\",\n        [\"🆖\"] = \"ng\",\n        [\"🆗\"] = \"ok\",\n        [\"🆙\"] = \"up\",\n        [\"🆒\"] = \"cool\",\n        [\"🆕\"] = \"new\",\n        [\"🆓\"] = \"free\",\n        [\"0️⃣\"] = \"zero\",\n        [\"1️⃣\"] = \"one\",\n        [\"2️⃣\"] = \"two\",\n        [\"3️⃣\"] = \"three\",\n        [\"4️⃣\"] = \"four\",\n        [\"5️⃣\"] = \"five\",\n        [\"6️⃣\"] = \"six\",\n        [\"7️⃣\"] = \"seven\",\n        [\"8️⃣\"] = \"eight\",\n        [\"9️⃣\"] = \"nine\",\n        [\"🔟\"] = \"keycap_ten\",\n        [\"🔢\"] = \"1234\",\n        [\"#️⃣\"] = \"hash\",\n        [\"*️⃣\"] = \"asterisk\",\n        [\"⏏️\"] = \"eject\",\n        [\"▶️\"] = \"arrow_forward\",\n        [\"⏸️\"] = \"pause_button\",\n        [\"⏯️\"] = \"play_pause\",\n        [\"⏹️\"] = \"stop_button\",\n        [\"⏺️\"] = \"record_button\",\n        [\"⏭️\"] = \"track_next\",\n        [\"⏮️\"] = \"track_previous\",\n        [\"⏩\"] = \"fast_forward\",\n        [\"⏪\"] = \"rewind\",\n        [\"⏫\"] = \"arrow_double_up\",\n        [\"⏬\"] = \"arrow_double_down\",\n        [\"◀️\"] = \"arrow_backward\",\n        [\"🔼\"] = \"arrow_up_small\",\n        [\"🔽\"] = \"arrow_down_small\",\n        [\"➡️\"] = \"arrow_right\",\n        [\"⬅️\"] = \"arrow_left\",\n        [\"⬆️\"] = \"arrow_up\",\n        [\"⬇️\"] = \"arrow_down\",\n        [\"↗️\"] = \"arrow_upper_right\",\n        [\"↘️\"] = \"arrow_lower_right\",\n        [\"↙️\"] = \"arrow_lower_left\",\n        [\"↖️\"] = \"arrow_upper_left\",\n        [\"↕️\"] = \"arrow_up_down\",\n        [\"↔️\"] = \"left_right_arrow\",\n        [\"↪️\"] = \"arrow_right_hook\",\n        [\"↩️\"] = \"leftwards_arrow_with_hook\",\n        [\"⤴️\"] = \"arrow_heading_up\",\n        [\"⤵️\"] = \"arrow_heading_down\",\n        [\"🔀\"] = \"twisted_rightwards_arrows\",\n        [\"🔁\"] = \"repeat\",\n        [\"🔂\"] = \"repeat_one\",\n        [\"🔄\"] = \"arrows_counterclockwise\",\n        [\"🔃\"] = \"arrows_clockwise\",\n        [\"🎵\"] = \"musical_note\",\n        [\"🎶\"] = \"notes\",\n        [\"➕\"] = \"heavy_plus_sign\",\n        [\"➖\"] = \"heavy_minus_sign\",\n        [\"➗\"] = \"heavy_division_sign\",\n        [\"✖️\"] = \"heavy_multiplication_x\",\n        [\"♾️\"] = \"infinity\",\n        [\"💲\"] = \"heavy_dollar_sign\",\n        [\"💱\"] = \"currency_exchange\",\n        [\"™️\"] = \"tm\",\n        [\"©️\"] = \"copyright\",\n        [\"®️\"] = \"registered\",\n        [\"〰️\"] = \"wavy_dash\",\n        [\"➰\"] = \"curly_loop\",\n        [\"➿\"] = \"loop\",\n        [\"🔚\"] = \"end\",\n        [\"🔙\"] = \"back\",\n        [\"🔛\"] = \"on\",\n        [\"🔝\"] = \"top\",\n        [\"🔜\"] = \"soon\",\n        [\"✔️\"] = \"heavy_check_mark\",\n        [\"☑️\"] = \"ballot_box_with_check\",\n        [\"🔘\"] = \"radio_button\",\n        [\"⚪\"] = \"white_circle\",\n        [\"⚫\"] = \"black_circle\",\n        [\"🔴\"] = \"red_circle\",\n        [\"🔵\"] = \"blue_circle\",\n        [\"🟤\"] = \"brown_circle\",\n        [\"🟣\"] = \"purple_circle\",\n        [\"🟢\"] = \"green_circle\",\n        [\"🟡\"] = \"yellow_circle\",\n        [\"🟠\"] = \"orange_circle\",\n        [\"🔺\"] = \"small_red_triangle\",\n        [\"🔻\"] = \"small_red_triangle_down\",\n        [\"🔸\"] = \"small_orange_diamond\",\n        [\"🔹\"] = \"small_blue_diamond\",\n        [\"🔶\"] = \"large_orange_diamond\",\n        [\"🔷\"] = \"large_blue_diamond\",\n        [\"🔳\"] = \"white_square_button\",\n        [\"🔲\"] = \"black_square_button\",\n        [\"▪️\"] = \"black_small_square\",\n        [\"▫️\"] = \"white_small_square\",\n        [\"◾\"] = \"black_medium_small_square\",\n        [\"◽\"] = \"white_medium_small_square\",\n        [\"◼️\"] = \"black_medium_square\",\n        [\"◻️\"] = \"white_medium_square\",\n        [\"⬛\"] = \"black_large_square\",\n        [\"⬜\"] = \"white_large_square\",\n        [\"🟧\"] = \"orange_square\",\n        [\"🟦\"] = \"blue_square\",\n        [\"🟥\"] = \"red_square\",\n        [\"🟫\"] = \"brown_square\",\n        [\"🟪\"] = \"purple_square\",\n        [\"🟩\"] = \"green_square\",\n        [\"🟨\"] = \"yellow_square\",\n        [\"🔈\"] = \"speaker\",\n        [\"🔇\"] = \"mute\",\n        [\"🔉\"] = \"sound\",\n        [\"🔊\"] = \"loud_sound\",\n        [\"🔔\"] = \"bell\",\n        [\"🔕\"] = \"no_bell\",\n        [\"📣\"] = \"mega\",\n        [\"📢\"] = \"loudspeaker\",\n        [\"🗨️\"] = \"speech_left\",\n        [\"👁‍🗨\"] = \"eye_in_speech_bubble\",\n        [\"💬\"] = \"speech_balloon\",\n        [\"💭\"] = \"thought_balloon\",\n        [\"🗯️\"] = \"anger_right\",\n        [\"♠️\"] = \"spades\",\n        [\"♣️\"] = \"clubs\",\n        [\"♥️\"] = \"hearts\",\n        [\"♦️\"] = \"diamonds\",\n        [\"🃏\"] = \"black_joker\",\n        [\"🎴\"] = \"flower_playing_cards\",\n        [\"🀄\"] = \"mahjong\",\n        [\"🕐\"] = \"clock1\",\n        [\"🕑\"] = \"clock2\",\n        [\"🕒\"] = \"clock3\",\n        [\"🕓\"] = \"clock4\",\n        [\"🕔\"] = \"clock5\",\n        [\"🕕\"] = \"clock6\",\n        [\"🕖\"] = \"clock7\",\n        [\"🕗\"] = \"clock8\",\n        [\"🕘\"] = \"clock9\",\n        [\"🕙\"] = \"clock10\",\n        [\"🕚\"] = \"clock11\",\n        [\"🕛\"] = \"clock12\",\n        [\"🕜\"] = \"clock130\",\n        [\"🕝\"] = \"clock230\",\n        [\"🕞\"] = \"clock330\",\n        [\"🕟\"] = \"clock430\",\n        [\"🕠\"] = \"clock530\",\n        [\"🕡\"] = \"clock630\",\n        [\"🕢\"] = \"clock730\",\n        [\"🕣\"] = \"clock830\",\n        [\"🕤\"] = \"clock930\",\n        [\"🕥\"] = \"clock1030\",\n        [\"🕦\"] = \"clock1130\",\n        [\"🕧\"] = \"clock1230\",\n        [\"♀️\"] = \"female_sign\",\n        [\"♂️\"] = \"male_sign\",\n        [\"⚧\"] = \"transgender_symbol\",\n        [\"⚕️\"] = \"medical_symbol\",\n        [\"🇿\"] = \"regional_indicator_z\",\n        [\"🇾\"] = \"regional_indicator_y\",\n        [\"🇽\"] = \"regional_indicator_x\",\n        [\"🇼\"] = \"regional_indicator_w\",\n        [\"🇻\"] = \"regional_indicator_v\",\n        [\"🇺\"] = \"regional_indicator_u\",\n        [\"🇹\"] = \"regional_indicator_t\",\n        [\"🇸\"] = \"regional_indicator_s\",\n        [\"🇷\"] = \"regional_indicator_r\",\n        [\"🇶\"] = \"regional_indicator_q\",\n        [\"🇵\"] = \"regional_indicator_p\",\n        [\"🇴\"] = \"regional_indicator_o\",\n        [\"🇳\"] = \"regional_indicator_n\",\n        [\"🇲\"] = \"regional_indicator_m\",\n        [\"🇱\"] = \"regional_indicator_l\",\n        [\"🇰\"] = \"regional_indicator_k\",\n        [\"🇯\"] = \"regional_indicator_j\",\n        [\"🇮\"] = \"regional_indicator_i\",\n        [\"🇭\"] = \"regional_indicator_h\",\n        [\"🇬\"] = \"regional_indicator_g\",\n        [\"🇫\"] = \"regional_indicator_f\",\n        [\"🇪\"] = \"regional_indicator_e\",\n        [\"🇩\"] = \"regional_indicator_d\",\n        [\"🇨\"] = \"regional_indicator_c\",\n        [\"🇧\"] = \"regional_indicator_b\",\n        [\"🇦\"] = \"regional_indicator_a\",\n        [\"🏳️\"] = \"flag_white\",\n        [\"🏴\"] = \"flag_black\",\n        [\"🏁\"] = \"checkered_flag\",\n        [\"🚩\"] = \"triangular_flag_on_post\",\n        [\"🏳️‍🌈\"] = \"rainbow_flag\",\n        [\"🏳️‍⚧️\"] = \"transgender_flag\",\n        [\"🏴‍☠️\"] = \"pirate_flag\",\n        [\"🇦🇫\"] = \"flag_af\",\n        [\"🇦🇽\"] = \"flag_ax\",\n        [\"🇦🇱\"] = \"flag_al\",\n        [\"🇩🇿\"] = \"flag_dz\",\n        [\"🇦🇸\"] = \"flag_as\",\n        [\"🇦🇩\"] = \"flag_ad\",\n        [\"🇦🇴\"] = \"flag_ao\",\n        [\"🇦🇮\"] = \"flag_ai\",\n        [\"🇦🇶\"] = \"flag_aq\",\n        [\"🇦🇬\"] = \"flag_ag\",\n        [\"🇦🇷\"] = \"flag_ar\",\n        [\"🇦🇲\"] = \"flag_am\",\n        [\"🇦🇼\"] = \"flag_aw\",\n        [\"🇦🇺\"] = \"flag_au\",\n        [\"🇦🇹\"] = \"flag_at\",\n        [\"🇦🇿\"] = \"flag_az\",\n        [\"🇧🇸\"] = \"flag_bs\",\n        [\"🇧🇭\"] = \"flag_bh\",\n        [\"🇧🇩\"] = \"flag_bd\",\n        [\"🇧🇧\"] = \"flag_bb\",\n        [\"🇧🇾\"] = \"flag_by\",\n        [\"🇧🇪\"] = \"flag_be\",\n        [\"🇧🇿\"] = \"flag_bz\",\n        [\"🇧🇯\"] = \"flag_bj\",\n        [\"🇧🇲\"] = \"flag_bm\",\n        [\"🇧🇹\"] = \"flag_bt\",\n        [\"🇧🇴\"] = \"flag_bo\",\n        [\"🇧🇦\"] = \"flag_ba\",\n        [\"🇧🇼\"] = \"flag_bw\",\n        [\"🇧🇷\"] = \"flag_br\",\n        [\"🇮🇴\"] = \"flag_io\",\n        [\"🇻🇬\"] = \"flag_vg\",\n        [\"🇧🇳\"] = \"flag_bn\",\n        [\"🇧🇬\"] = \"flag_bg\",\n        [\"🇧🇫\"] = \"flag_bf\",\n        [\"🇧🇮\"] = \"flag_bi\",\n        [\"🇰🇭\"] = \"flag_kh\",\n        [\"🇨🇲\"] = \"flag_cm\",\n        [\"🇨🇦\"] = \"flag_ca\",\n        [\"🇮🇨\"] = \"flag_ic\",\n        [\"🇨🇻\"] = \"flag_cv\",\n        [\"🇧🇶\"] = \"flag_bq\",\n        [\"🇰🇾\"] = \"flag_ky\",\n        [\"🇨🇫\"] = \"flag_cf\",\n        [\"🇹🇩\"] = \"flag_td\",\n        [\"🇨🇱\"] = \"flag_cl\",\n        [\"🇨🇳\"] = \"flag_cn\",\n        [\"🇨🇽\"] = \"flag_cx\",\n        [\"🇨🇨\"] = \"flag_cc\",\n        [\"🇨🇴\"] = \"flag_co\",\n        [\"🇰🇲\"] = \"flag_km\",\n        [\"🇨🇬\"] = \"flag_cg\",\n        [\"🇨🇩\"] = \"flag_cd\",\n        [\"🇨🇰\"] = \"flag_ck\",\n        [\"🇨🇷\"] = \"flag_cr\",\n        [\"🇨🇮\"] = \"flag_ci\",\n        [\"🇭🇷\"] = \"flag_hr\",\n        [\"🇨🇺\"] = \"flag_cu\",\n        [\"🇨🇼\"] = \"flag_cw\",\n        [\"🇨🇾\"] = \"flag_cy\",\n        [\"🇨🇿\"] = \"flag_cz\",\n        [\"🇩🇰\"] = \"flag_dk\",\n        [\"🇩🇯\"] = \"flag_dj\",\n        [\"🇩🇲\"] = \"flag_dm\",\n        [\"🇩🇴\"] = \"flag_do\",\n        [\"🇪🇨\"] = \"flag_ec\",\n        [\"🇪🇬\"] = \"flag_eg\",\n        [\"🇸🇻\"] = \"flag_sv\",\n        [\"🇬🇶\"] = \"flag_gq\",\n        [\"🇪🇷\"] = \"flag_er\",\n        [\"🇪🇪\"] = \"flag_ee\",\n        [\"🇪🇹\"] = \"flag_et\",\n        [\"🇪🇺\"] = \"flag_eu\",\n        [\"🇫🇰\"] = \"flag_fk\",\n        [\"🇫🇴\"] = \"flag_fo\",\n        [\"🇫🇯\"] = \"flag_fj\",\n        [\"🇫🇮\"] = \"flag_fi\",\n        [\"🇫🇷\"] = \"flag_fr\",\n        [\"🇬🇫\"] = \"flag_gf\",\n        [\"🇵🇫\"] = \"flag_pf\",\n        [\"🇹🇫\"] = \"flag_tf\",\n        [\"🇬🇦\"] = \"flag_ga\",\n        [\"🇬🇲\"] = \"flag_gm\",\n        [\"🇬🇪\"] = \"flag_ge\",\n        [\"🇩🇪\"] = \"flag_de\",\n        [\"🇬🇭\"] = \"flag_gh\",\n        [\"🇬🇮\"] = \"flag_gi\",\n        [\"🇬🇷\"] = \"flag_gr\",\n        [\"🇬🇱\"] = \"flag_gl\",\n        [\"🇬🇩\"] = \"flag_gd\",\n        [\"🇬🇵\"] = \"flag_gp\",\n        [\"🇬🇺\"] = \"flag_gu\",\n        [\"🇬🇹\"] = \"flag_gt\",\n        [\"🇬🇬\"] = \"flag_gg\",\n        [\"🇬🇳\"] = \"flag_gn\",\n        [\"🇬🇼\"] = \"flag_gw\",\n        [\"🇬🇾\"] = \"flag_gy\",\n        [\"🇭🇹\"] = \"flag_ht\",\n        [\"🇭🇳\"] = \"flag_hn\",\n        [\"🇭🇰\"] = \"flag_hk\",\n        [\"🇭🇺\"] = \"flag_hu\",\n        [\"🇮🇸\"] = \"flag_is\",\n        [\"🇮🇳\"] = \"flag_in\",\n        [\"🇮🇩\"] = \"flag_id\",\n        [\"🇮🇷\"] = \"flag_ir\",\n        [\"🇮🇶\"] = \"flag_iq\",\n        [\"🇮🇪\"] = \"flag_ie\",\n        [\"🇮🇲\"] = \"flag_im\",\n        [\"🇮🇱\"] = \"flag_il\",\n        [\"🇮🇹\"] = \"flag_it\",\n        [\"🇯🇲\"] = \"flag_jm\",\n        [\"🇯🇵\"] = \"flag_jp\",\n        [\"🎌\"] = \"crossed_flags\",\n        [\"🇯🇪\"] = \"flag_je\",\n        [\"🇯🇴\"] = \"flag_jo\",\n        [\"🇰🇿\"] = \"flag_kz\",\n        [\"🇰🇪\"] = \"flag_ke\",\n        [\"🇰🇮\"] = \"flag_ki\",\n        [\"🇽🇰\"] = \"flag_xk\",\n        [\"🇰🇼\"] = \"flag_kw\",\n        [\"🇰🇬\"] = \"flag_kg\",\n        [\"🇱🇦\"] = \"flag_la\",\n        [\"🇱🇻\"] = \"flag_lv\",\n        [\"🇱🇧\"] = \"flag_lb\",\n        [\"🇱🇸\"] = \"flag_ls\",\n        [\"🇱🇷\"] = \"flag_lr\",\n        [\"🇱🇾\"] = \"flag_ly\",\n        [\"🇱🇮\"] = \"flag_li\",\n        [\"🇱🇹\"] = \"flag_lt\",\n        [\"🇱🇺\"] = \"flag_lu\",\n        [\"🇲🇴\"] = \"flag_mo\",\n        [\"🇲🇰\"] = \"flag_mk\",\n        [\"🇲🇬\"] = \"flag_mg\",\n        [\"🇲🇼\"] = \"flag_mw\",\n        [\"🇲🇾\"] = \"flag_my\",\n        [\"🇲🇻\"] = \"flag_mv\",\n        [\"🇲🇱\"] = \"flag_ml\",\n        [\"🇲🇹\"] = \"flag_mt\",\n        [\"🇲🇭\"] = \"flag_mh\",\n        [\"🇲🇶\"] = \"flag_mq\",\n        [\"🇲🇷\"] = \"flag_mr\",\n        [\"🇲🇺\"] = \"flag_mu\",\n        [\"🇾🇹\"] = \"flag_yt\",\n        [\"🇲🇽\"] = \"flag_mx\",\n        [\"🇫🇲\"] = \"flag_fm\",\n        [\"🇲🇩\"] = \"flag_md\",\n        [\"🇲🇨\"] = \"flag_mc\",\n        [\"🇲🇳\"] = \"flag_mn\",\n        [\"🇲🇪\"] = \"flag_me\",\n        [\"🇲🇸\"] = \"flag_ms\",\n        [\"🇲🇦\"] = \"flag_ma\",\n        [\"🇲🇿\"] = \"flag_mz\",\n        [\"🇲🇲\"] = \"flag_mm\",\n        [\"🇳🇦\"] = \"flag_na\",\n        [\"🇳🇷\"] = \"flag_nr\",\n        [\"🇳🇵\"] = \"flag_np\",\n        [\"🇳🇱\"] = \"flag_nl\",\n        [\"🇳🇨\"] = \"flag_nc\",\n        [\"🇳🇿\"] = \"flag_nz\",\n        [\"🇳🇮\"] = \"flag_ni\",\n        [\"🇳🇪\"] = \"flag_ne\",\n        [\"🇳🇬\"] = \"flag_ng\",\n        [\"🇳🇺\"] = \"flag_nu\",\n        [\"🇳🇫\"] = \"flag_nf\",\n        [\"🇰🇵\"] = \"flag_kp\",\n        [\"🇲🇵\"] = \"flag_mp\",\n        [\"🇳🇴\"] = \"flag_no\",\n        [\"🇴🇲\"] = \"flag_om\",\n        [\"🇵🇰\"] = \"flag_pk\",\n        [\"🇵🇼\"] = \"flag_pw\",\n        [\"🇵🇸\"] = \"flag_ps\",\n        [\"🇵🇦\"] = \"flag_pa\",\n        [\"🇵🇬\"] = \"flag_pg\",\n        [\"🇵🇾\"] = \"flag_py\",\n        [\"🇵🇪\"] = \"flag_pe\",\n        [\"🇵🇭\"] = \"flag_ph\",\n        [\"🇵🇳\"] = \"flag_pn\",\n        [\"🇵🇱\"] = \"flag_pl\",\n        [\"🇵🇹\"] = \"flag_pt\",\n        [\"🇵🇷\"] = \"flag_pr\",\n        [\"🇶🇦\"] = \"flag_qa\",\n        [\"🇷🇪\"] = \"flag_re\",\n        [\"🇷🇴\"] = \"flag_ro\",\n        [\"🇷🇺\"] = \"flag_ru\",\n        [\"🇷🇼\"] = \"flag_rw\",\n        [\"🇼🇸\"] = \"flag_ws\",\n        [\"🇸🇲\"] = \"flag_sm\",\n        [\"🇸🇹\"] = \"flag_st\",\n        [\"🇸🇦\"] = \"flag_sa\",\n        [\"🇸🇳\"] = \"flag_sn\",\n        [\"🇷🇸\"] = \"flag_rs\",\n        [\"🇸🇨\"] = \"flag_sc\",\n        [\"🇸🇱\"] = \"flag_sl\",\n        [\"🇸🇬\"] = \"flag_sg\",\n        [\"🇸🇽\"] = \"flag_sx\",\n        [\"🇸🇰\"] = \"flag_sk\",\n        [\"🇸🇮\"] = \"flag_si\",\n        [\"🇬🇸\"] = \"flag_gs\",\n        [\"🇸🇧\"] = \"flag_sb\",\n        [\"🇸🇴\"] = \"flag_so\",\n        [\"🇿🇦\"] = \"flag_za\",\n        [\"🇰🇷\"] = \"flag_kr\",\n        [\"🇸🇸\"] = \"flag_ss\",\n        [\"🇪🇸\"] = \"flag_es\",\n        [\"🇱🇰\"] = \"flag_lk\",\n        [\"🇧🇱\"] = \"flag_bl\",\n        [\"🇸🇭\"] = \"flag_sh\",\n        [\"🇰🇳\"] = \"flag_kn\",\n        [\"🇱🇨\"] = \"flag_lc\",\n        [\"🇵🇲\"] = \"flag_pm\",\n        [\"🇻🇨\"] = \"flag_vc\",\n        [\"🇸🇩\"] = \"flag_sd\",\n        [\"🇸🇷\"] = \"flag_sr\",\n        [\"🇸🇿\"] = \"flag_sz\",\n        [\"🇸🇪\"] = \"flag_se\",\n        [\"🇨🇭\"] = \"flag_ch\",\n        [\"🇸🇾\"] = \"flag_sy\",\n        [\"🇹🇼\"] = \"flag_tw\",\n        [\"🇹🇯\"] = \"flag_tj\",\n        [\"🇹🇿\"] = \"flag_tz\",\n        [\"🇹🇭\"] = \"flag_th\",\n        [\"🇹🇱\"] = \"flag_tl\",\n        [\"🇹🇬\"] = \"flag_tg\",\n        [\"🇹🇰\"] = \"flag_tk\",\n        [\"🇹🇴\"] = \"flag_to\",\n        [\"🇹🇹\"] = \"flag_tt\",\n        [\"🇹🇳\"] = \"flag_tn\",\n        [\"🇹🇷\"] = \"flag_tr\",\n        [\"🇹🇲\"] = \"flag_tm\",\n        [\"🇹🇨\"] = \"flag_tc\",\n        [\"🇻🇮\"] = \"flag_vi\",\n        [\"🇹🇻\"] = \"flag_tv\",\n        [\"🇺🇬\"] = \"flag_ug\",\n        [\"🇺🇦\"] = \"flag_ua\",\n        [\"🇦🇪\"] = \"flag_ae\",\n        [\"🇬🇧\"] = \"flag_gb\",\n        [\"🏴󠁧󠁢󠁥󠁮󠁧󠁿\"] = \"england\",\n        [\"🏴󠁧󠁢󠁳󠁣󠁴󠁿\"] = \"scotland\",\n        [\"🏴󠁧󠁢󠁷󠁬󠁳󠁿\"] = \"wales\",\n        [\"🇺🇸\"] = \"flag_us\",\n        [\"🇺🇾\"] = \"flag_uy\",\n        [\"🇺🇿\"] = \"flag_uz\",\n        [\"🇻🇺\"] = \"flag_vu\",\n        [\"🇻🇦\"] = \"flag_va\",\n        [\"🇻🇪\"] = \"flag_ve\",\n        [\"🇻🇳\"] = \"flag_vn\",\n        [\"🇼🇫\"] = \"flag_wf\",\n        [\"🇪🇭\"] = \"flag_eh\",\n        [\"🇾🇪\"] = \"flag_ye\",\n        [\"🇿🇲\"] = \"flag_zm\",\n        [\"🇿🇼\"] = \"flag_zw\",\n        [\"🇦🇨\"] = \"flag_ac\",\n        [\"🇧🇻\"] = \"flag_bv\",\n        [\"🇨🇵\"] = \"flag_cp\",\n        [\"🇪🇦\"] = \"flag_ea\",\n        [\"🇩🇬\"] = \"flag_dg\",\n        [\"🇭🇲\"] = \"flag_hm\",\n        [\"🇲🇫\"] = \"flag_mf\",\n        [\"🇸🇯\"] = \"flag_sj\",\n        [\"🇹🇦\"] = \"flag_ta\",\n        [\"🇺🇲\"] = \"flag_um\",\n        [\"🇺🇳\"] = \"united_nations\",\n    };\n\n    private static Dictionary<string, string> _fromCodes = new(5000, StringComparer.Ordinal)\n    {\n        [\"grinning\"] = \"😀\",\n        [\"smiley\"] = \"😃\",\n        [\"smile\"] = \"😄\",\n        [\"grin\"] = \"😁\",\n        [\"laughing\"] = \"😆\",\n        [\"satisfied\"] = \"😆\",\n        [\"sweat_smile\"] = \"😅\",\n        [\"joy\"] = \"😂\",\n        [\"rofl\"] = \"🤣\",\n        [\"rolling_on_the_floor_laughing\"] = \"🤣\",\n        [\"relaxed\"] = \"☺️\",\n        [\"blush\"] = \"😊\",\n        [\"innocent\"] = \"😇\",\n        [\"slight_smile\"] = \"🙂\",\n        [\"slightly_smiling_face\"] = \"🙂\",\n        [\"upside_down\"] = \"🙃\",\n        [\"upside_down_face\"] = \"🙃\",\n        [\"wink\"] = \"😉\",\n        [\"relieved\"] = \"😌\",\n        [\"smiling_face_with_tear\"] = \"🥲\",\n        [\"heart_eyes\"] = \"😍\",\n        [\"smiling_face_with_3_hearts\"] = \"🥰\",\n        [\"kissing_heart\"] = \"😘\",\n        [\"kissing\"] = \"😗\",\n        [\"kissing_smiling_eyes\"] = \"😙\",\n        [\"kissing_closed_eyes\"] = \"😚\",\n        [\"yum\"] = \"😋\",\n        [\"stuck_out_tongue\"] = \"😛\",\n        [\"stuck_out_tongue_closed_eyes\"] = \"😝\",\n        [\"stuck_out_tongue_winking_eye\"] = \"😜\",\n        [\"zany_face\"] = \"🤪\",\n        [\"face_with_raised_eyebrow\"] = \"🤨\",\n        [\"face_with_monocle\"] = \"🧐\",\n        [\"nerd\"] = \"🤓\",\n        [\"nerd_face\"] = \"🤓\",\n        [\"sunglasses\"] = \"😎\",\n        [\"star_struck\"] = \"🤩\",\n        [\"partying_face\"] = \"🥳\",\n        [\"smirk\"] = \"😏\",\n        [\"unamused\"] = \"😒\",\n        [\"disappointed\"] = \"😞\",\n        [\"pensive\"] = \"😔\",\n        [\"worried\"] = \"😟\",\n        [\"confused\"] = \"😕\",\n        [\"slight_frown\"] = \"🙁\",\n        [\"slightly_frowning_face\"] = \"🙁\",\n        [\"frowning2\"] = \"☹️\",\n        [\"white_frowning_face\"] = \"☹️\",\n        [\"persevere\"] = \"😣\",\n        [\"confounded\"] = \"😖\",\n        [\"tired_face\"] = \"😫\",\n        [\"weary\"] = \"😩\",\n        [\"pleading_face\"] = \"🥺\",\n        [\"cry\"] = \"😢\",\n        [\"sob\"] = \"😭\",\n        [\"triumph\"] = \"😤\",\n        [\"face_exhaling\"] = \"😮‍💨\",\n        [\"angry\"] = \"😠\",\n        [\"rage\"] = \"😡\",\n        [\"face_with_symbols_over_mouth\"] = \"🤬\",\n        [\"exploding_head\"] = \"🤯\",\n        [\"flushed\"] = \"😳\",\n        [\"face_in_clouds\"] = \"😶‍🌫️\",\n        [\"hot_face\"] = \"🥵\",\n        [\"cold_face\"] = \"🥶\",\n        [\"scream\"] = \"😱\",\n        [\"fearful\"] = \"😨\",\n        [\"cold_sweat\"] = \"😰\",\n        [\"disappointed_relieved\"] = \"😥\",\n        [\"sweat\"] = \"😓\",\n        [\"hugging\"] = \"🤗\",\n        [\"hugging_face\"] = \"🤗\",\n        [\"thinking\"] = \"🤔\",\n        [\"thinking_face\"] = \"🤔\",\n        [\"face_with_hand_over_mouth\"] = \"🤭\",\n        [\"yawning_face\"] = \"🥱\",\n        [\"shushing_face\"] = \"🤫\",\n        [\"lying_face\"] = \"🤥\",\n        [\"liar\"] = \"🤥\",\n        [\"no_mouth\"] = \"😶\",\n        [\"neutral_face\"] = \"😐\",\n        [\"expressionless\"] = \"😑\",\n        [\"grimacing\"] = \"😬\",\n        [\"rolling_eyes\"] = \"🙄\",\n        [\"face_with_rolling_eyes\"] = \"🙄\",\n        [\"hushed\"] = \"😯\",\n        [\"frowning\"] = \"😦\",\n        [\"anguished\"] = \"😧\",\n        [\"open_mouth\"] = \"😮\",\n        [\"astonished\"] = \"😲\",\n        [\"sleeping\"] = \"😴\",\n        [\"drooling_face\"] = \"🤤\",\n        [\"drool\"] = \"🤤\",\n        [\"sleepy\"] = \"😪\",\n        [\"dizzy_face\"] = \"😵\",\n        [\"face_with_spiral_eyes\"] = \"😵‍💫\",\n        [\"zipper_mouth\"] = \"🤐\",\n        [\"zipper_mouth_face\"] = \"🤐\",\n        [\"woozy_face\"] = \"🥴\",\n        [\"nauseated_face\"] = \"🤢\",\n        [\"sick\"] = \"🤢\",\n        [\"face_vomiting\"] = \"🤮\",\n        [\"sneezing_face\"] = \"🤧\",\n        [\"sneeze\"] = \"🤧\",\n        [\"mask\"] = \"😷\",\n        [\"thermometer_face\"] = \"🤒\",\n        [\"face_with_thermometer\"] = \"🤒\",\n        [\"head_bandage\"] = \"🤕\",\n        [\"face_with_head_bandage\"] = \"🤕\",\n        [\"money_mouth\"] = \"🤑\",\n        [\"money_mouth_face\"] = \"🤑\",\n        [\"cowboy\"] = \"🤠\",\n        [\"face_with_cowboy_hat\"] = \"🤠\",\n        [\"disguised_face\"] = \"🥸\",\n        [\"smiling_imp\"] = \"😈\",\n        [\"imp\"] = \"👿\",\n        [\"japanese_ogre\"] = \"👹\",\n        [\"japanese_goblin\"] = \"👺\",\n        [\"clown\"] = \"🤡\",\n        [\"clown_face\"] = \"🤡\",\n        [\"poop\"] = \"💩\",\n        [\"shit\"] = \"💩\",\n        [\"hankey\"] = \"💩\",\n        [\"poo\"] = \"💩\",\n        [\"ghost\"] = \"👻\",\n        [\"skull\"] = \"💀\",\n        [\"skeleton\"] = \"💀\",\n        [\"skull_crossbones\"] = \"☠️\",\n        [\"skull_and_crossbones\"] = \"☠️\",\n        [\"alien\"] = \"👽\",\n        [\"space_invader\"] = \"👾\",\n        [\"robot\"] = \"🤖\",\n        [\"robot_face\"] = \"🤖\",\n        [\"jack_o_lantern\"] = \"🎃\",\n        [\"smiley_cat\"] = \"😺\",\n        [\"smile_cat\"] = \"😸\",\n        [\"joy_cat\"] = \"😹\",\n        [\"heart_eyes_cat\"] = \"😻\",\n        [\"smirk_cat\"] = \"😼\",\n        [\"kissing_cat\"] = \"😽\",\n        [\"scream_cat\"] = \"🙀\",\n        [\"crying_cat_face\"] = \"😿\",\n        [\"pouting_cat\"] = \"😾\",\n        [\"palms_up_together\"] = \"🤲\",\n        [\"palms_up_together_tone1\"] = \"🤲🏻\",\n        [\"palms_up_together_light_skin_tone\"] = \"🤲🏻\",\n        [\"palms_up_together_tone2\"] = \"🤲🏼\",\n        [\"palms_up_together_medium_light_skin_tone\"] = \"🤲🏼\",\n        [\"palms_up_together_tone3\"] = \"🤲🏽\",\n        [\"palms_up_together_medium_skin_tone\"] = \"🤲🏽\",\n        [\"palms_up_together_tone4\"] = \"🤲🏾\",\n        [\"palms_up_together_medium_dark_skin_tone\"] = \"🤲🏾\",\n        [\"palms_up_together_tone5\"] = \"🤲🏿\",\n        [\"palms_up_together_dark_skin_tone\"] = \"🤲🏿\",\n        [\"open_hands\"] = \"👐\",\n        [\"open_hands_tone1\"] = \"👐🏻\",\n        [\"open_hands_tone2\"] = \"👐🏼\",\n        [\"open_hands_tone3\"] = \"👐🏽\",\n        [\"open_hands_tone4\"] = \"👐🏾\",\n        [\"open_hands_tone5\"] = \"👐🏿\",\n        [\"raised_hands\"] = \"🙌\",\n        [\"raised_hands_tone1\"] = \"🙌🏻\",\n        [\"raised_hands_tone2\"] = \"🙌🏼\",\n        [\"raised_hands_tone3\"] = \"🙌🏽\",\n        [\"raised_hands_tone4\"] = \"🙌🏾\",\n        [\"raised_hands_tone5\"] = \"🙌🏿\",\n        [\"clap\"] = \"👏\",\n        [\"clap_tone1\"] = \"👏🏻\",\n        [\"clap_tone2\"] = \"👏🏼\",\n        [\"clap_tone3\"] = \"👏🏽\",\n        [\"clap_tone4\"] = \"👏🏾\",\n        [\"clap_tone5\"] = \"👏🏿\",\n        [\"handshake\"] = \"🤝\",\n        [\"shaking_hands\"] = \"🤝\",\n        [\"thumbsup\"] = \"👍\",\n        [\"+1\"] = \"👍\",\n        [\"thumbup\"] = \"👍\",\n        [\"thumbsup_tone1\"] = \"👍🏻\",\n        [\"+1_tone1\"] = \"👍🏻\",\n        [\"thumbup_tone1\"] = \"👍🏻\",\n        [\"thumbsup_tone2\"] = \"👍🏼\",\n        [\"+1_tone2\"] = \"👍🏼\",\n        [\"thumbup_tone2\"] = \"👍🏼\",\n        [\"thumbsup_tone3\"] = \"👍🏽\",\n        [\"+1_tone3\"] = \"👍🏽\",\n        [\"thumbup_tone3\"] = \"👍🏽\",\n        [\"thumbsup_tone4\"] = \"👍🏾\",\n        [\"+1_tone4\"] = \"👍🏾\",\n        [\"thumbup_tone4\"] = \"👍🏾\",\n        [\"thumbsup_tone5\"] = \"👍🏿\",\n        [\"+1_tone5\"] = \"👍🏿\",\n        [\"thumbup_tone5\"] = \"👍🏿\",\n        [\"thumbsdown\"] = \"👎\",\n        [\"-1\"] = \"👎\",\n        [\"thumbdown\"] = \"👎\",\n        [\"thumbsdown_tone1\"] = \"👎🏻\",\n        [\"_1_tone1\"] = \"👎🏻\",\n        [\"thumbdown_tone1\"] = \"👎🏻\",\n        [\"thumbsdown_tone2\"] = \"👎🏼\",\n        [\"_1_tone2\"] = \"👎🏼\",\n        [\"thumbdown_tone2\"] = \"👎🏼\",\n        [\"thumbsdown_tone3\"] = \"👎🏽\",\n        [\"_1_tone3\"] = \"👎🏽\",\n        [\"thumbdown_tone3\"] = \"👎🏽\",\n        [\"thumbsdown_tone4\"] = \"👎🏾\",\n        [\"_1_tone4\"] = \"👎🏾\",\n        [\"thumbdown_tone4\"] = \"👎🏾\",\n        [\"thumbsdown_tone5\"] = \"👎🏿\",\n        [\"_1_tone5\"] = \"👎🏿\",\n        [\"thumbdown_tone5\"] = \"👎🏿\",\n        [\"punch\"] = \"👊\",\n        [\"punch_tone1\"] = \"👊🏻\",\n        [\"punch_tone2\"] = \"👊🏼\",\n        [\"punch_tone3\"] = \"👊🏽\",\n        [\"punch_tone4\"] = \"👊🏾\",\n        [\"punch_tone5\"] = \"👊🏿\",\n        [\"fist\"] = \"✊\",\n        [\"fist_tone1\"] = \"✊🏻\",\n        [\"fist_tone2\"] = \"✊🏼\",\n        [\"fist_tone3\"] = \"✊🏽\",\n        [\"fist_tone4\"] = \"✊🏾\",\n        [\"fist_tone5\"] = \"✊🏿\",\n        [\"left_facing_fist\"] = \"🤛\",\n        [\"left_fist\"] = \"🤛\",\n        [\"left_facing_fist_tone1\"] = \"🤛🏻\",\n        [\"left_fist_tone1\"] = \"🤛🏻\",\n        [\"left_facing_fist_tone2\"] = \"🤛🏼\",\n        [\"left_fist_tone2\"] = \"🤛🏼\",\n        [\"left_facing_fist_tone3\"] = \"🤛🏽\",\n        [\"left_fist_tone3\"] = \"🤛🏽\",\n        [\"left_facing_fist_tone4\"] = \"🤛🏾\",\n        [\"left_fist_tone4\"] = \"🤛🏾\",\n        [\"left_facing_fist_tone5\"] = \"🤛🏿\",\n        [\"left_fist_tone5\"] = \"🤛🏿\",\n        [\"right_facing_fist\"] = \"🤜\",\n        [\"right_fist\"] = \"🤜\",\n        [\"right_facing_fist_tone1\"] = \"🤜🏻\",\n        [\"right_fist_tone1\"] = \"🤜🏻\",\n        [\"right_facing_fist_tone2\"] = \"🤜🏼\",\n        [\"right_fist_tone2\"] = \"🤜🏼\",\n        [\"right_facing_fist_tone3\"] = \"🤜🏽\",\n        [\"right_fist_tone3\"] = \"🤜🏽\",\n        [\"right_facing_fist_tone4\"] = \"🤜🏾\",\n        [\"right_fist_tone4\"] = \"🤜🏾\",\n        [\"right_facing_fist_tone5\"] = \"🤜🏿\",\n        [\"right_fist_tone5\"] = \"🤜🏿\",\n        [\"fingers_crossed\"] = \"🤞\",\n        [\"hand_with_index_and_middle_finger_crossed\"] = \"🤞\",\n        [\"fingers_crossed_tone1\"] = \"🤞🏻\",\n        [\"hand_with_index_and_middle_fingers_crossed_tone1\"] = \"🤞🏻\",\n        [\"fingers_crossed_tone2\"] = \"🤞🏼\",\n        [\"hand_with_index_and_middle_fingers_crossed_tone2\"] = \"🤞🏼\",\n        [\"fingers_crossed_tone3\"] = \"🤞🏽\",\n        [\"hand_with_index_and_middle_fingers_crossed_tone3\"] = \"🤞🏽\",\n        [\"fingers_crossed_tone4\"] = \"🤞🏾\",\n        [\"hand_with_index_and_middle_fingers_crossed_tone4\"] = \"🤞🏾\",\n        [\"fingers_crossed_tone5\"] = \"🤞🏿\",\n        [\"hand_with_index_and_middle_fingers_crossed_tone5\"] = \"🤞🏿\",\n        [\"v\"] = \"✌️\",\n        [\"v_tone1\"] = \"✌🏻\",\n        [\"v_tone2\"] = \"✌🏼\",\n        [\"v_tone3\"] = \"✌🏽\",\n        [\"v_tone4\"] = \"✌🏾\",\n        [\"v_tone5\"] = \"✌🏿\",\n        [\"love_you_gesture\"] = \"🤟\",\n        [\"love_you_gesture_tone1\"] = \"🤟🏻\",\n        [\"love_you_gesture_light_skin_tone\"] = \"🤟🏻\",\n        [\"love_you_gesture_tone2\"] = \"🤟🏼\",\n        [\"love_you_gesture_medium_light_skin_tone\"] = \"🤟🏼\",\n        [\"love_you_gesture_tone3\"] = \"🤟🏽\",\n        [\"love_you_gesture_medium_skin_tone\"] = \"🤟🏽\",\n        [\"love_you_gesture_tone4\"] = \"🤟🏾\",\n        [\"love_you_gesture_medium_dark_skin_tone\"] = \"🤟🏾\",\n        [\"love_you_gesture_tone5\"] = \"🤟🏿\",\n        [\"love_you_gesture_dark_skin_tone\"] = \"🤟🏿\",\n        [\"metal\"] = \"🤘\",\n        [\"sign_of_the_horns\"] = \"🤘\",\n        [\"metal_tone1\"] = \"🤘🏻\",\n        [\"sign_of_the_horns_tone1\"] = \"🤘🏻\",\n        [\"metal_tone2\"] = \"🤘🏼\",\n        [\"sign_of_the_horns_tone2\"] = \"🤘🏼\",\n        [\"metal_tone3\"] = \"🤘🏽\",\n        [\"sign_of_the_horns_tone3\"] = \"🤘🏽\",\n        [\"metal_tone4\"] = \"🤘🏾\",\n        [\"sign_of_the_horns_tone4\"] = \"🤘🏾\",\n        [\"metal_tone5\"] = \"🤘🏿\",\n        [\"sign_of_the_horns_tone5\"] = \"🤘🏿\",\n        [\"ok_hand\"] = \"👌\",\n        [\"ok_hand_tone1\"] = \"👌🏻\",\n        [\"ok_hand_tone2\"] = \"👌🏼\",\n        [\"ok_hand_tone3\"] = \"👌🏽\",\n        [\"ok_hand_tone4\"] = \"👌🏾\",\n        [\"ok_hand_tone5\"] = \"👌🏿\",\n        [\"pinching_hand\"] = \"🤏\",\n        [\"pinching_hand_tone1\"] = \"🤏🏻\",\n        [\"pinching_hand_light_skin_tone\"] = \"🤏🏻\",\n        [\"pinching_hand_tone2\"] = \"🤏🏼\",\n        [\"pinching_hand_medium_light_skin_tone\"] = \"🤏🏼\",\n        [\"pinching_hand_tone3\"] = \"🤏🏽\",\n        [\"pinching_hand_medium_skin_tone\"] = \"🤏🏽\",\n        [\"pinching_hand_tone4\"] = \"🤏🏾\",\n        [\"pinching_hand_medium_dark_skin_tone\"] = \"🤏🏾\",\n        [\"pinching_hand_tone5\"] = \"🤏🏿\",\n        [\"pinching_hand_dark_skin_tone\"] = \"🤏🏿\",\n        [\"pinched_fingers\"] = \"🤌\",\n        [\"pinched_fingers_tone2\"] = \"🤌🏼\",\n        [\"pinched_fingers_medium_light_skin_tone\"] = \"🤌🏼\",\n        [\"pinched_fingers_tone1\"] = \"🤌🏻\",\n        [\"pinched_fingers_light_skin_tone\"] = \"🤌🏻\",\n        [\"pinched_fingers_tone3\"] = \"🤌🏽\",\n        [\"pinched_fingers_medium_skin_tone\"] = \"🤌🏽\",\n        [\"pinched_fingers_tone4\"] = \"🤌🏾\",\n        [\"pinched_fingers_medium_dark_skin_tone\"] = \"🤌🏾\",\n        [\"pinched_fingers_tone5\"] = \"🤌🏿\",\n        [\"pinched_fingers_dark_skin_tone\"] = \"🤌🏿\",\n        [\"point_left\"] = \"👈\",\n        [\"point_left_tone1\"] = \"👈🏻\",\n        [\"point_left_tone2\"] = \"👈🏼\",\n        [\"point_left_tone3\"] = \"👈🏽\",\n        [\"point_left_tone4\"] = \"👈🏾\",\n        [\"point_left_tone5\"] = \"👈🏿\",\n        [\"point_right\"] = \"👉\",\n        [\"point_right_tone1\"] = \"👉🏻\",\n        [\"point_right_tone2\"] = \"👉🏼\",\n        [\"point_right_tone3\"] = \"👉🏽\",\n        [\"point_right_tone4\"] = \"👉🏾\",\n        [\"point_right_tone5\"] = \"👉🏿\",\n        [\"point_up_2\"] = \"👆\",\n        [\"point_up_2_tone1\"] = \"👆🏻\",\n        [\"point_up_2_tone2\"] = \"👆🏼\",\n        [\"point_up_2_tone3\"] = \"👆🏽\",\n        [\"point_up_2_tone4\"] = \"👆🏾\",\n        [\"point_up_2_tone5\"] = \"👆🏿\",\n        [\"point_down\"] = \"👇\",\n        [\"point_down_tone1\"] = \"👇🏻\",\n        [\"point_down_tone2\"] = \"👇🏼\",\n        [\"point_down_tone3\"] = \"👇🏽\",\n        [\"point_down_tone4\"] = \"👇🏾\",\n        [\"point_down_tone5\"] = \"👇🏿\",\n        [\"point_up\"] = \"☝️\",\n        [\"point_up_tone1\"] = \"☝🏻\",\n        [\"point_up_tone2\"] = \"☝🏼\",\n        [\"point_up_tone3\"] = \"☝🏽\",\n        [\"point_up_tone4\"] = \"☝🏾\",\n        [\"point_up_tone5\"] = \"☝🏿\",\n        [\"raised_hand\"] = \"✋\",\n        [\"raised_hand_tone1\"] = \"✋🏻\",\n        [\"raised_hand_tone2\"] = \"✋🏼\",\n        [\"raised_hand_tone3\"] = \"✋🏽\",\n        [\"raised_hand_tone4\"] = \"✋🏾\",\n        [\"raised_hand_tone5\"] = \"✋🏿\",\n        [\"raised_back_of_hand\"] = \"🤚\",\n        [\"back_of_hand\"] = \"🤚\",\n        [\"raised_back_of_hand_tone1\"] = \"🤚🏻\",\n        [\"back_of_hand_tone1\"] = \"🤚🏻\",\n        [\"raised_back_of_hand_tone2\"] = \"🤚🏼\",\n        [\"back_of_hand_tone2\"] = \"🤚🏼\",\n        [\"raised_back_of_hand_tone3\"] = \"🤚🏽\",\n        [\"back_of_hand_tone3\"] = \"🤚🏽\",\n        [\"raised_back_of_hand_tone4\"] = \"🤚🏾\",\n        [\"back_of_hand_tone4\"] = \"🤚🏾\",\n        [\"raised_back_of_hand_tone5\"] = \"🤚🏿\",\n        [\"back_of_hand_tone5\"] = \"🤚🏿\",\n        [\"hand_splayed\"] = \"🖐️\",\n        [\"raised_hand_with_fingers_splayed\"] = \"🖐️\",\n        [\"hand_splayed_tone1\"] = \"🖐🏻\",\n        [\"raised_hand_with_fingers_splayed_tone1\"] = \"🖐🏻\",\n        [\"hand_splayed_tone2\"] = \"🖐🏼\",\n        [\"raised_hand_with_fingers_splayed_tone2\"] = \"🖐🏼\",\n        [\"hand_splayed_tone3\"] = \"🖐🏽\",\n        [\"raised_hand_with_fingers_splayed_tone3\"] = \"🖐🏽\",\n        [\"hand_splayed_tone4\"] = \"🖐🏾\",\n        [\"raised_hand_with_fingers_splayed_tone4\"] = \"🖐🏾\",\n        [\"hand_splayed_tone5\"] = \"🖐🏿\",\n        [\"raised_hand_with_fingers_splayed_tone5\"] = \"🖐🏿\",\n        [\"vulcan\"] = \"🖖\",\n        [\"raised_hand_with_part_between_middle_and_ring_fingers\"] = \"🖖\",\n        [\"vulcan_tone1\"] = \"🖖🏻\",\n        [\"raised_hand_with_part_between_middle_and_ring_fingers_tone1\"] = \"🖖🏻\",\n        [\"vulcan_tone2\"] = \"🖖🏼\",\n        [\"raised_hand_with_part_between_middle_and_ring_fingers_tone2\"] = \"🖖🏼\",\n        [\"vulcan_tone3\"] = \"🖖🏽\",\n        [\"raised_hand_with_part_between_middle_and_ring_fingers_tone3\"] = \"🖖🏽\",\n        [\"vulcan_tone4\"] = \"🖖🏾\",\n        [\"raised_hand_with_part_between_middle_and_ring_fingers_tone4\"] = \"🖖🏾\",\n        [\"vulcan_tone5\"] = \"🖖🏿\",\n        [\"raised_hand_with_part_between_middle_and_ring_fingers_tone5\"] = \"🖖🏿\",\n        [\"wave\"] = \"👋\",\n        [\"wave_tone1\"] = \"👋🏻\",\n        [\"wave_tone2\"] = \"👋🏼\",\n        [\"wave_tone3\"] = \"👋🏽\",\n        [\"wave_tone4\"] = \"👋🏾\",\n        [\"wave_tone5\"] = \"👋🏿\",\n        [\"call_me\"] = \"🤙\",\n        [\"call_me_hand\"] = \"🤙\",\n        [\"call_me_tone1\"] = \"🤙🏻\",\n        [\"call_me_hand_tone1\"] = \"🤙🏻\",\n        [\"call_me_tone2\"] = \"🤙🏼\",\n        [\"call_me_hand_tone2\"] = \"🤙🏼\",\n        [\"call_me_tone3\"] = \"🤙🏽\",\n        [\"call_me_hand_tone3\"] = \"🤙🏽\",\n        [\"call_me_tone4\"] = \"🤙🏾\",\n        [\"call_me_hand_tone4\"] = \"🤙🏾\",\n        [\"call_me_tone5\"] = \"🤙🏿\",\n        [\"call_me_hand_tone5\"] = \"🤙🏿\",\n        [\"muscle\"] = \"💪\",\n        [\"muscle_tone1\"] = \"💪🏻\",\n        [\"muscle_tone2\"] = \"💪🏼\",\n        [\"muscle_tone3\"] = \"💪🏽\",\n        [\"muscle_tone4\"] = \"💪🏾\",\n        [\"muscle_tone5\"] = \"💪🏿\",\n        [\"mechanical_arm\"] = \"🦾\",\n        [\"middle_finger\"] = \"🖕\",\n        [\"reversed_hand_with_middle_finger_extended\"] = \"🖕\",\n        [\"middle_finger_tone1\"] = \"🖕🏻\",\n        [\"reversed_hand_with_middle_finger_extended_tone1\"] = \"🖕🏻\",\n        [\"middle_finger_tone2\"] = \"🖕🏼\",\n        [\"reversed_hand_with_middle_finger_extended_tone2\"] = \"🖕🏼\",\n        [\"middle_finger_tone3\"] = \"🖕🏽\",\n        [\"reversed_hand_with_middle_finger_extended_tone3\"] = \"🖕🏽\",\n        [\"middle_finger_tone4\"] = \"🖕🏾\",\n        [\"reversed_hand_with_middle_finger_extended_tone4\"] = \"🖕🏾\",\n        [\"middle_finger_tone5\"] = \"🖕🏿\",\n        [\"reversed_hand_with_middle_finger_extended_tone5\"] = \"🖕🏿\",\n        [\"writing_hand\"] = \"✍️\",\n        [\"writing_hand_tone1\"] = \"✍🏻\",\n        [\"writing_hand_tone2\"] = \"✍🏼\",\n        [\"writing_hand_tone3\"] = \"✍🏽\",\n        [\"writing_hand_tone4\"] = \"✍🏾\",\n        [\"writing_hand_tone5\"] = \"✍🏿\",\n        [\"pray\"] = \"🙏\",\n        [\"pray_tone1\"] = \"🙏🏻\",\n        [\"pray_tone2\"] = \"🙏🏼\",\n        [\"pray_tone3\"] = \"🙏🏽\",\n        [\"pray_tone4\"] = \"🙏🏾\",\n        [\"pray_tone5\"] = \"🙏🏿\",\n        [\"foot\"] = \"🦶\",\n        [\"foot_tone1\"] = \"🦶🏻\",\n        [\"foot_light_skin_tone\"] = \"🦶🏻\",\n        [\"foot_tone2\"] = \"🦶🏼\",\n        [\"foot_medium_light_skin_tone\"] = \"🦶🏼\",\n        [\"foot_tone3\"] = \"🦶🏽\",\n        [\"foot_medium_skin_tone\"] = \"🦶🏽\",\n        [\"foot_tone4\"] = \"🦶🏾\",\n        [\"foot_medium_dark_skin_tone\"] = \"🦶🏾\",\n        [\"foot_tone5\"] = \"🦶🏿\",\n        [\"foot_dark_skin_tone\"] = \"🦶🏿\",\n        [\"leg\"] = \"🦵\",\n        [\"leg_tone1\"] = \"🦵🏻\",\n        [\"leg_light_skin_tone\"] = \"🦵🏻\",\n        [\"leg_tone2\"] = \"🦵🏼\",\n        [\"leg_medium_light_skin_tone\"] = \"🦵🏼\",\n        [\"leg_tone3\"] = \"🦵🏽\",\n        [\"leg_medium_skin_tone\"] = \"🦵🏽\",\n        [\"leg_tone4\"] = \"🦵🏾\",\n        [\"leg_medium_dark_skin_tone\"] = \"🦵🏾\",\n        [\"leg_tone5\"] = \"🦵🏿\",\n        [\"leg_dark_skin_tone\"] = \"🦵🏿\",\n        [\"mechanical_leg\"] = \"🦿\",\n        [\"lipstick\"] = \"💄\",\n        [\"kiss\"] = \"💋\",\n        [\"lips\"] = \"👄\",\n        [\"tooth\"] = \"🦷\",\n        [\"tongue\"] = \"👅\",\n        [\"ear\"] = \"👂\",\n        [\"ear_tone1\"] = \"👂🏻\",\n        [\"ear_tone2\"] = \"👂🏼\",\n        [\"ear_tone3\"] = \"👂🏽\",\n        [\"ear_tone4\"] = \"👂🏾\",\n        [\"ear_tone5\"] = \"👂🏿\",\n        [\"ear_with_hearing_aid\"] = \"🦻\",\n        [\"ear_with_hearing_aid_tone1\"] = \"🦻🏻\",\n        [\"ear_with_hearing_aid_light_skin_tone\"] = \"🦻🏻\",\n        [\"ear_with_hearing_aid_tone2\"] = \"🦻🏼\",\n        [\"ear_with_hearing_aid_medium_light_skin_tone\"] = \"🦻🏼\",\n        [\"ear_with_hearing_aid_tone3\"] = \"🦻🏽\",\n        [\"ear_with_hearing_aid_medium_skin_tone\"] = \"🦻🏽\",\n        [\"ear_with_hearing_aid_tone4\"] = \"🦻🏾\",\n        [\"ear_with_hearing_aid_medium_dark_skin_tone\"] = \"🦻🏾\",\n        [\"ear_with_hearing_aid_tone5\"] = \"🦻🏿\",\n        [\"ear_with_hearing_aid_dark_skin_tone\"] = \"🦻🏿\",\n        [\"nose\"] = \"👃\",\n        [\"nose_tone1\"] = \"👃🏻\",\n        [\"nose_tone2\"] = \"👃🏼\",\n        [\"nose_tone3\"] = \"👃🏽\",\n        [\"nose_tone4\"] = \"👃🏾\",\n        [\"nose_tone5\"] = \"👃🏿\",\n        [\"footprints\"] = \"👣\",\n        [\"eye\"] = \"👁️\",\n        [\"eyes\"] = \"👀\",\n        [\"brain\"] = \"🧠\",\n        [\"anatomical_heart\"] = \"🫀\",\n        [\"lungs\"] = \"🫁\",\n        [\"bone\"] = \"🦴\",\n        [\"speaking_head\"] = \"🗣️\",\n        [\"speaking_head_in_silhouette\"] = \"🗣️\",\n        [\"bust_in_silhouette\"] = \"👤\",\n        [\"busts_in_silhouette\"] = \"👥\",\n        [\"people_hugging\"] = \"🫂\",\n        [\"baby\"] = \"👶\",\n        [\"baby_tone1\"] = \"👶🏻\",\n        [\"baby_tone2\"] = \"👶🏼\",\n        [\"baby_tone3\"] = \"👶🏽\",\n        [\"baby_tone4\"] = \"👶🏾\",\n        [\"baby_tone5\"] = \"👶🏿\",\n        [\"girl\"] = \"👧\",\n        [\"girl_tone1\"] = \"👧🏻\",\n        [\"girl_tone2\"] = \"👧🏼\",\n        [\"girl_tone3\"] = \"👧🏽\",\n        [\"girl_tone4\"] = \"👧🏾\",\n        [\"girl_tone5\"] = \"👧🏿\",\n        [\"child\"] = \"🧒\",\n        [\"child_tone1\"] = \"🧒🏻\",\n        [\"child_light_skin_tone\"] = \"🧒🏻\",\n        [\"child_tone2\"] = \"🧒🏼\",\n        [\"child_medium_light_skin_tone\"] = \"🧒🏼\",\n        [\"child_tone3\"] = \"🧒🏽\",\n        [\"child_medium_skin_tone\"] = \"🧒🏽\",\n        [\"child_tone4\"] = \"🧒🏾\",\n        [\"child_medium_dark_skin_tone\"] = \"🧒🏾\",\n        [\"child_tone5\"] = \"🧒🏿\",\n        [\"child_dark_skin_tone\"] = \"🧒🏿\",\n        [\"boy\"] = \"👦\",\n        [\"boy_tone1\"] = \"👦🏻\",\n        [\"boy_tone2\"] = \"👦🏼\",\n        [\"boy_tone3\"] = \"👦🏽\",\n        [\"boy_tone4\"] = \"👦🏾\",\n        [\"boy_tone5\"] = \"👦🏿\",\n        [\"woman\"] = \"👩\",\n        [\"woman_tone1\"] = \"👩🏻\",\n        [\"woman_tone2\"] = \"👩🏼\",\n        [\"woman_tone3\"] = \"👩🏽\",\n        [\"woman_tone4\"] = \"👩🏾\",\n        [\"woman_tone5\"] = \"👩🏿\",\n        [\"adult\"] = \"🧑\",\n        [\"adult_tone1\"] = \"🧑🏻\",\n        [\"adult_light_skin_tone\"] = \"🧑🏻\",\n        [\"adult_tone2\"] = \"🧑🏼\",\n        [\"adult_medium_light_skin_tone\"] = \"🧑🏼\",\n        [\"adult_tone3\"] = \"🧑🏽\",\n        [\"adult_medium_skin_tone\"] = \"🧑🏽\",\n        [\"adult_tone4\"] = \"🧑🏾\",\n        [\"adult_medium_dark_skin_tone\"] = \"🧑🏾\",\n        [\"adult_tone5\"] = \"🧑🏿\",\n        [\"adult_dark_skin_tone\"] = \"🧑🏿\",\n        [\"man\"] = \"👨\",\n        [\"man_tone1\"] = \"👨🏻\",\n        [\"man_tone2\"] = \"👨🏼\",\n        [\"man_tone3\"] = \"👨🏽\",\n        [\"man_tone4\"] = \"👨🏾\",\n        [\"man_tone5\"] = \"👨🏿\",\n        [\"person_curly_hair\"] = \"🧑‍🦱\",\n        [\"person_tone1_curly_hair\"] = \"🧑🏻‍🦱\",\n        [\"person_light_skin_tone_curly_hair\"] = \"🧑🏻‍🦱\",\n        [\"person_tone2_curly_hair\"] = \"🧑🏼‍🦱\",\n        [\"person_medium_light_skin_tone_curly_hair\"] = \"🧑🏼‍🦱\",\n        [\"person_tone3_curly_hair\"] = \"🧑🏽‍🦱\",\n        [\"person_medium_skin_tone_curly_hair\"] = \"🧑🏽‍🦱\",\n        [\"person_tone4_curly_hair\"] = \"🧑🏾‍🦱\",\n        [\"person_medium_dark_skin_tone_curly_hair\"] = \"🧑🏾‍🦱\",\n        [\"person_tone5_curly_hair\"] = \"🧑🏿‍🦱\",\n        [\"person_dark_skin_tone_curly_hair\"] = \"🧑🏿‍🦱\",\n        [\"woman_curly_haired\"] = \"👩‍🦱\",\n        [\"woman_curly_haired_tone1\"] = \"👩🏻‍🦱\",\n        [\"woman_curly_haired_light_skin_tone\"] = \"👩🏻‍🦱\",\n        [\"woman_curly_haired_tone2\"] = \"👩🏼‍🦱\",\n        [\"woman_curly_haired_medium_light_skin_tone\"] = \"👩🏼‍🦱\",\n        [\"woman_curly_haired_tone3\"] = \"👩🏽‍🦱\",\n        [\"woman_curly_haired_medium_skin_tone\"] = \"👩🏽‍🦱\",\n        [\"woman_curly_haired_tone4\"] = \"👩🏾‍🦱\",\n        [\"woman_curly_haired_medium_dark_skin_tone\"] = \"👩🏾‍🦱\",\n        [\"woman_curly_haired_tone5\"] = \"👩🏿‍🦱\",\n        [\"woman_curly_haired_dark_skin_tone\"] = \"👩🏿‍🦱\",\n        [\"man_curly_haired\"] = \"👨‍🦱\",\n        [\"man_curly_haired_tone1\"] = \"👨🏻‍🦱\",\n        [\"man_curly_haired_light_skin_tone\"] = \"👨🏻‍🦱\",\n        [\"man_curly_haired_tone2\"] = \"👨🏼‍🦱\",\n        [\"man_curly_haired_medium_light_skin_tone\"] = \"👨🏼‍🦱\",\n        [\"man_curly_haired_tone3\"] = \"👨🏽‍🦱\",\n        [\"man_curly_haired_medium_skin_tone\"] = \"👨🏽‍🦱\",\n        [\"man_curly_haired_tone4\"] = \"👨🏾‍🦱\",\n        [\"man_curly_haired_medium_dark_skin_tone\"] = \"👨🏾‍🦱\",\n        [\"man_curly_haired_tone5\"] = \"👨🏿‍🦱\",\n        [\"man_curly_haired_dark_skin_tone\"] = \"👨🏿‍🦱\",\n        [\"person_red_hair\"] = \"🧑‍🦰\",\n        [\"person_tone1_red_hair\"] = \"🧑🏻‍🦰\",\n        [\"person_light_skin_tone_red_hair\"] = \"🧑🏻‍🦰\",\n        [\"person_tone2_red_hair\"] = \"🧑🏼‍🦰\",\n        [\"person_medium_light_skin_tone_red_hair\"] = \"🧑🏼‍🦰\",\n        [\"person_tone3_red_hair\"] = \"🧑🏽‍🦰\",\n        [\"person_medium_skin_tone_red_hair\"] = \"🧑🏽‍🦰\",\n        [\"person_tone4_red_hair\"] = \"🧑🏾‍🦰\",\n        [\"person_medium_dark_skin_tone_red_hair\"] = \"🧑🏾‍🦰\",\n        [\"person_tone5_red_hair\"] = \"🧑🏿‍🦰\",\n        [\"person_dark_skin_tone_red_hair\"] = \"🧑🏿‍🦰\",\n        [\"woman_red_haired\"] = \"👩‍🦰\",\n        [\"woman_red_haired_tone1\"] = \"👩🏻‍🦰\",\n        [\"woman_red_haired_light_skin_tone\"] = \"👩🏻‍🦰\",\n        [\"woman_red_haired_tone2\"] = \"👩🏼‍🦰\",\n        [\"woman_red_haired_medium_light_skin_tone\"] = \"👩🏼‍🦰\",\n        [\"woman_red_haired_tone3\"] = \"👩🏽‍🦰\",\n        [\"woman_red_haired_medium_skin_tone\"] = \"👩🏽‍🦰\",\n        [\"woman_red_haired_tone4\"] = \"👩🏾‍🦰\",\n        [\"woman_red_haired_medium_dark_skin_tone\"] = \"👩🏾‍🦰\",\n        [\"woman_red_haired_tone5\"] = \"👩🏿‍🦰\",\n        [\"woman_red_haired_dark_skin_tone\"] = \"👩🏿‍🦰\",\n        [\"man_red_haired\"] = \"👨‍🦰\",\n        [\"man_red_haired_tone1\"] = \"👨🏻‍🦰\",\n        [\"man_red_haired_light_skin_tone\"] = \"👨🏻‍🦰\",\n        [\"man_red_haired_tone2\"] = \"👨🏼‍🦰\",\n        [\"man_red_haired_medium_light_skin_tone\"] = \"👨🏼‍🦰\",\n        [\"man_red_haired_tone3\"] = \"👨🏽‍🦰\",\n        [\"man_red_haired_medium_skin_tone\"] = \"👨🏽‍🦰\",\n        [\"man_red_haired_tone4\"] = \"👨🏾‍🦰\",\n        [\"man_red_haired_medium_dark_skin_tone\"] = \"👨🏾‍🦰\",\n        [\"man_red_haired_tone5\"] = \"👨🏿‍🦰\",\n        [\"man_red_haired_dark_skin_tone\"] = \"👨🏿‍🦰\",\n        [\"blond_haired_woman\"] = \"👱‍♀️\",\n        [\"blond_haired_woman_tone1\"] = \"👱🏻‍♀️\",\n        [\"blond_haired_woman_light_skin_tone\"] = \"👱🏻‍♀️\",\n        [\"blond_haired_woman_tone2\"] = \"👱🏼‍♀️\",\n        [\"blond_haired_woman_medium_light_skin_tone\"] = \"👱🏼‍♀️\",\n        [\"blond_haired_woman_tone3\"] = \"👱🏽‍♀️\",\n        [\"blond_haired_woman_medium_skin_tone\"] = \"👱🏽‍♀️\",\n        [\"blond_haired_woman_tone4\"] = \"👱🏾‍♀️\",\n        [\"blond_haired_woman_medium_dark_skin_tone\"] = \"👱🏾‍♀️\",\n        [\"blond_haired_woman_tone5\"] = \"👱🏿‍♀️\",\n        [\"blond_haired_woman_dark_skin_tone\"] = \"👱🏿‍♀️\",\n        [\"blond_haired_person\"] = \"👱\",\n        [\"person_with_blond_hair\"] = \"👱\",\n        [\"blond_haired_person_tone1\"] = \"👱🏻\",\n        [\"person_with_blond_hair_tone1\"] = \"👱🏻\",\n        [\"blond_haired_person_tone2\"] = \"👱🏼\",\n        [\"person_with_blond_hair_tone2\"] = \"👱🏼\",\n        [\"blond_haired_person_tone3\"] = \"👱🏽\",\n        [\"person_with_blond_hair_tone3\"] = \"👱🏽\",\n        [\"blond_haired_person_tone4\"] = \"👱🏾\",\n        [\"person_with_blond_hair_tone4\"] = \"👱🏾\",\n        [\"blond_haired_person_tone5\"] = \"👱🏿\",\n        [\"person_with_blond_hair_tone5\"] = \"👱🏿\",\n        [\"blond_haired_man\"] = \"👱‍♂️\",\n        [\"blond_haired_man_tone1\"] = \"👱🏻‍♂️\",\n        [\"blond_haired_man_light_skin_tone\"] = \"👱🏻‍♂️\",\n        [\"blond_haired_man_tone2\"] = \"👱🏼‍♂️\",\n        [\"blond_haired_man_medium_light_skin_tone\"] = \"👱🏼‍♂️\",\n        [\"blond_haired_man_tone3\"] = \"👱🏽‍♂️\",\n        [\"blond_haired_man_medium_skin_tone\"] = \"👱🏽‍♂️\",\n        [\"blond_haired_man_tone4\"] = \"👱🏾‍♂️\",\n        [\"blond_haired_man_medium_dark_skin_tone\"] = \"👱🏾‍♂️\",\n        [\"blond_haired_man_tone5\"] = \"👱🏿‍♂️\",\n        [\"blond_haired_man_dark_skin_tone\"] = \"👱🏿‍♂️\",\n        [\"person_white_hair\"] = \"🧑‍🦳\",\n        [\"person_tone1_white_hair\"] = \"🧑🏻‍🦳\",\n        [\"person_light_skin_tone_white_hair\"] = \"🧑🏻‍🦳\",\n        [\"person_tone2_white_hair\"] = \"🧑🏼‍🦳\",\n        [\"person_medium_light_skin_tone_white_hair\"] = \"🧑🏼‍🦳\",\n        [\"person_tone3_white_hair\"] = \"🧑🏽‍🦳\",\n        [\"person_medium_skin_tone_white_hair\"] = \"🧑🏽‍🦳\",\n        [\"person_tone4_white_hair\"] = \"🧑🏾‍🦳\",\n        [\"person_medium_dark_skin_tone_white_hair\"] = \"🧑🏾‍🦳\",\n        [\"person_tone5_white_hair\"] = \"🧑🏿‍🦳\",\n        [\"person_dark_skin_tone_white_hair\"] = \"🧑🏿‍🦳\",\n        [\"woman_white_haired\"] = \"👩‍🦳\",\n        [\"woman_white_haired_tone1\"] = \"👩🏻‍🦳\",\n        [\"woman_white_haired_light_skin_tone\"] = \"👩🏻‍🦳\",\n        [\"woman_white_haired_tone2\"] = \"👩🏼‍🦳\",\n        [\"woman_white_haired_medium_light_skin_tone\"] = \"👩🏼‍🦳\",\n        [\"woman_white_haired_tone3\"] = \"👩🏽‍🦳\",\n        [\"woman_white_haired_medium_skin_tone\"] = \"👩🏽‍🦳\",\n        [\"woman_white_haired_tone4\"] = \"👩🏾‍🦳\",\n        [\"woman_white_haired_medium_dark_skin_tone\"] = \"👩🏾‍🦳\",\n        [\"woman_white_haired_tone5\"] = \"👩🏿‍🦳\",\n        [\"woman_white_haired_dark_skin_tone\"] = \"👩🏿‍🦳\",\n        [\"man_white_haired\"] = \"👨‍🦳\",\n        [\"man_white_haired_tone1\"] = \"👨🏻‍🦳\",\n        [\"man_white_haired_light_skin_tone\"] = \"👨🏻‍🦳\",\n        [\"man_white_haired_tone2\"] = \"👨🏼‍🦳\",\n        [\"man_white_haired_medium_light_skin_tone\"] = \"👨🏼‍🦳\",\n        [\"man_white_haired_tone3\"] = \"👨🏽‍🦳\",\n        [\"man_white_haired_medium_skin_tone\"] = \"👨🏽‍🦳\",\n        [\"man_white_haired_tone4\"] = \"👨🏾‍🦳\",\n        [\"man_white_haired_medium_dark_skin_tone\"] = \"👨🏾‍🦳\",\n        [\"man_white_haired_tone5\"] = \"👨🏿‍🦳\",\n        [\"man_white_haired_dark_skin_tone\"] = \"👨🏿‍🦳\",\n        [\"person_bald\"] = \"🧑‍🦲\",\n        [\"person_tone1_bald\"] = \"🧑🏻‍🦲\",\n        [\"person_light_skin_tone_bald\"] = \"🧑🏻‍🦲\",\n        [\"person_tone2_bald\"] = \"🧑🏼‍🦲\",\n        [\"person_medium_light_skin_tone_bald\"] = \"🧑🏼‍🦲\",\n        [\"person_tone3_bald\"] = \"🧑🏽‍🦲\",\n        [\"person_medium_skin_tone_bald\"] = \"🧑🏽‍🦲\",\n        [\"person_tone4_bald\"] = \"🧑🏾‍🦲\",\n        [\"person_medium_dark_skin_tone_bald\"] = \"🧑🏾‍🦲\",\n        [\"person_tone5_bald\"] = \"🧑🏿‍🦲\",\n        [\"person_dark_skin_tone_bald\"] = \"🧑🏿‍🦲\",\n        [\"woman_bald\"] = \"👩‍🦲\",\n        [\"woman_bald_tone1\"] = \"👩🏻‍🦲\",\n        [\"woman_bald_light_skin_tone\"] = \"👩🏻‍🦲\",\n        [\"woman_bald_tone2\"] = \"👩🏼‍🦲\",\n        [\"woman_bald_medium_light_skin_tone\"] = \"👩🏼‍🦲\",\n        [\"woman_bald_tone3\"] = \"👩🏽‍🦲\",\n        [\"woman_bald_medium_skin_tone\"] = \"👩🏽‍🦲\",\n        [\"woman_bald_tone4\"] = \"👩🏾‍🦲\",\n        [\"woman_bald_medium_dark_skin_tone\"] = \"👩🏾‍🦲\",\n        [\"woman_bald_tone5\"] = \"👩🏿‍🦲\",\n        [\"woman_bald_dark_skin_tone\"] = \"👩🏿‍🦲\",\n        [\"man_bald\"] = \"👨‍🦲\",\n        [\"man_bald_tone1\"] = \"👨🏻‍🦲\",\n        [\"man_bald_light_skin_tone\"] = \"👨🏻‍🦲\",\n        [\"man_bald_tone2\"] = \"👨🏼‍🦲\",\n        [\"man_bald_medium_light_skin_tone\"] = \"👨🏼‍🦲\",\n        [\"man_bald_tone3\"] = \"👨🏽‍🦲\",\n        [\"man_bald_medium_skin_tone\"] = \"👨🏽‍🦲\",\n        [\"man_bald_tone4\"] = \"👨🏾‍🦲\",\n        [\"man_bald_medium_dark_skin_tone\"] = \"👨🏾‍🦲\",\n        [\"man_bald_tone5\"] = \"👨🏿‍🦲\",\n        [\"man_bald_dark_skin_tone\"] = \"👨🏿‍🦲\",\n        [\"bearded_person\"] = \"🧔\",\n        [\"bearded_person_tone1\"] = \"🧔🏻\",\n        [\"bearded_person_light_skin_tone\"] = \"🧔🏻\",\n        [\"bearded_person_tone2\"] = \"🧔🏼\",\n        [\"bearded_person_medium_light_skin_tone\"] = \"🧔🏼\",\n        [\"bearded_person_tone3\"] = \"🧔🏽\",\n        [\"bearded_person_medium_skin_tone\"] = \"🧔🏽\",\n        [\"bearded_person_tone4\"] = \"🧔🏾\",\n        [\"bearded_person_medium_dark_skin_tone\"] = \"🧔🏾\",\n        [\"bearded_person_tone5\"] = \"🧔🏿\",\n        [\"bearded_person_dark_skin_tone\"] = \"🧔🏿\",\n        [\"man_beard\"] = \"🧔‍♂️\",\n        [\"man_tone1_beard\"] = \"🧔🏻‍♂️\",\n        [\"man_light_skin_tone_beard\"] = \"🧔🏻‍♂️\",\n        [\"man_tone2_beard\"] = \"🧔🏼‍♂️\",\n        [\"man_medium_light_skin_tone_beard\"] = \"🧔🏼‍♂️\",\n        [\"man_tone3_beard\"] = \"🧔🏽‍♂️\",\n        [\"man_medium_skin_tone_beard\"] = \"🧔🏽‍♂️\",\n        [\"man_tone4_beard\"] = \"🧔🏾‍♂️\",\n        [\"man_medium_dark_skin_tone_beard\"] = \"🧔🏾‍♂️\",\n        [\"man_tone5_beard\"] = \"🧔🏿‍♂️\",\n        [\"man_dark_skin_tone_beard\"] = \"🧔🏿‍♂️\",\n        [\"woman_beard\"] = \"🧔‍♀️\",\n        [\"woman_tone1_beard\"] = \"🧔🏻‍♀️\",\n        [\"woman_light_skin_tone_beard\"] = \"🧔🏻‍♀️\",\n        [\"woman_tone2_beard\"] = \"🧔🏼‍♀️\",\n        [\"woman_medium_light_skin_tone_beard\"] = \"🧔🏼‍♀️\",\n        [\"woman_tone3_beard\"] = \"🧔🏽‍♀️\",\n        [\"woman_medium_skin_tone_beard\"] = \"🧔🏽‍♀️\",\n        [\"woman_tone4_beard\"] = \"🧔🏾‍♀️\",\n        [\"woman_medium_dark_skin_tone_beard\"] = \"🧔🏾‍♀️\",\n        [\"woman_tone5_beard\"] = \"🧔🏿‍♀️\",\n        [\"woman_dark_skin_tone_beard\"] = \"🧔🏿‍♀️\",\n        [\"older_woman\"] = \"👵\",\n        [\"grandma\"] = \"👵\",\n        [\"older_woman_tone1\"] = \"👵🏻\",\n        [\"grandma_tone1\"] = \"👵🏻\",\n        [\"older_woman_tone2\"] = \"👵🏼\",\n        [\"grandma_tone2\"] = \"👵🏼\",\n        [\"older_woman_tone3\"] = \"👵🏽\",\n        [\"grandma_tone3\"] = \"👵🏽\",\n        [\"older_woman_tone4\"] = \"👵🏾\",\n        [\"grandma_tone4\"] = \"👵🏾\",\n        [\"older_woman_tone5\"] = \"👵🏿\",\n        [\"grandma_tone5\"] = \"👵🏿\",\n        [\"older_adult\"] = \"🧓\",\n        [\"older_adult_tone1\"] = \"🧓🏻\",\n        [\"older_adult_light_skin_tone\"] = \"🧓🏻\",\n        [\"older_adult_tone2\"] = \"🧓🏼\",\n        [\"older_adult_medium_light_skin_tone\"] = \"🧓🏼\",\n        [\"older_adult_tone3\"] = \"🧓🏽\",\n        [\"older_adult_medium_skin_tone\"] = \"🧓🏽\",\n        [\"older_adult_tone4\"] = \"🧓🏾\",\n        [\"older_adult_medium_dark_skin_tone\"] = \"🧓🏾\",\n        [\"older_adult_tone5\"] = \"🧓🏿\",\n        [\"older_adult_dark_skin_tone\"] = \"🧓🏿\",\n        [\"older_man\"] = \"👴\",\n        [\"older_man_tone1\"] = \"👴🏻\",\n        [\"older_man_tone2\"] = \"👴🏼\",\n        [\"older_man_tone3\"] = \"👴🏽\",\n        [\"older_man_tone4\"] = \"👴🏾\",\n        [\"older_man_tone5\"] = \"👴🏿\",\n        [\"man_with_chinese_cap\"] = \"👲\",\n        [\"man_with_gua_pi_mao\"] = \"👲\",\n        [\"man_with_chinese_cap_tone1\"] = \"👲🏻\",\n        [\"man_with_gua_pi_mao_tone1\"] = \"👲🏻\",\n        [\"man_with_chinese_cap_tone2\"] = \"👲🏼\",\n        [\"man_with_gua_pi_mao_tone2\"] = \"👲🏼\",\n        [\"man_with_chinese_cap_tone3\"] = \"👲🏽\",\n        [\"man_with_gua_pi_mao_tone3\"] = \"👲🏽\",\n        [\"man_with_chinese_cap_tone4\"] = \"👲🏾\",\n        [\"man_with_gua_pi_mao_tone4\"] = \"👲🏾\",\n        [\"man_with_chinese_cap_tone5\"] = \"👲🏿\",\n        [\"man_with_gua_pi_mao_tone5\"] = \"👲🏿\",\n        [\"person_wearing_turban\"] = \"👳\",\n        [\"man_with_turban\"] = \"👳\",\n        [\"person_wearing_turban_tone1\"] = \"👳🏻\",\n        [\"man_with_turban_tone1\"] = \"👳🏻\",\n        [\"person_wearing_turban_tone2\"] = \"👳🏼\",\n        [\"man_with_turban_tone2\"] = \"👳🏼\",\n        [\"person_wearing_turban_tone3\"] = \"👳🏽\",\n        [\"man_with_turban_tone3\"] = \"👳🏽\",\n        [\"person_wearing_turban_tone4\"] = \"👳🏾\",\n        [\"man_with_turban_tone4\"] = \"👳🏾\",\n        [\"person_wearing_turban_tone5\"] = \"👳🏿\",\n        [\"man_with_turban_tone5\"] = \"👳🏿\",\n        [\"woman_wearing_turban\"] = \"👳‍♀️\",\n        [\"woman_wearing_turban_tone1\"] = \"👳🏻‍♀️\",\n        [\"woman_wearing_turban_light_skin_tone\"] = \"👳🏻‍♀️\",\n        [\"woman_wearing_turban_tone2\"] = \"👳🏼‍♀️\",\n        [\"woman_wearing_turban_medium_light_skin_tone\"] = \"👳🏼‍♀️\",\n        [\"woman_wearing_turban_tone3\"] = \"👳🏽‍♀️\",\n        [\"woman_wearing_turban_medium_skin_tone\"] = \"👳🏽‍♀️\",\n        [\"woman_wearing_turban_tone4\"] = \"👳🏾‍♀️\",\n        [\"woman_wearing_turban_medium_dark_skin_tone\"] = \"👳🏾‍♀️\",\n        [\"woman_wearing_turban_tone5\"] = \"👳🏿‍♀️\",\n        [\"woman_wearing_turban_dark_skin_tone\"] = \"👳🏿‍♀️\",\n        [\"man_wearing_turban\"] = \"👳‍♂️\",\n        [\"man_wearing_turban_tone1\"] = \"👳🏻‍♂️\",\n        [\"man_wearing_turban_light_skin_tone\"] = \"👳🏻‍♂️\",\n        [\"man_wearing_turban_tone2\"] = \"👳🏼‍♂️\",\n        [\"man_wearing_turban_medium_light_skin_tone\"] = \"👳🏼‍♂️\",\n        [\"man_wearing_turban_tone3\"] = \"👳🏽‍♂️\",\n        [\"man_wearing_turban_medium_skin_tone\"] = \"👳🏽‍♂️\",\n        [\"man_wearing_turban_tone4\"] = \"👳🏾‍♂️\",\n        [\"man_wearing_turban_medium_dark_skin_tone\"] = \"👳🏾‍♂️\",\n        [\"man_wearing_turban_tone5\"] = \"👳🏿‍♂️\",\n        [\"man_wearing_turban_dark_skin_tone\"] = \"👳🏿‍♂️\",\n        [\"woman_with_headscarf\"] = \"🧕\",\n        [\"woman_with_headscarf_tone1\"] = \"🧕🏻\",\n        [\"woman_with_headscarf_light_skin_tone\"] = \"🧕🏻\",\n        [\"woman_with_headscarf_tone2\"] = \"🧕🏼\",\n        [\"woman_with_headscarf_medium_light_skin_tone\"] = \"🧕🏼\",\n        [\"woman_with_headscarf_tone3\"] = \"🧕🏽\",\n        [\"woman_with_headscarf_medium_skin_tone\"] = \"🧕🏽\",\n        [\"woman_with_headscarf_tone4\"] = \"🧕🏾\",\n        [\"woman_with_headscarf_medium_dark_skin_tone\"] = \"🧕🏾\",\n        [\"woman_with_headscarf_tone5\"] = \"🧕🏿\",\n        [\"woman_with_headscarf_dark_skin_tone\"] = \"🧕🏿\",\n        [\"police_officer\"] = \"👮\",\n        [\"cop\"] = \"👮\",\n        [\"police_officer_tone1\"] = \"👮🏻\",\n        [\"cop_tone1\"] = \"👮🏻\",\n        [\"police_officer_tone2\"] = \"👮🏼\",\n        [\"cop_tone2\"] = \"👮🏼\",\n        [\"police_officer_tone3\"] = \"👮🏽\",\n        [\"cop_tone3\"] = \"👮🏽\",\n        [\"police_officer_tone4\"] = \"👮🏾\",\n        [\"cop_tone4\"] = \"👮🏾\",\n        [\"police_officer_tone5\"] = \"👮🏿\",\n        [\"cop_tone5\"] = \"👮🏿\",\n        [\"woman_police_officer\"] = \"👮‍♀️\",\n        [\"woman_police_officer_tone1\"] = \"👮🏻‍♀️\",\n        [\"woman_police_officer_light_skin_tone\"] = \"👮🏻‍♀️\",\n        [\"woman_police_officer_tone2\"] = \"👮🏼‍♀️\",\n        [\"woman_police_officer_medium_light_skin_tone\"] = \"👮🏼‍♀️\",\n        [\"woman_police_officer_tone3\"] = \"👮🏽‍♀️\",\n        [\"woman_police_officer_medium_skin_tone\"] = \"👮🏽‍♀️\",\n        [\"woman_police_officer_tone4\"] = \"👮🏾‍♀️\",\n        [\"woman_police_officer_medium_dark_skin_tone\"] = \"👮🏾‍♀️\",\n        [\"woman_police_officer_tone5\"] = \"👮🏿‍♀️\",\n        [\"woman_police_officer_dark_skin_tone\"] = \"👮🏿‍♀️\",\n        [\"man_police_officer\"] = \"👮‍♂️\",\n        [\"man_police_officer_tone1\"] = \"👮🏻‍♂️\",\n        [\"man_police_officer_light_skin_tone\"] = \"👮🏻‍♂️\",\n        [\"man_police_officer_tone2\"] = \"👮🏼‍♂️\",\n        [\"man_police_officer_medium_light_skin_tone\"] = \"👮🏼‍♂️\",\n        [\"man_police_officer_tone3\"] = \"👮🏽‍♂️\",\n        [\"man_police_officer_medium_skin_tone\"] = \"👮🏽‍♂️\",\n        [\"man_police_officer_tone4\"] = \"👮🏾‍♂️\",\n        [\"man_police_officer_medium_dark_skin_tone\"] = \"👮🏾‍♂️\",\n        [\"man_police_officer_tone5\"] = \"👮🏿‍♂️\",\n        [\"man_police_officer_dark_skin_tone\"] = \"👮🏿‍♂️\",\n        [\"construction_worker\"] = \"👷\",\n        [\"construction_worker_tone1\"] = \"👷🏻\",\n        [\"construction_worker_tone2\"] = \"👷🏼\",\n        [\"construction_worker_tone3\"] = \"👷🏽\",\n        [\"construction_worker_tone4\"] = \"👷🏾\",\n        [\"construction_worker_tone5\"] = \"👷🏿\",\n        [\"woman_construction_worker\"] = \"👷‍♀️\",\n        [\"woman_construction_worker_tone1\"] = \"👷🏻‍♀️\",\n        [\"woman_construction_worker_light_skin_tone\"] = \"👷🏻‍♀️\",\n        [\"woman_construction_worker_tone2\"] = \"👷🏼‍♀️\",\n        [\"woman_construction_worker_medium_light_skin_tone\"] = \"👷🏼‍♀️\",\n        [\"woman_construction_worker_tone3\"] = \"👷🏽‍♀️\",\n        [\"woman_construction_worker_medium_skin_tone\"] = \"👷🏽‍♀️\",\n        [\"woman_construction_worker_tone4\"] = \"👷🏾‍♀️\",\n        [\"woman_construction_worker_medium_dark_skin_tone\"] = \"👷🏾‍♀️\",\n        [\"woman_construction_worker_tone5\"] = \"👷🏿‍♀️\",\n        [\"woman_construction_worker_dark_skin_tone\"] = \"👷🏿‍♀️\",\n        [\"man_construction_worker\"] = \"👷‍♂️\",\n        [\"man_construction_worker_tone1\"] = \"👷🏻‍♂️\",\n        [\"man_construction_worker_light_skin_tone\"] = \"👷🏻‍♂️\",\n        [\"man_construction_worker_tone2\"] = \"👷🏼‍♂️\",\n        [\"man_construction_worker_medium_light_skin_tone\"] = \"👷🏼‍♂️\",\n        [\"man_construction_worker_tone3\"] = \"👷🏽‍♂️\",\n        [\"man_construction_worker_medium_skin_tone\"] = \"👷🏽‍♂️\",\n        [\"man_construction_worker_tone4\"] = \"👷🏾‍♂️\",\n        [\"man_construction_worker_medium_dark_skin_tone\"] = \"👷🏾‍♂️\",\n        [\"man_construction_worker_tone5\"] = \"👷🏿‍♂️\",\n        [\"man_construction_worker_dark_skin_tone\"] = \"👷🏿‍♂️\",\n        [\"guard\"] = \"💂\",\n        [\"guardsman\"] = \"💂\",\n        [\"guard_tone1\"] = \"💂🏻\",\n        [\"guardsman_tone1\"] = \"💂🏻\",\n        [\"guard_tone2\"] = \"💂🏼\",\n        [\"guardsman_tone2\"] = \"💂🏼\",\n        [\"guard_tone3\"] = \"💂🏽\",\n        [\"guardsman_tone3\"] = \"💂🏽\",\n        [\"guard_tone4\"] = \"💂🏾\",\n        [\"guardsman_tone4\"] = \"💂🏾\",\n        [\"guard_tone5\"] = \"💂🏿\",\n        [\"guardsman_tone5\"] = \"💂🏿\",\n        [\"woman_guard\"] = \"💂‍♀️\",\n        [\"woman_guard_tone1\"] = \"💂🏻‍♀️\",\n        [\"woman_guard_light_skin_tone\"] = \"💂🏻‍♀️\",\n        [\"woman_guard_tone2\"] = \"💂🏼‍♀️\",\n        [\"woman_guard_medium_light_skin_tone\"] = \"💂🏼‍♀️\",\n        [\"woman_guard_tone3\"] = \"💂🏽‍♀️\",\n        [\"woman_guard_medium_skin_tone\"] = \"💂🏽‍♀️\",\n        [\"woman_guard_tone4\"] = \"💂🏾‍♀️\",\n        [\"woman_guard_medium_dark_skin_tone\"] = \"💂🏾‍♀️\",\n        [\"woman_guard_tone5\"] = \"💂🏿‍♀️\",\n        [\"woman_guard_dark_skin_tone\"] = \"💂🏿‍♀️\",\n        [\"man_guard\"] = \"💂‍♂️\",\n        [\"man_guard_tone1\"] = \"💂🏻‍♂️\",\n        [\"man_guard_light_skin_tone\"] = \"💂🏻‍♂️\",\n        [\"man_guard_tone2\"] = \"💂🏼‍♂️\",\n        [\"man_guard_medium_light_skin_tone\"] = \"💂🏼‍♂️\",\n        [\"man_guard_tone3\"] = \"💂🏽‍♂️\",\n        [\"man_guard_medium_skin_tone\"] = \"💂🏽‍♂️\",\n        [\"man_guard_tone4\"] = \"💂🏾‍♂️\",\n        [\"man_guard_medium_dark_skin_tone\"] = \"💂🏾‍♂️\",\n        [\"man_guard_tone5\"] = \"💂🏿‍♂️\",\n        [\"man_guard_dark_skin_tone\"] = \"💂🏿‍♂️\",\n        [\"detective\"] = \"🕵️\",\n        [\"spy\"] = \"🕵️\",\n        [\"sleuth_or_spy\"] = \"🕵️\",\n        [\"detective_tone1\"] = \"🕵🏻\",\n        [\"spy_tone1\"] = \"🕵🏻\",\n        [\"sleuth_or_spy_tone1\"] = \"🕵🏻\",\n        [\"detective_tone2\"] = \"🕵🏼\",\n        [\"spy_tone2\"] = \"🕵🏼\",\n        [\"sleuth_or_spy_tone2\"] = \"🕵🏼\",\n        [\"detective_tone3\"] = \"🕵🏽\",\n        [\"spy_tone3\"] = \"🕵🏽\",\n        [\"sleuth_or_spy_tone3\"] = \"🕵🏽\",\n        [\"detective_tone4\"] = \"🕵🏾\",\n        [\"spy_tone4\"] = \"🕵🏾\",\n        [\"sleuth_or_spy_tone4\"] = \"🕵🏾\",\n        [\"detective_tone5\"] = \"🕵🏿\",\n        [\"spy_tone5\"] = \"🕵🏿\",\n        [\"sleuth_or_spy_tone5\"] = \"🕵🏿\",\n        [\"woman_detective\"] = \"🕵️‍♀️\",\n        [\"woman_detective_tone1\"] = \"🕵🏻‍♀️\",\n        [\"woman_detective_light_skin_tone\"] = \"🕵🏻‍♀️\",\n        [\"woman_detective_tone2\"] = \"🕵🏼‍♀️\",\n        [\"woman_detective_medium_light_skin_tone\"] = \"🕵🏼‍♀️\",\n        [\"woman_detective_tone3\"] = \"🕵🏽‍♀️\",\n        [\"woman_detective_medium_skin_tone\"] = \"🕵🏽‍♀️\",\n        [\"woman_detective_tone4\"] = \"🕵🏾‍♀️\",\n        [\"woman_detective_medium_dark_skin_tone\"] = \"🕵🏾‍♀️\",\n        [\"woman_detective_tone5\"] = \"🕵🏿‍♀️\",\n        [\"woman_detective_dark_skin_tone\"] = \"🕵🏿‍♀️\",\n        [\"man_detective\"] = \"🕵️‍♂️\",\n        [\"man_detective_tone1\"] = \"🕵🏻‍♂️\",\n        [\"man_detective_light_skin_tone\"] = \"🕵🏻‍♂️\",\n        [\"man_detective_tone2\"] = \"🕵🏼‍♂️\",\n        [\"man_detective_medium_light_skin_tone\"] = \"🕵🏼‍♂️\",\n        [\"man_detective_tone3\"] = \"🕵🏽‍♂️\",\n        [\"man_detective_medium_skin_tone\"] = \"🕵🏽‍♂️\",\n        [\"man_detective_tone4\"] = \"🕵🏾‍♂️\",\n        [\"man_detective_medium_dark_skin_tone\"] = \"🕵🏾‍♂️\",\n        [\"man_detective_tone5\"] = \"🕵🏿‍♂️\",\n        [\"man_detective_dark_skin_tone\"] = \"🕵🏿‍♂️\",\n        [\"health_worker\"] = \"🧑‍⚕️\",\n        [\"health_worker_tone1\"] = \"🧑🏻‍⚕️\",\n        [\"health_worker_light_skin_tone\"] = \"🧑🏻‍⚕️\",\n        [\"health_worker_tone2\"] = \"🧑🏼‍⚕️\",\n        [\"health_worker_medium_light_skin_tone\"] = \"🧑🏼‍⚕️\",\n        [\"health_worker_tone3\"] = \"🧑🏽‍⚕️\",\n        [\"health_worker_medium_skin_tone\"] = \"🧑🏽‍⚕️\",\n        [\"health_worker_tone4\"] = \"🧑🏾‍⚕️\",\n        [\"health_worker_medium_dark_skin_tone\"] = \"🧑🏾‍⚕️\",\n        [\"health_worker_tone5\"] = \"🧑🏿‍⚕️\",\n        [\"health_worker_dark_skin_tone\"] = \"🧑🏿‍⚕️\",\n        [\"woman_health_worker\"] = \"👩‍⚕️\",\n        [\"woman_health_worker_tone1\"] = \"👩🏻‍⚕️\",\n        [\"woman_health_worker_light_skin_tone\"] = \"👩🏻‍⚕️\",\n        [\"woman_health_worker_tone2\"] = \"👩🏼‍⚕️\",\n        [\"woman_health_worker_medium_light_skin_tone\"] = \"👩🏼‍⚕️\",\n        [\"woman_health_worker_tone3\"] = \"👩🏽‍⚕️\",\n        [\"woman_health_worker_medium_skin_tone\"] = \"👩🏽‍⚕️\",\n        [\"woman_health_worker_tone4\"] = \"👩🏾‍⚕️\",\n        [\"woman_health_worker_medium_dark_skin_tone\"] = \"👩🏾‍⚕️\",\n        [\"woman_health_worker_tone5\"] = \"👩🏿‍⚕️\",\n        [\"woman_health_worker_dark_skin_tone\"] = \"👩🏿‍⚕️\",\n        [\"man_health_worker\"] = \"👨‍⚕️\",\n        [\"man_health_worker_tone1\"] = \"👨🏻‍⚕️\",\n        [\"man_health_worker_light_skin_tone\"] = \"👨🏻‍⚕️\",\n        [\"man_health_worker_tone2\"] = \"👨🏼‍⚕️\",\n        [\"man_health_worker_medium_light_skin_tone\"] = \"👨🏼‍⚕️\",\n        [\"man_health_worker_tone3\"] = \"👨🏽‍⚕️\",\n        [\"man_health_worker_medium_skin_tone\"] = \"👨🏽‍⚕️\",\n        [\"man_health_worker_tone4\"] = \"👨🏾‍⚕️\",\n        [\"man_health_worker_medium_dark_skin_tone\"] = \"👨🏾‍⚕️\",\n        [\"man_health_worker_tone5\"] = \"👨🏿‍⚕️\",\n        [\"man_health_worker_dark_skin_tone\"] = \"👨🏿‍⚕️\",\n        [\"farmer\"] = \"🧑‍🌾\",\n        [\"farmer_tone1\"] = \"🧑🏻‍🌾\",\n        [\"farmer_light_skin_tone\"] = \"🧑🏻‍🌾\",\n        [\"farmer_tone2\"] = \"🧑🏼‍🌾\",\n        [\"farmer_medium_light_skin_tone\"] = \"🧑🏼‍🌾\",\n        [\"farmer_tone3\"] = \"🧑🏽‍🌾\",\n        [\"farmer_medium_skin_tone\"] = \"🧑🏽‍🌾\",\n        [\"farmer_tone4\"] = \"🧑🏾‍🌾\",\n        [\"farmer_medium_dark_skin_tone\"] = \"🧑🏾‍🌾\",\n        [\"farmer_tone5\"] = \"🧑🏿‍🌾\",\n        [\"farmer_dark_skin_tone\"] = \"🧑🏿‍🌾\",\n        [\"woman_farmer\"] = \"👩‍🌾\",\n        [\"woman_farmer_tone1\"] = \"👩🏻‍🌾\",\n        [\"woman_farmer_light_skin_tone\"] = \"👩🏻‍🌾\",\n        [\"woman_farmer_tone2\"] = \"👩🏼‍🌾\",\n        [\"woman_farmer_medium_light_skin_tone\"] = \"👩🏼‍🌾\",\n        [\"woman_farmer_tone3\"] = \"👩🏽‍🌾\",\n        [\"woman_farmer_medium_skin_tone\"] = \"👩🏽‍🌾\",\n        [\"woman_farmer_tone4\"] = \"👩🏾‍🌾\",\n        [\"woman_farmer_medium_dark_skin_tone\"] = \"👩🏾‍🌾\",\n        [\"woman_farmer_tone5\"] = \"👩🏿‍🌾\",\n        [\"woman_farmer_dark_skin_tone\"] = \"👩🏿‍🌾\",\n        [\"man_farmer\"] = \"👨‍🌾\",\n        [\"man_farmer_tone1\"] = \"👨🏻‍🌾\",\n        [\"man_farmer_light_skin_tone\"] = \"👨🏻‍🌾\",\n        [\"man_farmer_tone2\"] = \"👨🏼‍🌾\",\n        [\"man_farmer_medium_light_skin_tone\"] = \"👨🏼‍🌾\",\n        [\"man_farmer_tone3\"] = \"👨🏽‍🌾\",\n        [\"man_farmer_medium_skin_tone\"] = \"👨🏽‍🌾\",\n        [\"man_farmer_tone4\"] = \"👨🏾‍🌾\",\n        [\"man_farmer_medium_dark_skin_tone\"] = \"👨🏾‍🌾\",\n        [\"man_farmer_tone5\"] = \"👨🏿‍🌾\",\n        [\"man_farmer_dark_skin_tone\"] = \"👨🏿‍🌾\",\n        [\"cook\"] = \"🧑‍🍳\",\n        [\"cook_tone1\"] = \"🧑🏻‍🍳\",\n        [\"cook_light_skin_tone\"] = \"🧑🏻‍🍳\",\n        [\"cook_tone2\"] = \"🧑🏼‍🍳\",\n        [\"cook_medium_light_skin_tone\"] = \"🧑🏼‍🍳\",\n        [\"cook_tone3\"] = \"🧑🏽‍🍳\",\n        [\"cook_medium_skin_tone\"] = \"🧑🏽‍🍳\",\n        [\"cook_tone4\"] = \"🧑🏾‍🍳\",\n        [\"cook_medium_dark_skin_tone\"] = \"🧑🏾‍🍳\",\n        [\"cook_tone5\"] = \"🧑🏿‍🍳\",\n        [\"cook_dark_skin_tone\"] = \"🧑🏿‍🍳\",\n        [\"woman_cook\"] = \"👩‍🍳\",\n        [\"woman_cook_tone1\"] = \"👩🏻‍🍳\",\n        [\"woman_cook_light_skin_tone\"] = \"👩🏻‍🍳\",\n        [\"woman_cook_tone2\"] = \"👩🏼‍🍳\",\n        [\"woman_cook_medium_light_skin_tone\"] = \"👩🏼‍🍳\",\n        [\"woman_cook_tone3\"] = \"👩🏽‍🍳\",\n        [\"woman_cook_medium_skin_tone\"] = \"👩🏽‍🍳\",\n        [\"woman_cook_tone4\"] = \"👩🏾‍🍳\",\n        [\"woman_cook_medium_dark_skin_tone\"] = \"👩🏾‍🍳\",\n        [\"woman_cook_tone5\"] = \"👩🏿‍🍳\",\n        [\"woman_cook_dark_skin_tone\"] = \"👩🏿‍🍳\",\n        [\"man_cook\"] = \"👨‍🍳\",\n        [\"man_cook_tone1\"] = \"👨🏻‍🍳\",\n        [\"man_cook_light_skin_tone\"] = \"👨🏻‍🍳\",\n        [\"man_cook_tone2\"] = \"👨🏼‍🍳\",\n        [\"man_cook_medium_light_skin_tone\"] = \"👨🏼‍🍳\",\n        [\"man_cook_tone3\"] = \"👨🏽‍🍳\",\n        [\"man_cook_medium_skin_tone\"] = \"👨🏽‍🍳\",\n        [\"man_cook_tone4\"] = \"👨🏾‍🍳\",\n        [\"man_cook_medium_dark_skin_tone\"] = \"👨🏾‍🍳\",\n        [\"man_cook_tone5\"] = \"👨🏿‍🍳\",\n        [\"man_cook_dark_skin_tone\"] = \"👨🏿‍🍳\",\n        [\"student\"] = \"🧑‍🎓\",\n        [\"student_tone1\"] = \"🧑🏻‍🎓\",\n        [\"student_light_skin_tone\"] = \"🧑🏻‍🎓\",\n        [\"student_tone2\"] = \"🧑🏼‍🎓\",\n        [\"student_medium_light_skin_tone\"] = \"🧑🏼‍🎓\",\n        [\"student_tone3\"] = \"🧑🏽‍🎓\",\n        [\"student_medium_skin_tone\"] = \"🧑🏽‍🎓\",\n        [\"student_tone4\"] = \"🧑🏾‍🎓\",\n        [\"student_medium_dark_skin_tone\"] = \"🧑🏾‍🎓\",\n        [\"student_tone5\"] = \"🧑🏿‍🎓\",\n        [\"student_dark_skin_tone\"] = \"🧑🏿‍🎓\",\n        [\"woman_student\"] = \"👩‍🎓\",\n        [\"woman_student_tone1\"] = \"👩🏻‍🎓\",\n        [\"woman_student_light_skin_tone\"] = \"👩🏻‍🎓\",\n        [\"woman_student_tone2\"] = \"👩🏼‍🎓\",\n        [\"woman_student_medium_light_skin_tone\"] = \"👩🏼‍🎓\",\n        [\"woman_student_tone3\"] = \"👩🏽‍🎓\",\n        [\"woman_student_medium_skin_tone\"] = \"👩🏽‍🎓\",\n        [\"woman_student_tone4\"] = \"👩🏾‍🎓\",\n        [\"woman_student_medium_dark_skin_tone\"] = \"👩🏾‍🎓\",\n        [\"woman_student_tone5\"] = \"👩🏿‍🎓\",\n        [\"woman_student_dark_skin_tone\"] = \"👩🏿‍🎓\",\n        [\"man_student\"] = \"👨‍🎓\",\n        [\"man_student_tone1\"] = \"👨🏻‍🎓\",\n        [\"man_student_light_skin_tone\"] = \"👨🏻‍🎓\",\n        [\"man_student_tone2\"] = \"👨🏼‍🎓\",\n        [\"man_student_medium_light_skin_tone\"] = \"👨🏼‍🎓\",\n        [\"man_student_tone3\"] = \"👨🏽‍🎓\",\n        [\"man_student_medium_skin_tone\"] = \"👨🏽‍🎓\",\n        [\"man_student_tone4\"] = \"👨🏾‍🎓\",\n        [\"man_student_medium_dark_skin_tone\"] = \"👨🏾‍🎓\",\n        [\"man_student_tone5\"] = \"👨🏿‍🎓\",\n        [\"man_student_dark_skin_tone\"] = \"👨🏿‍🎓\",\n        [\"singer\"] = \"🧑‍🎤\",\n        [\"singer_tone1\"] = \"🧑🏻‍🎤\",\n        [\"singer_light_skin_tone\"] = \"🧑🏻‍🎤\",\n        [\"singer_tone2\"] = \"🧑🏼‍🎤\",\n        [\"singer_medium_light_skin_tone\"] = \"🧑🏼‍🎤\",\n        [\"singer_tone3\"] = \"🧑🏽‍🎤\",\n        [\"singer_medium_skin_tone\"] = \"🧑🏽‍🎤\",\n        [\"singer_tone4\"] = \"🧑🏾‍🎤\",\n        [\"singer_medium_dark_skin_tone\"] = \"🧑🏾‍🎤\",\n        [\"singer_tone5\"] = \"🧑🏿‍🎤\",\n        [\"singer_dark_skin_tone\"] = \"🧑🏿‍🎤\",\n        [\"woman_singer\"] = \"👩‍🎤\",\n        [\"woman_singer_tone1\"] = \"👩🏻‍🎤\",\n        [\"woman_singer_light_skin_tone\"] = \"👩🏻‍🎤\",\n        [\"woman_singer_tone2\"] = \"👩🏼‍🎤\",\n        [\"woman_singer_medium_light_skin_tone\"] = \"👩🏼‍🎤\",\n        [\"woman_singer_tone3\"] = \"👩🏽‍🎤\",\n        [\"woman_singer_medium_skin_tone\"] = \"👩🏽‍🎤\",\n        [\"woman_singer_tone4\"] = \"👩🏾‍🎤\",\n        [\"woman_singer_medium_dark_skin_tone\"] = \"👩🏾‍🎤\",\n        [\"woman_singer_tone5\"] = \"👩🏿‍🎤\",\n        [\"woman_singer_dark_skin_tone\"] = \"👩🏿‍🎤\",\n        [\"man_singer\"] = \"👨‍🎤\",\n        [\"man_singer_tone1\"] = \"👨🏻‍🎤\",\n        [\"man_singer_light_skin_tone\"] = \"👨🏻‍🎤\",\n        [\"man_singer_tone2\"] = \"👨🏼‍🎤\",\n        [\"man_singer_medium_light_skin_tone\"] = \"👨🏼‍🎤\",\n        [\"man_singer_tone3\"] = \"👨🏽‍🎤\",\n        [\"man_singer_medium_skin_tone\"] = \"👨🏽‍🎤\",\n        [\"man_singer_tone4\"] = \"👨🏾‍🎤\",\n        [\"man_singer_medium_dark_skin_tone\"] = \"👨🏾‍🎤\",\n        [\"man_singer_tone5\"] = \"👨🏿‍🎤\",\n        [\"man_singer_dark_skin_tone\"] = \"👨🏿‍🎤\",\n        [\"teacher\"] = \"🧑‍🏫\",\n        [\"teacher_tone1\"] = \"🧑🏻‍🏫\",\n        [\"teacher_light_skin_tone\"] = \"🧑🏻‍🏫\",\n        [\"teacher_tone2\"] = \"🧑🏼‍🏫\",\n        [\"teacher_medium_light_skin_tone\"] = \"🧑🏼‍🏫\",\n        [\"teacher_tone3\"] = \"🧑🏽‍🏫\",\n        [\"teacher_medium_skin_tone\"] = \"🧑🏽‍🏫\",\n        [\"teacher_tone4\"] = \"🧑🏾‍🏫\",\n        [\"teacher_medium_dark_skin_tone\"] = \"🧑🏾‍🏫\",\n        [\"teacher_tone5\"] = \"🧑🏿‍🏫\",\n        [\"teacher_dark_skin_tone\"] = \"🧑🏿‍🏫\",\n        [\"woman_teacher\"] = \"👩‍🏫\",\n        [\"woman_teacher_tone1\"] = \"👩🏻‍🏫\",\n        [\"woman_teacher_light_skin_tone\"] = \"👩🏻‍🏫\",\n        [\"woman_teacher_tone2\"] = \"👩🏼‍🏫\",\n        [\"woman_teacher_medium_light_skin_tone\"] = \"👩🏼‍🏫\",\n        [\"woman_teacher_tone3\"] = \"👩🏽‍🏫\",\n        [\"woman_teacher_medium_skin_tone\"] = \"👩🏽‍🏫\",\n        [\"woman_teacher_tone4\"] = \"👩🏾‍🏫\",\n        [\"woman_teacher_medium_dark_skin_tone\"] = \"👩🏾‍🏫\",\n        [\"woman_teacher_tone5\"] = \"👩🏿‍🏫\",\n        [\"woman_teacher_dark_skin_tone\"] = \"👩🏿‍🏫\",\n        [\"man_teacher\"] = \"👨‍🏫\",\n        [\"man_teacher_tone1\"] = \"👨🏻‍🏫\",\n        [\"man_teacher_light_skin_tone\"] = \"👨🏻‍🏫\",\n        [\"man_teacher_tone2\"] = \"👨🏼‍🏫\",\n        [\"man_teacher_medium_light_skin_tone\"] = \"👨🏼‍🏫\",\n        [\"man_teacher_tone3\"] = \"👨🏽‍🏫\",\n        [\"man_teacher_medium_skin_tone\"] = \"👨🏽‍🏫\",\n        [\"man_teacher_tone4\"] = \"👨🏾‍🏫\",\n        [\"man_teacher_medium_dark_skin_tone\"] = \"👨🏾‍🏫\",\n        [\"man_teacher_tone5\"] = \"👨🏿‍🏫\",\n        [\"man_teacher_dark_skin_tone\"] = \"👨🏿‍🏫\",\n        [\"factory_worker\"] = \"🧑‍🏭\",\n        [\"factory_worker_tone1\"] = \"🧑🏻‍🏭\",\n        [\"factory_worker_light_skin_tone\"] = \"🧑🏻‍🏭\",\n        [\"factory_worker_tone2\"] = \"🧑🏼‍🏭\",\n        [\"factory_worker_medium_light_skin_tone\"] = \"🧑🏼‍🏭\",\n        [\"factory_worker_tone3\"] = \"🧑🏽‍🏭\",\n        [\"factory_worker_medium_skin_tone\"] = \"🧑🏽‍🏭\",\n        [\"factory_worker_tone4\"] = \"🧑🏾‍🏭\",\n        [\"factory_worker_medium_dark_skin_tone\"] = \"🧑🏾‍🏭\",\n        [\"factory_worker_tone5\"] = \"🧑🏿‍🏭\",\n        [\"factory_worker_dark_skin_tone\"] = \"🧑🏿‍🏭\",\n        [\"woman_factory_worker\"] = \"👩‍🏭\",\n        [\"woman_factory_worker_tone1\"] = \"👩🏻‍🏭\",\n        [\"woman_factory_worker_light_skin_tone\"] = \"👩🏻‍🏭\",\n        [\"woman_factory_worker_tone2\"] = \"👩🏼‍🏭\",\n        [\"woman_factory_worker_medium_light_skin_tone\"] = \"👩🏼‍🏭\",\n        [\"woman_factory_worker_tone3\"] = \"👩🏽‍🏭\",\n        [\"woman_factory_worker_medium_skin_tone\"] = \"👩🏽‍🏭\",\n        [\"woman_factory_worker_tone4\"] = \"👩🏾‍🏭\",\n        [\"woman_factory_worker_medium_dark_skin_tone\"] = \"👩🏾‍🏭\",\n        [\"woman_factory_worker_tone5\"] = \"👩🏿‍🏭\",\n        [\"woman_factory_worker_dark_skin_tone\"] = \"👩🏿‍🏭\",\n        [\"man_factory_worker\"] = \"👨‍🏭\",\n        [\"man_factory_worker_tone1\"] = \"👨🏻‍🏭\",\n        [\"man_factory_worker_light_skin_tone\"] = \"👨🏻‍🏭\",\n        [\"man_factory_worker_tone2\"] = \"👨🏼‍🏭\",\n        [\"man_factory_worker_medium_light_skin_tone\"] = \"👨🏼‍🏭\",\n        [\"man_factory_worker_tone3\"] = \"👨🏽‍🏭\",\n        [\"man_factory_worker_medium_skin_tone\"] = \"👨🏽‍🏭\",\n        [\"man_factory_worker_tone4\"] = \"👨🏾‍🏭\",\n        [\"man_factory_worker_medium_dark_skin_tone\"] = \"👨🏾‍🏭\",\n        [\"man_factory_worker_tone5\"] = \"👨🏿‍🏭\",\n        [\"man_factory_worker_dark_skin_tone\"] = \"👨🏿‍🏭\",\n        [\"technologist\"] = \"🧑‍💻\",\n        [\"technologist_tone1\"] = \"🧑🏻‍💻\",\n        [\"technologist_light_skin_tone\"] = \"🧑🏻‍💻\",\n        [\"technologist_tone2\"] = \"🧑🏼‍💻\",\n        [\"technologist_medium_light_skin_tone\"] = \"🧑🏼‍💻\",\n        [\"technologist_tone3\"] = \"🧑🏽‍💻\",\n        [\"technologist_medium_skin_tone\"] = \"🧑🏽‍💻\",\n        [\"technologist_tone4\"] = \"🧑🏾‍💻\",\n        [\"technologist_medium_dark_skin_tone\"] = \"🧑🏾‍💻\",\n        [\"technologist_tone5\"] = \"🧑🏿‍💻\",\n        [\"technologist_dark_skin_tone\"] = \"🧑🏿‍💻\",\n        [\"woman_technologist\"] = \"👩‍💻\",\n        [\"woman_technologist_tone1\"] = \"👩🏻‍💻\",\n        [\"woman_technologist_light_skin_tone\"] = \"👩🏻‍💻\",\n        [\"woman_technologist_tone2\"] = \"👩🏼‍💻\",\n        [\"woman_technologist_medium_light_skin_tone\"] = \"👩🏼‍💻\",\n        [\"woman_technologist_tone3\"] = \"👩🏽‍💻\",\n        [\"woman_technologist_medium_skin_tone\"] = \"👩🏽‍💻\",\n        [\"woman_technologist_tone4\"] = \"👩🏾‍💻\",\n        [\"woman_technologist_medium_dark_skin_tone\"] = \"👩🏾‍💻\",\n        [\"woman_technologist_tone5\"] = \"👩🏿‍💻\",\n        [\"woman_technologist_dark_skin_tone\"] = \"👩🏿‍💻\",\n        [\"man_technologist\"] = \"👨‍💻\",\n        [\"man_technologist_tone1\"] = \"👨🏻‍💻\",\n        [\"man_technologist_light_skin_tone\"] = \"👨🏻‍💻\",\n        [\"man_technologist_tone2\"] = \"👨🏼‍💻\",\n        [\"man_technologist_medium_light_skin_tone\"] = \"👨🏼‍💻\",\n        [\"man_technologist_tone3\"] = \"👨🏽‍💻\",\n        [\"man_technologist_medium_skin_tone\"] = \"👨🏽‍💻\",\n        [\"man_technologist_tone4\"] = \"👨🏾‍💻\",\n        [\"man_technologist_medium_dark_skin_tone\"] = \"👨🏾‍💻\",\n        [\"man_technologist_tone5\"] = \"👨🏿‍💻\",\n        [\"man_technologist_dark_skin_tone\"] = \"👨🏿‍💻\",\n        [\"office_worker\"] = \"🧑‍💼\",\n        [\"office_worker_tone1\"] = \"🧑🏻‍💼\",\n        [\"office_worker_light_skin_tone\"] = \"🧑🏻‍💼\",\n        [\"office_worker_tone2\"] = \"🧑🏼‍💼\",\n        [\"office_worker_medium_light_skin_tone\"] = \"🧑🏼‍💼\",\n        [\"office_worker_tone3\"] = \"🧑🏽‍💼\",\n        [\"office_worker_medium_skin_tone\"] = \"🧑🏽‍💼\",\n        [\"office_worker_tone4\"] = \"🧑🏾‍💼\",\n        [\"office_worker_medium_dark_skin_tone\"] = \"🧑🏾‍💼\",\n        [\"office_worker_tone5\"] = \"🧑🏿‍💼\",\n        [\"office_worker_dark_skin_tone\"] = \"🧑🏿‍💼\",\n        [\"woman_office_worker\"] = \"👩‍💼\",\n        [\"woman_office_worker_tone1\"] = \"👩🏻‍💼\",\n        [\"woman_office_worker_light_skin_tone\"] = \"👩🏻‍💼\",\n        [\"woman_office_worker_tone2\"] = \"👩🏼‍💼\",\n        [\"woman_office_worker_medium_light_skin_tone\"] = \"👩🏼‍💼\",\n        [\"woman_office_worker_tone3\"] = \"👩🏽‍💼\",\n        [\"woman_office_worker_medium_skin_tone\"] = \"👩🏽‍💼\",\n        [\"woman_office_worker_tone4\"] = \"👩🏾‍💼\",\n        [\"woman_office_worker_medium_dark_skin_tone\"] = \"👩🏾‍💼\",\n        [\"woman_office_worker_tone5\"] = \"👩🏿‍💼\",\n        [\"woman_office_worker_dark_skin_tone\"] = \"👩🏿‍💼\",\n        [\"man_office_worker\"] = \"👨‍💼\",\n        [\"man_office_worker_tone1\"] = \"👨🏻‍💼\",\n        [\"man_office_worker_light_skin_tone\"] = \"👨🏻‍💼\",\n        [\"man_office_worker_tone2\"] = \"👨🏼‍💼\",\n        [\"man_office_worker_medium_light_skin_tone\"] = \"👨🏼‍💼\",\n        [\"man_office_worker_tone3\"] = \"👨🏽‍💼\",\n        [\"man_office_worker_medium_skin_tone\"] = \"👨🏽‍💼\",\n        [\"man_office_worker_tone4\"] = \"👨🏾‍💼\",\n        [\"man_office_worker_medium_dark_skin_tone\"] = \"👨🏾‍💼\",\n        [\"man_office_worker_tone5\"] = \"👨🏿‍💼\",\n        [\"man_office_worker_dark_skin_tone\"] = \"👨🏿‍💼\",\n        [\"mechanic\"] = \"🧑‍🔧\",\n        [\"mechanic_tone1\"] = \"🧑🏻‍🔧\",\n        [\"mechanic_light_skin_tone\"] = \"🧑🏻‍🔧\",\n        [\"mechanic_tone2\"] = \"🧑🏼‍🔧\",\n        [\"mechanic_medium_light_skin_tone\"] = \"🧑🏼‍🔧\",\n        [\"mechanic_tone3\"] = \"🧑🏽‍🔧\",\n        [\"mechanic_medium_skin_tone\"] = \"🧑🏽‍🔧\",\n        [\"mechanic_tone4\"] = \"🧑🏾‍🔧\",\n        [\"mechanic_medium_dark_skin_tone\"] = \"🧑🏾‍🔧\",\n        [\"mechanic_tone5\"] = \"🧑🏿‍🔧\",\n        [\"mechanic_dark_skin_tone\"] = \"🧑🏿‍🔧\",\n        [\"woman_mechanic\"] = \"👩‍🔧\",\n        [\"woman_mechanic_tone1\"] = \"👩🏻‍🔧\",\n        [\"woman_mechanic_light_skin_tone\"] = \"👩🏻‍🔧\",\n        [\"woman_mechanic_tone2\"] = \"👩🏼‍🔧\",\n        [\"woman_mechanic_medium_light_skin_tone\"] = \"👩🏼‍🔧\",\n        [\"woman_mechanic_tone3\"] = \"👩🏽‍🔧\",\n        [\"woman_mechanic_medium_skin_tone\"] = \"👩🏽‍🔧\",\n        [\"woman_mechanic_tone4\"] = \"👩🏾‍🔧\",\n        [\"woman_mechanic_medium_dark_skin_tone\"] = \"👩🏾‍🔧\",\n        [\"woman_mechanic_tone5\"] = \"👩🏿‍🔧\",\n        [\"woman_mechanic_dark_skin_tone\"] = \"👩🏿‍🔧\",\n        [\"man_mechanic\"] = \"👨‍🔧\",\n        [\"man_mechanic_tone1\"] = \"👨🏻‍🔧\",\n        [\"man_mechanic_light_skin_tone\"] = \"👨🏻‍🔧\",\n        [\"man_mechanic_tone2\"] = \"👨🏼‍🔧\",\n        [\"man_mechanic_medium_light_skin_tone\"] = \"👨🏼‍🔧\",\n        [\"man_mechanic_tone3\"] = \"👨🏽‍🔧\",\n        [\"man_mechanic_medium_skin_tone\"] = \"👨🏽‍🔧\",\n        [\"man_mechanic_tone4\"] = \"👨🏾‍🔧\",\n        [\"man_mechanic_medium_dark_skin_tone\"] = \"👨🏾‍🔧\",\n        [\"man_mechanic_tone5\"] = \"👨🏿‍🔧\",\n        [\"man_mechanic_dark_skin_tone\"] = \"👨🏿‍🔧\",\n        [\"scientist\"] = \"🧑‍🔬\",\n        [\"scientist_tone1\"] = \"🧑🏻‍🔬\",\n        [\"scientist_light_skin_tone\"] = \"🧑🏻‍🔬\",\n        [\"scientist_tone2\"] = \"🧑🏼‍🔬\",\n        [\"scientist_medium_light_skin_tone\"] = \"🧑🏼‍🔬\",\n        [\"scientist_tone3\"] = \"🧑🏽‍🔬\",\n        [\"scientist_medium_skin_tone\"] = \"🧑🏽‍🔬\",\n        [\"scientist_tone4\"] = \"🧑🏾‍🔬\",\n        [\"scientist_medium_dark_skin_tone\"] = \"🧑🏾‍🔬\",\n        [\"scientist_tone5\"] = \"🧑🏿‍🔬\",\n        [\"scientist_dark_skin_tone\"] = \"🧑🏿‍🔬\",\n        [\"woman_scientist\"] = \"👩‍🔬\",\n        [\"woman_scientist_tone1\"] = \"👩🏻‍🔬\",\n        [\"woman_scientist_light_skin_tone\"] = \"👩🏻‍🔬\",\n        [\"woman_scientist_tone2\"] = \"👩🏼‍🔬\",\n        [\"woman_scientist_medium_light_skin_tone\"] = \"👩🏼‍🔬\",\n        [\"woman_scientist_tone3\"] = \"👩🏽‍🔬\",\n        [\"woman_scientist_medium_skin_tone\"] = \"👩🏽‍🔬\",\n        [\"woman_scientist_tone4\"] = \"👩🏾‍🔬\",\n        [\"woman_scientist_medium_dark_skin_tone\"] = \"👩🏾‍🔬\",\n        [\"woman_scientist_tone5\"] = \"👩🏿‍🔬\",\n        [\"woman_scientist_dark_skin_tone\"] = \"👩🏿‍🔬\",\n        [\"man_scientist\"] = \"👨‍🔬\",\n        [\"man_scientist_tone1\"] = \"👨🏻‍🔬\",\n        [\"man_scientist_light_skin_tone\"] = \"👨🏻‍🔬\",\n        [\"man_scientist_tone2\"] = \"👨🏼‍🔬\",\n        [\"man_scientist_medium_light_skin_tone\"] = \"👨🏼‍🔬\",\n        [\"man_scientist_tone3\"] = \"👨🏽‍🔬\",\n        [\"man_scientist_medium_skin_tone\"] = \"👨🏽‍🔬\",\n        [\"man_scientist_tone4\"] = \"👨🏾‍🔬\",\n        [\"man_scientist_medium_dark_skin_tone\"] = \"👨🏾‍🔬\",\n        [\"man_scientist_tone5\"] = \"👨🏿‍🔬\",\n        [\"man_scientist_dark_skin_tone\"] = \"👨🏿‍🔬\",\n        [\"artist\"] = \"🧑‍🎨\",\n        [\"artist_tone1\"] = \"🧑🏻‍🎨\",\n        [\"artist_light_skin_tone\"] = \"🧑🏻‍🎨\",\n        [\"artist_tone2\"] = \"🧑🏼‍🎨\",\n        [\"artist_medium_light_skin_tone\"] = \"🧑🏼‍🎨\",\n        [\"artist_tone3\"] = \"🧑🏽‍🎨\",\n        [\"artist_medium_skin_tone\"] = \"🧑🏽‍🎨\",\n        [\"artist_tone4\"] = \"🧑🏾‍🎨\",\n        [\"artist_medium_dark_skin_tone\"] = \"🧑🏾‍🎨\",\n        [\"artist_tone5\"] = \"🧑🏿‍🎨\",\n        [\"artist_dark_skin_tone\"] = \"🧑🏿‍🎨\",\n        [\"woman_artist\"] = \"👩‍🎨\",\n        [\"woman_artist_tone1\"] = \"👩🏻‍🎨\",\n        [\"woman_artist_light_skin_tone\"] = \"👩🏻‍🎨\",\n        [\"woman_artist_tone2\"] = \"👩🏼‍🎨\",\n        [\"woman_artist_medium_light_skin_tone\"] = \"👩🏼‍🎨\",\n        [\"woman_artist_tone3\"] = \"👩🏽‍🎨\",\n        [\"woman_artist_medium_skin_tone\"] = \"👩🏽‍🎨\",\n        [\"woman_artist_tone4\"] = \"👩🏾‍🎨\",\n        [\"woman_artist_medium_dark_skin_tone\"] = \"👩🏾‍🎨\",\n        [\"woman_artist_tone5\"] = \"👩🏿‍🎨\",\n        [\"woman_artist_dark_skin_tone\"] = \"👩🏿‍🎨\",\n        [\"man_artist\"] = \"👨‍🎨\",\n        [\"man_artist_tone1\"] = \"👨🏻‍🎨\",\n        [\"man_artist_light_skin_tone\"] = \"👨🏻‍🎨\",\n        [\"man_artist_tone2\"] = \"👨🏼‍🎨\",\n        [\"man_artist_medium_light_skin_tone\"] = \"👨🏼‍🎨\",\n        [\"man_artist_tone3\"] = \"👨🏽‍🎨\",\n        [\"man_artist_medium_skin_tone\"] = \"👨🏽‍🎨\",\n        [\"man_artist_tone4\"] = \"👨🏾‍🎨\",\n        [\"man_artist_medium_dark_skin_tone\"] = \"👨🏾‍🎨\",\n        [\"man_artist_tone5\"] = \"👨🏿‍🎨\",\n        [\"man_artist_dark_skin_tone\"] = \"👨🏿‍🎨\",\n        [\"firefighter\"] = \"🧑‍🚒\",\n        [\"firefighter_tone1\"] = \"🧑🏻‍🚒\",\n        [\"firefighter_light_skin_tone\"] = \"🧑🏻‍🚒\",\n        [\"firefighter_tone2\"] = \"🧑🏼‍🚒\",\n        [\"firefighter_medium_light_skin_tone\"] = \"🧑🏼‍🚒\",\n        [\"firefighter_tone3\"] = \"🧑🏽‍🚒\",\n        [\"firefighter_medium_skin_tone\"] = \"🧑🏽‍🚒\",\n        [\"firefighter_tone4\"] = \"🧑🏾‍🚒\",\n        [\"firefighter_medium_dark_skin_tone\"] = \"🧑🏾‍🚒\",\n        [\"firefighter_tone5\"] = \"🧑🏿‍🚒\",\n        [\"firefighter_dark_skin_tone\"] = \"🧑🏿‍🚒\",\n        [\"woman_firefighter\"] = \"👩‍🚒\",\n        [\"woman_firefighter_tone1\"] = \"👩🏻‍🚒\",\n        [\"woman_firefighter_light_skin_tone\"] = \"👩🏻‍🚒\",\n        [\"woman_firefighter_tone2\"] = \"👩🏼‍🚒\",\n        [\"woman_firefighter_medium_light_skin_tone\"] = \"👩🏼‍🚒\",\n        [\"woman_firefighter_tone3\"] = \"👩🏽‍🚒\",\n        [\"woman_firefighter_medium_skin_tone\"] = \"👩🏽‍🚒\",\n        [\"woman_firefighter_tone4\"] = \"👩🏾‍🚒\",\n        [\"woman_firefighter_medium_dark_skin_tone\"] = \"👩🏾‍🚒\",\n        [\"woman_firefighter_tone5\"] = \"👩🏿‍🚒\",\n        [\"woman_firefighter_dark_skin_tone\"] = \"👩🏿‍🚒\",\n        [\"man_firefighter\"] = \"👨‍🚒\",\n        [\"man_firefighter_tone1\"] = \"👨🏻‍🚒\",\n        [\"man_firefighter_light_skin_tone\"] = \"👨🏻‍🚒\",\n        [\"man_firefighter_tone2\"] = \"👨🏼‍🚒\",\n        [\"man_firefighter_medium_light_skin_tone\"] = \"👨🏼‍🚒\",\n        [\"man_firefighter_tone3\"] = \"👨🏽‍🚒\",\n        [\"man_firefighter_medium_skin_tone\"] = \"👨🏽‍🚒\",\n        [\"man_firefighter_tone4\"] = \"👨🏾‍🚒\",\n        [\"man_firefighter_medium_dark_skin_tone\"] = \"👨🏾‍🚒\",\n        [\"man_firefighter_tone5\"] = \"👨🏿‍🚒\",\n        [\"man_firefighter_dark_skin_tone\"] = \"👨🏿‍🚒\",\n        [\"pilot\"] = \"🧑‍✈️\",\n        [\"pilot_tone1\"] = \"🧑🏻‍✈️\",\n        [\"pilot_light_skin_tone\"] = \"🧑🏻‍✈️\",\n        [\"pilot_tone2\"] = \"🧑🏼‍✈️\",\n        [\"pilot_medium_light_skin_tone\"] = \"🧑🏼‍✈️\",\n        [\"pilot_tone3\"] = \"🧑🏽‍✈️\",\n        [\"pilot_medium_skin_tone\"] = \"🧑🏽‍✈️\",\n        [\"pilot_tone4\"] = \"🧑🏾‍✈️\",\n        [\"pilot_medium_dark_skin_tone\"] = \"🧑🏾‍✈️\",\n        [\"pilot_tone5\"] = \"🧑🏿‍✈️\",\n        [\"pilot_dark_skin_tone\"] = \"🧑🏿‍✈️\",\n        [\"woman_pilot\"] = \"👩‍✈️\",\n        [\"woman_pilot_tone1\"] = \"👩🏻‍✈️\",\n        [\"woman_pilot_light_skin_tone\"] = \"👩🏻‍✈️\",\n        [\"woman_pilot_tone2\"] = \"👩🏼‍✈️\",\n        [\"woman_pilot_medium_light_skin_tone\"] = \"👩🏼‍✈️\",\n        [\"woman_pilot_tone3\"] = \"👩🏽‍✈️\",\n        [\"woman_pilot_medium_skin_tone\"] = \"👩🏽‍✈️\",\n        [\"woman_pilot_tone4\"] = \"👩🏾‍✈️\",\n        [\"woman_pilot_medium_dark_skin_tone\"] = \"👩🏾‍✈️\",\n        [\"woman_pilot_tone5\"] = \"👩🏿‍✈️\",\n        [\"woman_pilot_dark_skin_tone\"] = \"👩🏿‍✈️\",\n        [\"man_pilot\"] = \"👨‍✈️\",\n        [\"man_pilot_tone1\"] = \"👨🏻‍✈️\",\n        [\"man_pilot_light_skin_tone\"] = \"👨🏻‍✈️\",\n        [\"man_pilot_tone2\"] = \"👨🏼‍✈️\",\n        [\"man_pilot_medium_light_skin_tone\"] = \"👨🏼‍✈️\",\n        [\"man_pilot_tone3\"] = \"👨🏽‍✈️\",\n        [\"man_pilot_medium_skin_tone\"] = \"👨🏽‍✈️\",\n        [\"man_pilot_tone4\"] = \"👨🏾‍✈️\",\n        [\"man_pilot_medium_dark_skin_tone\"] = \"👨🏾‍✈️\",\n        [\"man_pilot_tone5\"] = \"👨🏿‍✈️\",\n        [\"man_pilot_dark_skin_tone\"] = \"👨🏿‍✈️\",\n        [\"astronaut\"] = \"🧑‍🚀\",\n        [\"astronaut_tone1\"] = \"🧑🏻‍🚀\",\n        [\"astronaut_light_skin_tone\"] = \"🧑🏻‍🚀\",\n        [\"astronaut_tone2\"] = \"🧑🏼‍🚀\",\n        [\"astronaut_medium_light_skin_tone\"] = \"🧑🏼‍🚀\",\n        [\"astronaut_tone3\"] = \"🧑🏽‍🚀\",\n        [\"astronaut_medium_skin_tone\"] = \"🧑🏽‍🚀\",\n        [\"astronaut_tone4\"] = \"🧑🏾‍🚀\",\n        [\"astronaut_medium_dark_skin_tone\"] = \"🧑🏾‍🚀\",\n        [\"astronaut_tone5\"] = \"🧑🏿‍🚀\",\n        [\"astronaut_dark_skin_tone\"] = \"🧑🏿‍🚀\",\n        [\"woman_astronaut\"] = \"👩‍🚀\",\n        [\"woman_astronaut_tone1\"] = \"👩🏻‍🚀\",\n        [\"woman_astronaut_light_skin_tone\"] = \"👩🏻‍🚀\",\n        [\"woman_astronaut_tone2\"] = \"👩🏼‍🚀\",\n        [\"woman_astronaut_medium_light_skin_tone\"] = \"👩🏼‍🚀\",\n        [\"woman_astronaut_tone3\"] = \"👩🏽‍🚀\",\n        [\"woman_astronaut_medium_skin_tone\"] = \"👩🏽‍🚀\",\n        [\"woman_astronaut_tone4\"] = \"👩🏾‍🚀\",\n        [\"woman_astronaut_medium_dark_skin_tone\"] = \"👩🏾‍🚀\",\n        [\"woman_astronaut_tone5\"] = \"👩🏿‍🚀\",\n        [\"woman_astronaut_dark_skin_tone\"] = \"👩🏿‍🚀\",\n        [\"man_astronaut\"] = \"👨‍🚀\",\n        [\"man_astronaut_tone1\"] = \"👨🏻‍🚀\",\n        [\"man_astronaut_light_skin_tone\"] = \"👨🏻‍🚀\",\n        [\"man_astronaut_tone2\"] = \"👨🏼‍🚀\",\n        [\"man_astronaut_medium_light_skin_tone\"] = \"👨🏼‍🚀\",\n        [\"man_astronaut_tone3\"] = \"👨🏽‍🚀\",\n        [\"man_astronaut_medium_skin_tone\"] = \"👨🏽‍🚀\",\n        [\"man_astronaut_tone4\"] = \"👨🏾‍🚀\",\n        [\"man_astronaut_medium_dark_skin_tone\"] = \"👨🏾‍🚀\",\n        [\"man_astronaut_tone5\"] = \"👨🏿‍🚀\",\n        [\"man_astronaut_dark_skin_tone\"] = \"👨🏿‍🚀\",\n        [\"judge\"] = \"🧑‍⚖️\",\n        [\"judge_tone1\"] = \"🧑🏻‍⚖️\",\n        [\"judge_light_skin_tone\"] = \"🧑🏻‍⚖️\",\n        [\"judge_tone2\"] = \"🧑🏼‍⚖️\",\n        [\"judge_medium_light_skin_tone\"] = \"🧑🏼‍⚖️\",\n        [\"judge_tone3\"] = \"🧑🏽‍⚖️\",\n        [\"judge_medium_skin_tone\"] = \"🧑🏽‍⚖️\",\n        [\"judge_tone4\"] = \"🧑🏾‍⚖️\",\n        [\"judge_medium_dark_skin_tone\"] = \"🧑🏾‍⚖️\",\n        [\"judge_tone5\"] = \"🧑🏿‍⚖️\",\n        [\"judge_dark_skin_tone\"] = \"🧑🏿‍⚖️\",\n        [\"woman_judge\"] = \"👩‍⚖️\",\n        [\"woman_judge_tone1\"] = \"👩🏻‍⚖️\",\n        [\"woman_judge_light_skin_tone\"] = \"👩🏻‍⚖️\",\n        [\"woman_judge_tone2\"] = \"👩🏼‍⚖️\",\n        [\"woman_judge_medium_light_skin_tone\"] = \"👩🏼‍⚖️\",\n        [\"woman_judge_tone3\"] = \"👩🏽‍⚖️\",\n        [\"woman_judge_medium_skin_tone\"] = \"👩🏽‍⚖️\",\n        [\"woman_judge_tone4\"] = \"👩🏾‍⚖️\",\n        [\"woman_judge_medium_dark_skin_tone\"] = \"👩🏾‍⚖️\",\n        [\"woman_judge_tone5\"] = \"👩🏿‍⚖️\",\n        [\"woman_judge_dark_skin_tone\"] = \"👩🏿‍⚖️\",\n        [\"man_judge\"] = \"👨‍⚖️\",\n        [\"man_judge_tone1\"] = \"👨🏻‍⚖️\",\n        [\"man_judge_light_skin_tone\"] = \"👨🏻‍⚖️\",\n        [\"man_judge_tone2\"] = \"👨🏼‍⚖️\",\n        [\"man_judge_medium_light_skin_tone\"] = \"👨🏼‍⚖️\",\n        [\"man_judge_tone3\"] = \"👨🏽‍⚖️\",\n        [\"man_judge_medium_skin_tone\"] = \"👨🏽‍⚖️\",\n        [\"man_judge_tone4\"] = \"👨🏾‍⚖️\",\n        [\"man_judge_medium_dark_skin_tone\"] = \"👨🏾‍⚖️\",\n        [\"man_judge_tone5\"] = \"👨🏿‍⚖️\",\n        [\"man_judge_dark_skin_tone\"] = \"👨🏿‍⚖️\",\n        [\"person_with_veil\"] = \"👰\",\n        [\"person_with_veil_tone1\"] = \"👰🏻\",\n        [\"person_with_veil_tone2\"] = \"👰🏼\",\n        [\"person_with_veil_tone3\"] = \"👰🏽\",\n        [\"person_with_veil_tone4\"] = \"👰🏾\",\n        [\"person_with_veil_tone5\"] = \"👰🏿\",\n        [\"woman_with_veil\"] = \"👰‍♀️\",\n        [\"bride_with_veil\"] = \"👰‍♀️\",\n        [\"woman_with_veil_tone1\"] = \"👰🏻‍♀️\",\n        [\"woman_with_veil_light_skin_tone\"] = \"👰🏻‍♀️\",\n        [\"woman_with_veil_tone2\"] = \"👰🏼‍♀️\",\n        [\"woman_with_veil_medium_light_skin_tone\"] = \"👰🏼‍♀️\",\n        [\"woman_with_veil_tone3\"] = \"👰🏽‍♀️\",\n        [\"woman_with_veil_medium_skin_tone\"] = \"👰🏽‍♀️\",\n        [\"woman_with_veil_tone4\"] = \"👰🏾‍♀️\",\n        [\"woman_with_veil_medium_dark_skin_tone\"] = \"👰🏾‍♀️\",\n        [\"woman_with_veil_tone5\"] = \"👰🏿‍♀️\",\n        [\"woman_with_veil_dark_skin_tone\"] = \"👰🏿‍♀️\",\n        [\"man_with_veil\"] = \"👰‍♂️\",\n        [\"man_with_veil_tone1\"] = \"👰🏻‍♂️\",\n        [\"man_with_veil_light_skin_tone\"] = \"👰🏻‍♂️\",\n        [\"man_with_veil_tone2\"] = \"👰🏼‍♂️\",\n        [\"man_with_veil_medium_light_skin_tone\"] = \"👰🏼‍♂️\",\n        [\"man_with_veil_tone3\"] = \"👰🏽‍♂️\",\n        [\"man_with_veil_medium_skin_tone\"] = \"👰🏽‍♂️\",\n        [\"man_with_veil_tone4\"] = \"👰🏾‍♂️\",\n        [\"man_with_veil_medium_dark_skin_tone\"] = \"👰🏾‍♂️\",\n        [\"man_with_veil_tone5\"] = \"👰🏿‍♂️\",\n        [\"man_with_veil_dark_skin_tone\"] = \"👰🏿‍♂️\",\n        [\"person_in_tuxedo\"] = \"🤵\",\n        [\"person_in_tuxedo_tone1\"] = \"🤵🏻\",\n        [\"tuxedo_tone1\"] = \"🤵🏻\",\n        [\"person_in_tuxedo_tone2\"] = \"🤵🏼\",\n        [\"tuxedo_tone2\"] = \"🤵🏼\",\n        [\"person_in_tuxedo_tone3\"] = \"🤵🏽\",\n        [\"tuxedo_tone3\"] = \"🤵🏽\",\n        [\"person_in_tuxedo_tone4\"] = \"🤵🏾\",\n        [\"tuxedo_tone4\"] = \"🤵🏾\",\n        [\"person_in_tuxedo_tone5\"] = \"🤵🏿\",\n        [\"tuxedo_tone5\"] = \"🤵🏿\",\n        [\"woman_in_tuxedo\"] = \"🤵‍♀️\",\n        [\"woman_in_tuxedo_tone1\"] = \"🤵🏻‍♀️\",\n        [\"woman_in_tuxedo_light_skin_tone\"] = \"🤵🏻‍♀️\",\n        [\"woman_in_tuxedo_tone2\"] = \"🤵🏼‍♀️\",\n        [\"woman_in_tuxedo_medium_light_skin_tone\"] = \"🤵🏼‍♀️\",\n        [\"woman_in_tuxedo_tone3\"] = \"🤵🏽‍♀️\",\n        [\"woman_in_tuxedo_medium_skin_tone\"] = \"🤵🏽‍♀️\",\n        [\"woman_in_tuxedo_tone4\"] = \"🤵🏾‍♀️\",\n        [\"woman_in_tuxedo_medium_dark_skin_tone\"] = \"🤵🏾‍♀️\",\n        [\"woman_in_tuxedo_tone5\"] = \"🤵🏿‍♀️\",\n        [\"woman_in_tuxedo_dark_skin_tone\"] = \"🤵🏿‍♀️\",\n        [\"man_in_tuxedo\"] = \"🤵‍♂️\",\n        [\"man_in_tuxedo_tone1\"] = \"🤵🏻‍♂️\",\n        [\"man_in_tuxedo_light_skin_tone\"] = \"🤵🏻‍♂️\",\n        [\"man_in_tuxedo_tone2\"] = \"🤵🏼‍♂️\",\n        [\"man_in_tuxedo_medium_light_skin_tone\"] = \"🤵🏼‍♂️\",\n        [\"man_in_tuxedo_tone3\"] = \"🤵🏽‍♂️\",\n        [\"man_in_tuxedo_medium_skin_tone\"] = \"🤵🏽‍♂️\",\n        [\"man_in_tuxedo_tone4\"] = \"🤵🏾‍♂️\",\n        [\"man_in_tuxedo_medium_dark_skin_tone\"] = \"🤵🏾‍♂️\",\n        [\"man_in_tuxedo_tone5\"] = \"🤵🏿‍♂️\",\n        [\"man_in_tuxedo_dark_skin_tone\"] = \"🤵🏿‍♂️\",\n        [\"princess\"] = \"👸\",\n        [\"princess_tone1\"] = \"👸🏻\",\n        [\"princess_tone2\"] = \"👸🏼\",\n        [\"princess_tone3\"] = \"👸🏽\",\n        [\"princess_tone4\"] = \"👸🏾\",\n        [\"princess_tone5\"] = \"👸🏿\",\n        [\"prince\"] = \"🤴\",\n        [\"prince_tone1\"] = \"🤴🏻\",\n        [\"prince_tone2\"] = \"🤴🏼\",\n        [\"prince_tone3\"] = \"🤴🏽\",\n        [\"prince_tone4\"] = \"🤴🏾\",\n        [\"prince_tone5\"] = \"🤴🏿\",\n        [\"superhero\"] = \"🦸\",\n        [\"superhero_tone1\"] = \"🦸🏻\",\n        [\"superhero_light_skin_tone\"] = \"🦸🏻\",\n        [\"superhero_tone2\"] = \"🦸🏼\",\n        [\"superhero_medium_light_skin_tone\"] = \"🦸🏼\",\n        [\"superhero_tone3\"] = \"🦸🏽\",\n        [\"superhero_medium_skin_tone\"] = \"🦸🏽\",\n        [\"superhero_tone4\"] = \"🦸🏾\",\n        [\"superhero_medium_dark_skin_tone\"] = \"🦸🏾\",\n        [\"superhero_tone5\"] = \"🦸🏿\",\n        [\"superhero_dark_skin_tone\"] = \"🦸🏿\",\n        [\"woman_superhero\"] = \"🦸‍♀️\",\n        [\"woman_superhero_tone1\"] = \"🦸🏻‍♀️\",\n        [\"woman_superhero_light_skin_tone\"] = \"🦸🏻‍♀️\",\n        [\"woman_superhero_tone2\"] = \"🦸🏼‍♀️\",\n        [\"woman_superhero_medium_light_skin_tone\"] = \"🦸🏼‍♀️\",\n        [\"woman_superhero_tone3\"] = \"🦸🏽‍♀️\",\n        [\"woman_superhero_medium_skin_tone\"] = \"🦸🏽‍♀️\",\n        [\"woman_superhero_tone4\"] = \"🦸🏾‍♀️\",\n        [\"woman_superhero_medium_dark_skin_tone\"] = \"🦸🏾‍♀️\",\n        [\"woman_superhero_tone5\"] = \"🦸🏿‍♀️\",\n        [\"woman_superhero_dark_skin_tone\"] = \"🦸🏿‍♀️\",\n        [\"man_superhero\"] = \"🦸‍♂️\",\n        [\"man_superhero_tone1\"] = \"🦸🏻‍♂️\",\n        [\"man_superhero_light_skin_tone\"] = \"🦸🏻‍♂️\",\n        [\"man_superhero_tone2\"] = \"🦸🏼‍♂️\",\n        [\"man_superhero_medium_light_skin_tone\"] = \"🦸🏼‍♂️\",\n        [\"man_superhero_tone3\"] = \"🦸🏽‍♂️\",\n        [\"man_superhero_medium_skin_tone\"] = \"🦸🏽‍♂️\",\n        [\"man_superhero_tone4\"] = \"🦸🏾‍♂️\",\n        [\"man_superhero_medium_dark_skin_tone\"] = \"🦸🏾‍♂️\",\n        [\"man_superhero_tone5\"] = \"🦸🏿‍♂️\",\n        [\"man_superhero_dark_skin_tone\"] = \"🦸🏿‍♂️\",\n        [\"supervillain\"] = \"🦹\",\n        [\"supervillain_tone1\"] = \"🦹🏻\",\n        [\"supervillain_light_skin_tone\"] = \"🦹🏻\",\n        [\"supervillain_tone2\"] = \"🦹🏼\",\n        [\"supervillain_medium_light_skin_tone\"] = \"🦹🏼\",\n        [\"supervillain_tone3\"] = \"🦹🏽\",\n        [\"supervillain_medium_skin_tone\"] = \"🦹🏽\",\n        [\"supervillain_tone4\"] = \"🦹🏾\",\n        [\"supervillain_medium_dark_skin_tone\"] = \"🦹🏾\",\n        [\"supervillain_tone5\"] = \"🦹🏿\",\n        [\"supervillain_dark_skin_tone\"] = \"🦹🏿\",\n        [\"woman_supervillain\"] = \"🦹‍♀️\",\n        [\"woman_supervillain_tone1\"] = \"🦹🏻‍♀️\",\n        [\"woman_supervillain_light_skin_tone\"] = \"🦹🏻‍♀️\",\n        [\"woman_supervillain_tone2\"] = \"🦹🏼‍♀️\",\n        [\"woman_supervillain_medium_light_skin_tone\"] = \"🦹🏼‍♀️\",\n        [\"woman_supervillain_tone3\"] = \"🦹🏽‍♀️\",\n        [\"woman_supervillain_medium_skin_tone\"] = \"🦹🏽‍♀️\",\n        [\"woman_supervillain_tone4\"] = \"🦹🏾‍♀️\",\n        [\"woman_supervillain_medium_dark_skin_tone\"] = \"🦹🏾‍♀️\",\n        [\"woman_supervillain_tone5\"] = \"🦹🏿‍♀️\",\n        [\"woman_supervillain_dark_skin_tone\"] = \"🦹🏿‍♀️\",\n        [\"man_supervillain\"] = \"🦹‍♂️\",\n        [\"man_supervillain_tone1\"] = \"🦹🏻‍♂️\",\n        [\"man_supervillain_light_skin_tone\"] = \"🦹🏻‍♂️\",\n        [\"man_supervillain_tone2\"] = \"🦹🏼‍♂️\",\n        [\"man_supervillain_medium_light_skin_tone\"] = \"🦹🏼‍♂️\",\n        [\"man_supervillain_tone3\"] = \"🦹🏽‍♂️\",\n        [\"man_supervillain_medium_skin_tone\"] = \"🦹🏽‍♂️\",\n        [\"man_supervillain_tone4\"] = \"🦹🏾‍♂️\",\n        [\"man_supervillain_medium_dark_skin_tone\"] = \"🦹🏾‍♂️\",\n        [\"man_supervillain_tone5\"] = \"🦹🏿‍♂️\",\n        [\"man_supervillain_dark_skin_tone\"] = \"🦹🏿‍♂️\",\n        [\"ninja\"] = \"🥷\",\n        [\"ninja_tone1\"] = \"🥷🏻\",\n        [\"ninja_light_skin_tone\"] = \"🥷🏻\",\n        [\"ninja_tone2\"] = \"🥷🏼\",\n        [\"ninja_medium_light_skin_tone\"] = \"🥷🏼\",\n        [\"ninja_tone3\"] = \"🥷🏽\",\n        [\"ninja_medium_skin_tone\"] = \"🥷🏽\",\n        [\"ninja_tone4\"] = \"🥷🏾\",\n        [\"ninja_medium_dark_skin_tone\"] = \"🥷🏾\",\n        [\"ninja_tone5\"] = \"🥷🏿\",\n        [\"ninja_dark_skin_tone\"] = \"🥷🏿\",\n        [\"mx_claus\"] = \"🧑‍🎄\",\n        [\"mx_claus_tone1\"] = \"🧑🏻‍🎄\",\n        [\"mx_claus_light_skin_tone\"] = \"🧑🏻‍🎄\",\n        [\"mx_claus_tone2\"] = \"🧑🏼‍🎄\",\n        [\"mx_claus_medium_light_skin_tone\"] = \"🧑🏼‍🎄\",\n        [\"mx_claus_tone3\"] = \"🧑🏽‍🎄\",\n        [\"mx_claus_medium_skin_tone\"] = \"🧑🏽‍🎄\",\n        [\"mx_claus_tone4\"] = \"🧑🏾‍🎄\",\n        [\"mx_claus_medium_dark_skin_tone\"] = \"🧑🏾‍🎄\",\n        [\"mx_claus_tone5\"] = \"🧑🏿‍🎄\",\n        [\"mx_claus_dark_skin_tone\"] = \"🧑🏿‍🎄\",\n        [\"mrs_claus\"] = \"🤶\",\n        [\"mother_christmas\"] = \"🤶\",\n        [\"mrs_claus_tone1\"] = \"🤶🏻\",\n        [\"mother_christmas_tone1\"] = \"🤶🏻\",\n        [\"mrs_claus_tone2\"] = \"🤶🏼\",\n        [\"mother_christmas_tone2\"] = \"🤶🏼\",\n        [\"mrs_claus_tone3\"] = \"🤶🏽\",\n        [\"mother_christmas_tone3\"] = \"🤶🏽\",\n        [\"mrs_claus_tone4\"] = \"🤶🏾\",\n        [\"mother_christmas_tone4\"] = \"🤶🏾\",\n        [\"mrs_claus_tone5\"] = \"🤶🏿\",\n        [\"mother_christmas_tone5\"] = \"🤶🏿\",\n        [\"santa\"] = \"🎅\",\n        [\"santa_tone1\"] = \"🎅🏻\",\n        [\"santa_tone2\"] = \"🎅🏼\",\n        [\"santa_tone3\"] = \"🎅🏽\",\n        [\"santa_tone4\"] = \"🎅🏾\",\n        [\"santa_tone5\"] = \"🎅🏿\",\n        [\"mage\"] = \"🧙\",\n        [\"mage_tone1\"] = \"🧙🏻\",\n        [\"mage_light_skin_tone\"] = \"🧙🏻\",\n        [\"mage_tone2\"] = \"🧙🏼\",\n        [\"mage_medium_light_skin_tone\"] = \"🧙🏼\",\n        [\"mage_tone3\"] = \"🧙🏽\",\n        [\"mage_medium_skin_tone\"] = \"🧙🏽\",\n        [\"mage_tone4\"] = \"🧙🏾\",\n        [\"mage_medium_dark_skin_tone\"] = \"🧙🏾\",\n        [\"mage_tone5\"] = \"🧙🏿\",\n        [\"mage_dark_skin_tone\"] = \"🧙🏿\",\n        [\"woman_mage\"] = \"🧙‍♀️\",\n        [\"woman_mage_tone1\"] = \"🧙🏻‍♀️\",\n        [\"woman_mage_light_skin_tone\"] = \"🧙🏻‍♀️\",\n        [\"woman_mage_tone2\"] = \"🧙🏼‍♀️\",\n        [\"woman_mage_medium_light_skin_tone\"] = \"🧙🏼‍♀️\",\n        [\"woman_mage_tone3\"] = \"🧙🏽‍♀️\",\n        [\"woman_mage_medium_skin_tone\"] = \"🧙🏽‍♀️\",\n        [\"woman_mage_tone4\"] = \"🧙🏾‍♀️\",\n        [\"woman_mage_medium_dark_skin_tone\"] = \"🧙🏾‍♀️\",\n        [\"woman_mage_tone5\"] = \"🧙🏿‍♀️\",\n        [\"woman_mage_dark_skin_tone\"] = \"🧙🏿‍♀️\",\n        [\"man_mage\"] = \"🧙‍♂️\",\n        [\"man_mage_tone1\"] = \"🧙🏻‍♂️\",\n        [\"man_mage_light_skin_tone\"] = \"🧙🏻‍♂️\",\n        [\"man_mage_tone2\"] = \"🧙🏼‍♂️\",\n        [\"man_mage_medium_light_skin_tone\"] = \"🧙🏼‍♂️\",\n        [\"man_mage_tone3\"] = \"🧙🏽‍♂️\",\n        [\"man_mage_medium_skin_tone\"] = \"🧙🏽‍♂️\",\n        [\"man_mage_tone4\"] = \"🧙🏾‍♂️\",\n        [\"man_mage_medium_dark_skin_tone\"] = \"🧙🏾‍♂️\",\n        [\"man_mage_tone5\"] = \"🧙🏿‍♂️\",\n        [\"man_mage_dark_skin_tone\"] = \"🧙🏿‍♂️\",\n        [\"elf\"] = \"🧝\",\n        [\"elf_tone1\"] = \"🧝🏻\",\n        [\"elf_light_skin_tone\"] = \"🧝🏻\",\n        [\"elf_tone2\"] = \"🧝🏼\",\n        [\"elf_medium_light_skin_tone\"] = \"🧝🏼\",\n        [\"elf_tone3\"] = \"🧝🏽\",\n        [\"elf_medium_skin_tone\"] = \"🧝🏽\",\n        [\"elf_tone4\"] = \"🧝🏾\",\n        [\"elf_medium_dark_skin_tone\"] = \"🧝🏾\",\n        [\"elf_tone5\"] = \"🧝🏿\",\n        [\"elf_dark_skin_tone\"] = \"🧝🏿\",\n        [\"woman_elf\"] = \"🧝‍♀️\",\n        [\"woman_elf_tone1\"] = \"🧝🏻‍♀️\",\n        [\"woman_elf_light_skin_tone\"] = \"🧝🏻‍♀️\",\n        [\"woman_elf_tone2\"] = \"🧝🏼‍♀️\",\n        [\"woman_elf_medium_light_skin_tone\"] = \"🧝🏼‍♀️\",\n        [\"woman_elf_tone3\"] = \"🧝🏽‍♀️\",\n        [\"woman_elf_medium_skin_tone\"] = \"🧝🏽‍♀️\",\n        [\"woman_elf_tone4\"] = \"🧝🏾‍♀️\",\n        [\"woman_elf_medium_dark_skin_tone\"] = \"🧝🏾‍♀️\",\n        [\"woman_elf_tone5\"] = \"🧝🏿‍♀️\",\n        [\"woman_elf_dark_skin_tone\"] = \"🧝🏿‍♀️\",\n        [\"man_elf\"] = \"🧝‍♂️\",\n        [\"man_elf_tone1\"] = \"🧝🏻‍♂️\",\n        [\"man_elf_light_skin_tone\"] = \"🧝🏻‍♂️\",\n        [\"man_elf_tone2\"] = \"🧝🏼‍♂️\",\n        [\"man_elf_medium_light_skin_tone\"] = \"🧝🏼‍♂️\",\n        [\"man_elf_tone3\"] = \"🧝🏽‍♂️\",\n        [\"man_elf_medium_skin_tone\"] = \"🧝🏽‍♂️\",\n        [\"man_elf_tone4\"] = \"🧝🏾‍♂️\",\n        [\"man_elf_medium_dark_skin_tone\"] = \"🧝🏾‍♂️\",\n        [\"man_elf_tone5\"] = \"🧝🏿‍♂️\",\n        [\"man_elf_dark_skin_tone\"] = \"🧝🏿‍♂️\",\n        [\"vampire\"] = \"🧛\",\n        [\"vampire_tone1\"] = \"🧛🏻\",\n        [\"vampire_light_skin_tone\"] = \"🧛🏻\",\n        [\"vampire_tone2\"] = \"🧛🏼\",\n        [\"vampire_medium_light_skin_tone\"] = \"🧛🏼\",\n        [\"vampire_tone3\"] = \"🧛🏽\",\n        [\"vampire_medium_skin_tone\"] = \"🧛🏽\",\n        [\"vampire_tone4\"] = \"🧛🏾\",\n        [\"vampire_medium_dark_skin_tone\"] = \"🧛🏾\",\n        [\"vampire_tone5\"] = \"🧛🏿\",\n        [\"vampire_dark_skin_tone\"] = \"🧛🏿\",\n        [\"woman_vampire\"] = \"🧛‍♀️\",\n        [\"woman_vampire_tone1\"] = \"🧛🏻‍♀️\",\n        [\"woman_vampire_light_skin_tone\"] = \"🧛🏻‍♀️\",\n        [\"woman_vampire_tone2\"] = \"🧛🏼‍♀️\",\n        [\"woman_vampire_medium_light_skin_tone\"] = \"🧛🏼‍♀️\",\n        [\"woman_vampire_tone3\"] = \"🧛🏽‍♀️\",\n        [\"woman_vampire_medium_skin_tone\"] = \"🧛🏽‍♀️\",\n        [\"woman_vampire_tone4\"] = \"🧛🏾‍♀️\",\n        [\"woman_vampire_medium_dark_skin_tone\"] = \"🧛🏾‍♀️\",\n        [\"woman_vampire_tone5\"] = \"🧛🏿‍♀️\",\n        [\"woman_vampire_dark_skin_tone\"] = \"🧛🏿‍♀️\",\n        [\"man_vampire\"] = \"🧛‍♂️\",\n        [\"man_vampire_tone1\"] = \"🧛🏻‍♂️\",\n        [\"man_vampire_light_skin_tone\"] = \"🧛🏻‍♂️\",\n        [\"man_vampire_tone2\"] = \"🧛🏼‍♂️\",\n        [\"man_vampire_medium_light_skin_tone\"] = \"🧛🏼‍♂️\",\n        [\"man_vampire_tone3\"] = \"🧛🏽‍♂️\",\n        [\"man_vampire_medium_skin_tone\"] = \"🧛🏽‍♂️\",\n        [\"man_vampire_tone4\"] = \"🧛🏾‍♂️\",\n        [\"man_vampire_medium_dark_skin_tone\"] = \"🧛🏾‍♂️\",\n        [\"man_vampire_tone5\"] = \"🧛🏿‍♂️\",\n        [\"man_vampire_dark_skin_tone\"] = \"🧛🏿‍♂️\",\n        [\"zombie\"] = \"🧟\",\n        [\"woman_zombie\"] = \"🧟‍♀️\",\n        [\"man_zombie\"] = \"🧟‍♂️\",\n        [\"genie\"] = \"🧞\",\n        [\"woman_genie\"] = \"🧞‍♀️\",\n        [\"man_genie\"] = \"🧞‍♂️\",\n        [\"merperson\"] = \"🧜\",\n        [\"merperson_tone1\"] = \"🧜🏻\",\n        [\"merperson_light_skin_tone\"] = \"🧜🏻\",\n        [\"merperson_tone2\"] = \"🧜🏼\",\n        [\"merperson_medium_light_skin_tone\"] = \"🧜🏼\",\n        [\"merperson_tone3\"] = \"🧜🏽\",\n        [\"merperson_medium_skin_tone\"] = \"🧜🏽\",\n        [\"merperson_tone4\"] = \"🧜🏾\",\n        [\"merperson_medium_dark_skin_tone\"] = \"🧜🏾\",\n        [\"merperson_tone5\"] = \"🧜🏿\",\n        [\"merperson_dark_skin_tone\"] = \"🧜🏿\",\n        [\"mermaid\"] = \"🧜‍♀️\",\n        [\"mermaid_tone1\"] = \"🧜🏻‍♀️\",\n        [\"mermaid_light_skin_tone\"] = \"🧜🏻‍♀️\",\n        [\"mermaid_tone2\"] = \"🧜🏼‍♀️\",\n        [\"mermaid_medium_light_skin_tone\"] = \"🧜🏼‍♀️\",\n        [\"mermaid_tone3\"] = \"🧜🏽‍♀️\",\n        [\"mermaid_medium_skin_tone\"] = \"🧜🏽‍♀️\",\n        [\"mermaid_tone4\"] = \"🧜🏾‍♀️\",\n        [\"mermaid_medium_dark_skin_tone\"] = \"🧜🏾‍♀️\",\n        [\"mermaid_tone5\"] = \"🧜🏿‍♀️\",\n        [\"mermaid_dark_skin_tone\"] = \"🧜🏿‍♀️\",\n        [\"merman\"] = \"🧜‍♂️\",\n        [\"merman_tone1\"] = \"🧜🏻‍♂️\",\n        [\"merman_light_skin_tone\"] = \"🧜🏻‍♂️\",\n        [\"merman_tone2\"] = \"🧜🏼‍♂️\",\n        [\"merman_medium_light_skin_tone\"] = \"🧜🏼‍♂️\",\n        [\"merman_tone3\"] = \"🧜🏽‍♂️\",\n        [\"merman_medium_skin_tone\"] = \"🧜🏽‍♂️\",\n        [\"merman_tone4\"] = \"🧜🏾‍♂️\",\n        [\"merman_medium_dark_skin_tone\"] = \"🧜🏾‍♂️\",\n        [\"merman_tone5\"] = \"🧜🏿‍♂️\",\n        [\"merman_dark_skin_tone\"] = \"🧜🏿‍♂️\",\n        [\"fairy\"] = \"🧚\",\n        [\"fairy_tone1\"] = \"🧚🏻\",\n        [\"fairy_light_skin_tone\"] = \"🧚🏻\",\n        [\"fairy_tone2\"] = \"🧚🏼\",\n        [\"fairy_medium_light_skin_tone\"] = \"🧚🏼\",\n        [\"fairy_tone3\"] = \"🧚🏽\",\n        [\"fairy_medium_skin_tone\"] = \"🧚🏽\",\n        [\"fairy_tone4\"] = \"🧚🏾\",\n        [\"fairy_medium_dark_skin_tone\"] = \"🧚🏾\",\n        [\"fairy_tone5\"] = \"🧚🏿\",\n        [\"fairy_dark_skin_tone\"] = \"🧚🏿\",\n        [\"woman_fairy\"] = \"🧚‍♀️\",\n        [\"woman_fairy_tone1\"] = \"🧚🏻‍♀️\",\n        [\"woman_fairy_light_skin_tone\"] = \"🧚🏻‍♀️\",\n        [\"woman_fairy_tone2\"] = \"🧚🏼‍♀️\",\n        [\"woman_fairy_medium_light_skin_tone\"] = \"🧚🏼‍♀️\",\n        [\"woman_fairy_tone3\"] = \"🧚🏽‍♀️\",\n        [\"woman_fairy_medium_skin_tone\"] = \"🧚🏽‍♀️\",\n        [\"woman_fairy_tone4\"] = \"🧚🏾‍♀️\",\n        [\"woman_fairy_medium_dark_skin_tone\"] = \"🧚🏾‍♀️\",\n        [\"woman_fairy_tone5\"] = \"🧚🏿‍♀️\",\n        [\"woman_fairy_dark_skin_tone\"] = \"🧚🏿‍♀️\",\n        [\"man_fairy\"] = \"🧚‍♂️\",\n        [\"man_fairy_tone1\"] = \"🧚🏻‍♂️\",\n        [\"man_fairy_light_skin_tone\"] = \"🧚🏻‍♂️\",\n        [\"man_fairy_tone2\"] = \"🧚🏼‍♂️\",\n        [\"man_fairy_medium_light_skin_tone\"] = \"🧚🏼‍♂️\",\n        [\"man_fairy_tone3\"] = \"🧚🏽‍♂️\",\n        [\"man_fairy_medium_skin_tone\"] = \"🧚🏽‍♂️\",\n        [\"man_fairy_tone4\"] = \"🧚🏾‍♂️\",\n        [\"man_fairy_medium_dark_skin_tone\"] = \"🧚🏾‍♂️\",\n        [\"man_fairy_tone5\"] = \"🧚🏿‍♂️\",\n        [\"man_fairy_dark_skin_tone\"] = \"🧚🏿‍♂️\",\n        [\"angel\"] = \"👼\",\n        [\"angel_tone1\"] = \"👼🏻\",\n        [\"angel_tone2\"] = \"👼🏼\",\n        [\"angel_tone3\"] = \"👼🏽\",\n        [\"angel_tone4\"] = \"👼🏾\",\n        [\"angel_tone5\"] = \"👼🏿\",\n        [\"pregnant_woman\"] = \"🤰\",\n        [\"expecting_woman\"] = \"🤰\",\n        [\"pregnant_woman_tone1\"] = \"🤰🏻\",\n        [\"expecting_woman_tone1\"] = \"🤰🏻\",\n        [\"pregnant_woman_tone2\"] = \"🤰🏼\",\n        [\"expecting_woman_tone2\"] = \"🤰🏼\",\n        [\"pregnant_woman_tone3\"] = \"🤰🏽\",\n        [\"expecting_woman_tone3\"] = \"🤰🏽\",\n        [\"pregnant_woman_tone4\"] = \"🤰🏾\",\n        [\"expecting_woman_tone4\"] = \"🤰🏾\",\n        [\"pregnant_woman_tone5\"] = \"🤰🏿\",\n        [\"expecting_woman_tone5\"] = \"🤰🏿\",\n        [\"breast_feeding\"] = \"🤱\",\n        [\"breast_feeding_tone1\"] = \"🤱🏻\",\n        [\"breast_feeding_light_skin_tone\"] = \"🤱🏻\",\n        [\"breast_feeding_tone2\"] = \"🤱🏼\",\n        [\"breast_feeding_medium_light_skin_tone\"] = \"🤱🏼\",\n        [\"breast_feeding_tone3\"] = \"🤱🏽\",\n        [\"breast_feeding_medium_skin_tone\"] = \"🤱🏽\",\n        [\"breast_feeding_tone4\"] = \"🤱🏾\",\n        [\"breast_feeding_medium_dark_skin_tone\"] = \"🤱🏾\",\n        [\"breast_feeding_tone5\"] = \"🤱🏿\",\n        [\"breast_feeding_dark_skin_tone\"] = \"🤱🏿\",\n        [\"person_feeding_baby\"] = \"🧑‍🍼\",\n        [\"person_feeding_baby_tone1\"] = \"🧑🏻‍🍼\",\n        [\"person_feeding_baby_light_skin_tone\"] = \"🧑🏻‍🍼\",\n        [\"person_feeding_baby_tone2\"] = \"🧑🏼‍🍼\",\n        [\"person_feeding_baby_medium_light_skin_tone\"] = \"🧑🏼‍🍼\",\n        [\"person_feeding_baby_tone3\"] = \"🧑🏽‍🍼\",\n        [\"person_feeding_baby_medium_skin_tone\"] = \"🧑🏽‍🍼\",\n        [\"person_feeding_baby_tone4\"] = \"🧑🏾‍🍼\",\n        [\"person_feeding_baby_medium_dark_skin_tone\"] = \"🧑🏾‍🍼\",\n        [\"person_feeding_baby_tone5\"] = \"🧑🏿‍🍼\",\n        [\"person_feeding_baby_dark_skin_tone\"] = \"🧑🏿‍🍼\",\n        [\"woman_feeding_baby\"] = \"👩‍🍼\",\n        [\"woman_feeding_baby_tone1\"] = \"👩🏻‍🍼\",\n        [\"woman_feeding_baby_light_skin_tone\"] = \"👩🏻‍🍼\",\n        [\"woman_feeding_baby_tone2\"] = \"👩🏼‍🍼\",\n        [\"woman_feeding_baby_medium_light_skin_tone\"] = \"👩🏼‍🍼\",\n        [\"woman_feeding_baby_tone3\"] = \"👩🏽‍🍼\",\n        [\"woman_feeding_baby_medium_skin_tone\"] = \"👩🏽‍🍼\",\n        [\"woman_feeding_baby_tone4\"] = \"👩🏾‍🍼\",\n        [\"woman_feeding_baby_medium_dark_skin_tone\"] = \"👩🏾‍🍼\",\n        [\"woman_feeding_baby_tone5\"] = \"👩🏿‍🍼\",\n        [\"woman_feeding_baby_dark_skin_tone\"] = \"👩🏿‍🍼\",\n        [\"man_feeding_baby\"] = \"👨‍🍼\",\n        [\"man_feeding_baby_tone1\"] = \"👨🏻‍🍼\",\n        [\"man_feeding_baby_light_skin_tone\"] = \"👨🏻‍🍼\",\n        [\"man_feeding_baby_tone2\"] = \"👨🏼‍🍼\",\n        [\"man_feeding_baby_medium_light_skin_tone\"] = \"👨🏼‍🍼\",\n        [\"man_feeding_baby_tone3\"] = \"👨🏽‍🍼\",\n        [\"man_feeding_baby_medium_skin_tone\"] = \"👨🏽‍🍼\",\n        [\"man_feeding_baby_tone4\"] = \"👨🏾‍🍼\",\n        [\"man_feeding_baby_medium_dark_skin_tone\"] = \"👨🏾‍🍼\",\n        [\"man_feeding_baby_tone5\"] = \"👨🏿‍🍼\",\n        [\"man_feeding_baby_dark_skin_tone\"] = \"👨🏿‍🍼\",\n        [\"person_bowing\"] = \"🙇\",\n        [\"bow\"] = \"🙇\",\n        [\"person_bowing_tone1\"] = \"🙇🏻\",\n        [\"bow_tone1\"] = \"🙇🏻\",\n        [\"person_bowing_tone2\"] = \"🙇🏼\",\n        [\"bow_tone2\"] = \"🙇🏼\",\n        [\"person_bowing_tone3\"] = \"🙇🏽\",\n        [\"bow_tone3\"] = \"🙇🏽\",\n        [\"person_bowing_tone4\"] = \"🙇🏾\",\n        [\"bow_tone4\"] = \"🙇🏾\",\n        [\"person_bowing_tone5\"] = \"🙇🏿\",\n        [\"bow_tone5\"] = \"🙇🏿\",\n        [\"woman_bowing\"] = \"🙇‍♀️\",\n        [\"woman_bowing_tone1\"] = \"🙇🏻‍♀️\",\n        [\"woman_bowing_light_skin_tone\"] = \"🙇🏻‍♀️\",\n        [\"woman_bowing_tone2\"] = \"🙇🏼‍♀️\",\n        [\"woman_bowing_medium_light_skin_tone\"] = \"🙇🏼‍♀️\",\n        [\"woman_bowing_tone3\"] = \"🙇🏽‍♀️\",\n        [\"woman_bowing_medium_skin_tone\"] = \"🙇🏽‍♀️\",\n        [\"woman_bowing_tone4\"] = \"🙇🏾‍♀️\",\n        [\"woman_bowing_medium_dark_skin_tone\"] = \"🙇🏾‍♀️\",\n        [\"woman_bowing_tone5\"] = \"🙇🏿‍♀️\",\n        [\"woman_bowing_dark_skin_tone\"] = \"🙇🏿‍♀️\",\n        [\"man_bowing\"] = \"🙇‍♂️\",\n        [\"man_bowing_tone1\"] = \"🙇🏻‍♂️\",\n        [\"man_bowing_light_skin_tone\"] = \"🙇🏻‍♂️\",\n        [\"man_bowing_tone2\"] = \"🙇🏼‍♂️\",\n        [\"man_bowing_medium_light_skin_tone\"] = \"🙇🏼‍♂️\",\n        [\"man_bowing_tone3\"] = \"🙇🏽‍♂️\",\n        [\"man_bowing_medium_skin_tone\"] = \"🙇🏽‍♂️\",\n        [\"man_bowing_tone4\"] = \"🙇🏾‍♂️\",\n        [\"man_bowing_medium_dark_skin_tone\"] = \"🙇🏾‍♂️\",\n        [\"man_bowing_tone5\"] = \"🙇🏿‍♂️\",\n        [\"man_bowing_dark_skin_tone\"] = \"🙇🏿‍♂️\",\n        [\"person_tipping_hand\"] = \"💁\",\n        [\"information_desk_person\"] = \"💁\",\n        [\"person_tipping_hand_tone1\"] = \"💁🏻\",\n        [\"information_desk_person_tone1\"] = \"💁🏻\",\n        [\"person_tipping_hand_tone2\"] = \"💁🏼\",\n        [\"information_desk_person_tone2\"] = \"💁🏼\",\n        [\"person_tipping_hand_tone3\"] = \"💁🏽\",\n        [\"information_desk_person_tone3\"] = \"💁🏽\",\n        [\"person_tipping_hand_tone4\"] = \"💁🏾\",\n        [\"information_desk_person_tone4\"] = \"💁🏾\",\n        [\"person_tipping_hand_tone5\"] = \"💁🏿\",\n        [\"information_desk_person_tone5\"] = \"💁🏿\",\n        [\"woman_tipping_hand\"] = \"💁‍♀️\",\n        [\"woman_tipping_hand_tone1\"] = \"💁🏻‍♀️\",\n        [\"woman_tipping_hand_light_skin_tone\"] = \"💁🏻‍♀️\",\n        [\"woman_tipping_hand_tone2\"] = \"💁🏼‍♀️\",\n        [\"woman_tipping_hand_medium_light_skin_tone\"] = \"💁🏼‍♀️\",\n        [\"woman_tipping_hand_tone3\"] = \"💁🏽‍♀️\",\n        [\"woman_tipping_hand_medium_skin_tone\"] = \"💁🏽‍♀️\",\n        [\"woman_tipping_hand_tone4\"] = \"💁🏾‍♀️\",\n        [\"woman_tipping_hand_medium_dark_skin_tone\"] = \"💁🏾‍♀️\",\n        [\"woman_tipping_hand_tone5\"] = \"💁🏿‍♀️\",\n        [\"woman_tipping_hand_dark_skin_tone\"] = \"💁🏿‍♀️\",\n        [\"man_tipping_hand\"] = \"💁‍♂️\",\n        [\"man_tipping_hand_tone1\"] = \"💁🏻‍♂️\",\n        [\"man_tipping_hand_light_skin_tone\"] = \"💁🏻‍♂️\",\n        [\"man_tipping_hand_tone2\"] = \"💁🏼‍♂️\",\n        [\"man_tipping_hand_medium_light_skin_tone\"] = \"💁🏼‍♂️\",\n        [\"man_tipping_hand_tone3\"] = \"💁🏽‍♂️\",\n        [\"man_tipping_hand_medium_skin_tone\"] = \"💁🏽‍♂️\",\n        [\"man_tipping_hand_tone4\"] = \"💁🏾‍♂️\",\n        [\"man_tipping_hand_medium_dark_skin_tone\"] = \"💁🏾‍♂️\",\n        [\"man_tipping_hand_tone5\"] = \"💁🏿‍♂️\",\n        [\"man_tipping_hand_dark_skin_tone\"] = \"💁🏿‍♂️\",\n        [\"person_gesturing_no\"] = \"🙅\",\n        [\"no_good\"] = \"🙅\",\n        [\"person_gesturing_no_tone1\"] = \"🙅🏻\",\n        [\"no_good_tone1\"] = \"🙅🏻\",\n        [\"person_gesturing_no_tone2\"] = \"🙅🏼\",\n        [\"no_good_tone2\"] = \"🙅🏼\",\n        [\"person_gesturing_no_tone3\"] = \"🙅🏽\",\n        [\"no_good_tone3\"] = \"🙅🏽\",\n        [\"person_gesturing_no_tone4\"] = \"🙅🏾\",\n        [\"no_good_tone4\"] = \"🙅🏾\",\n        [\"person_gesturing_no_tone5\"] = \"🙅🏿\",\n        [\"no_good_tone5\"] = \"🙅🏿\",\n        [\"woman_gesturing_no\"] = \"🙅‍♀️\",\n        [\"woman_gesturing_no_tone1\"] = \"🙅🏻‍♀️\",\n        [\"woman_gesturing_no_light_skin_tone\"] = \"🙅🏻‍♀️\",\n        [\"woman_gesturing_no_tone2\"] = \"🙅🏼‍♀️\",\n        [\"woman_gesturing_no_medium_light_skin_tone\"] = \"🙅🏼‍♀️\",\n        [\"woman_gesturing_no_tone3\"] = \"🙅🏽‍♀️\",\n        [\"woman_gesturing_no_medium_skin_tone\"] = \"🙅🏽‍♀️\",\n        [\"woman_gesturing_no_tone4\"] = \"🙅🏾‍♀️\",\n        [\"woman_gesturing_no_medium_dark_skin_tone\"] = \"🙅🏾‍♀️\",\n        [\"woman_gesturing_no_tone5\"] = \"🙅🏿‍♀️\",\n        [\"woman_gesturing_no_dark_skin_tone\"] = \"🙅🏿‍♀️\",\n        [\"man_gesturing_no\"] = \"🙅‍♂️\",\n        [\"man_gesturing_no_tone1\"] = \"🙅🏻‍♂️\",\n        [\"man_gesturing_no_light_skin_tone\"] = \"🙅🏻‍♂️\",\n        [\"man_gesturing_no_tone2\"] = \"🙅🏼‍♂️\",\n        [\"man_gesturing_no_medium_light_skin_tone\"] = \"🙅🏼‍♂️\",\n        [\"man_gesturing_no_tone3\"] = \"🙅🏽‍♂️\",\n        [\"man_gesturing_no_medium_skin_tone\"] = \"🙅🏽‍♂️\",\n        [\"man_gesturing_no_tone4\"] = \"🙅🏾‍♂️\",\n        [\"man_gesturing_no_medium_dark_skin_tone\"] = \"🙅🏾‍♂️\",\n        [\"man_gesturing_no_tone5\"] = \"🙅🏿‍♂️\",\n        [\"man_gesturing_no_dark_skin_tone\"] = \"🙅🏿‍♂️\",\n        [\"person_gesturing_ok\"] = \"🙆\",\n        [\"ok_woman\"] = \"🙆\",\n        [\"person_gesturing_ok_tone1\"] = \"🙆🏻\",\n        [\"ok_woman_tone1\"] = \"🙆🏻\",\n        [\"person_gesturing_ok_tone2\"] = \"🙆🏼\",\n        [\"ok_woman_tone2\"] = \"🙆🏼\",\n        [\"person_gesturing_ok_tone3\"] = \"🙆🏽\",\n        [\"ok_woman_tone3\"] = \"🙆🏽\",\n        [\"person_gesturing_ok_tone4\"] = \"🙆🏾\",\n        [\"ok_woman_tone4\"] = \"🙆🏾\",\n        [\"person_gesturing_ok_tone5\"] = \"🙆🏿\",\n        [\"ok_woman_tone5\"] = \"🙆🏿\",\n        [\"woman_gesturing_ok\"] = \"🙆‍♀️\",\n        [\"woman_gesturing_ok_tone1\"] = \"🙆🏻‍♀️\",\n        [\"woman_gesturing_ok_light_skin_tone\"] = \"🙆🏻‍♀️\",\n        [\"woman_gesturing_ok_tone2\"] = \"🙆🏼‍♀️\",\n        [\"woman_gesturing_ok_medium_light_skin_tone\"] = \"🙆🏼‍♀️\",\n        [\"woman_gesturing_ok_tone3\"] = \"🙆🏽‍♀️\",\n        [\"woman_gesturing_ok_medium_skin_tone\"] = \"🙆🏽‍♀️\",\n        [\"woman_gesturing_ok_tone4\"] = \"🙆🏾‍♀️\",\n        [\"woman_gesturing_ok_medium_dark_skin_tone\"] = \"🙆🏾‍♀️\",\n        [\"woman_gesturing_ok_tone5\"] = \"🙆🏿‍♀️\",\n        [\"woman_gesturing_ok_dark_skin_tone\"] = \"🙆🏿‍♀️\",\n        [\"man_gesturing_ok\"] = \"🙆‍♂️\",\n        [\"man_gesturing_ok_tone1\"] = \"🙆🏻‍♂️\",\n        [\"man_gesturing_ok_light_skin_tone\"] = \"🙆🏻‍♂️\",\n        [\"man_gesturing_ok_tone2\"] = \"🙆🏼‍♂️\",\n        [\"man_gesturing_ok_medium_light_skin_tone\"] = \"🙆🏼‍♂️\",\n        [\"man_gesturing_ok_tone3\"] = \"🙆🏽‍♂️\",\n        [\"man_gesturing_ok_medium_skin_tone\"] = \"🙆🏽‍♂️\",\n        [\"man_gesturing_ok_tone4\"] = \"🙆🏾‍♂️\",\n        [\"man_gesturing_ok_medium_dark_skin_tone\"] = \"🙆🏾‍♂️\",\n        [\"man_gesturing_ok_tone5\"] = \"🙆🏿‍♂️\",\n        [\"man_gesturing_ok_dark_skin_tone\"] = \"🙆🏿‍♂️\",\n        [\"person_raising_hand\"] = \"🙋\",\n        [\"raising_hand\"] = \"🙋\",\n        [\"person_raising_hand_tone1\"] = \"🙋🏻\",\n        [\"raising_hand_tone1\"] = \"🙋🏻\",\n        [\"person_raising_hand_tone2\"] = \"🙋🏼\",\n        [\"raising_hand_tone2\"] = \"🙋🏼\",\n        [\"person_raising_hand_tone3\"] = \"🙋🏽\",\n        [\"raising_hand_tone3\"] = \"🙋🏽\",\n        [\"person_raising_hand_tone4\"] = \"🙋🏾\",\n        [\"raising_hand_tone4\"] = \"🙋🏾\",\n        [\"person_raising_hand_tone5\"] = \"🙋🏿\",\n        [\"raising_hand_tone5\"] = \"🙋🏿\",\n        [\"woman_raising_hand\"] = \"🙋‍♀️\",\n        [\"woman_raising_hand_tone1\"] = \"🙋🏻‍♀️\",\n        [\"woman_raising_hand_light_skin_tone\"] = \"🙋🏻‍♀️\",\n        [\"woman_raising_hand_tone2\"] = \"🙋🏼‍♀️\",\n        [\"woman_raising_hand_medium_light_skin_tone\"] = \"🙋🏼‍♀️\",\n        [\"woman_raising_hand_tone3\"] = \"🙋🏽‍♀️\",\n        [\"woman_raising_hand_medium_skin_tone\"] = \"🙋🏽‍♀️\",\n        [\"woman_raising_hand_tone4\"] = \"🙋🏾‍♀️\",\n        [\"woman_raising_hand_medium_dark_skin_tone\"] = \"🙋🏾‍♀️\",\n        [\"woman_raising_hand_tone5\"] = \"🙋🏿‍♀️\",\n        [\"woman_raising_hand_dark_skin_tone\"] = \"🙋🏿‍♀️\",\n        [\"man_raising_hand\"] = \"🙋‍♂️\",\n        [\"man_raising_hand_tone1\"] = \"🙋🏻‍♂️\",\n        [\"man_raising_hand_light_skin_tone\"] = \"🙋🏻‍♂️\",\n        [\"man_raising_hand_tone2\"] = \"🙋🏼‍♂️\",\n        [\"man_raising_hand_medium_light_skin_tone\"] = \"🙋🏼‍♂️\",\n        [\"man_raising_hand_tone3\"] = \"🙋🏽‍♂️\",\n        [\"man_raising_hand_medium_skin_tone\"] = \"🙋🏽‍♂️\",\n        [\"man_raising_hand_tone4\"] = \"🙋🏾‍♂️\",\n        [\"man_raising_hand_medium_dark_skin_tone\"] = \"🙋🏾‍♂️\",\n        [\"man_raising_hand_tone5\"] = \"🙋🏿‍♂️\",\n        [\"man_raising_hand_dark_skin_tone\"] = \"🙋🏿‍♂️\",\n        [\"deaf_person\"] = \"🧏\",\n        [\"deaf_person_tone1\"] = \"🧏🏻\",\n        [\"deaf_person_light_skin_tone\"] = \"🧏🏻\",\n        [\"deaf_person_tone2\"] = \"🧏🏼\",\n        [\"deaf_person_medium_light_skin_tone\"] = \"🧏🏼\",\n        [\"deaf_person_tone3\"] = \"🧏🏽\",\n        [\"deaf_person_medium_skin_tone\"] = \"🧏🏽\",\n        [\"deaf_person_tone4\"] = \"🧏🏾\",\n        [\"deaf_person_medium_dark_skin_tone\"] = \"🧏🏾\",\n        [\"deaf_person_tone5\"] = \"🧏🏿\",\n        [\"deaf_person_dark_skin_tone\"] = \"🧏🏿\",\n        [\"deaf_woman\"] = \"🧏‍♀️\",\n        [\"deaf_woman_tone1\"] = \"🧏🏻‍♀️\",\n        [\"deaf_woman_light_skin_tone\"] = \"🧏🏻‍♀️\",\n        [\"deaf_woman_tone2\"] = \"🧏🏼‍♀️\",\n        [\"deaf_woman_medium_light_skin_tone\"] = \"🧏🏼‍♀️\",\n        [\"deaf_woman_tone3\"] = \"🧏🏽‍♀️\",\n        [\"deaf_woman_medium_skin_tone\"] = \"🧏🏽‍♀️\",\n        [\"deaf_woman_tone4\"] = \"🧏🏾‍♀️\",\n        [\"deaf_woman_medium_dark_skin_tone\"] = \"🧏🏾‍♀️\",\n        [\"deaf_woman_tone5\"] = \"🧏🏿‍♀️\",\n        [\"deaf_woman_dark_skin_tone\"] = \"🧏🏿‍♀️\",\n        [\"deaf_man\"] = \"🧏‍♂️\",\n        [\"deaf_man_tone1\"] = \"🧏🏻‍♂️\",\n        [\"deaf_man_light_skin_tone\"] = \"🧏🏻‍♂️\",\n        [\"deaf_man_tone2\"] = \"🧏🏼‍♂️\",\n        [\"deaf_man_medium_light_skin_tone\"] = \"🧏🏼‍♂️\",\n        [\"deaf_man_tone3\"] = \"🧏🏽‍♂️\",\n        [\"deaf_man_medium_skin_tone\"] = \"🧏🏽‍♂️\",\n        [\"deaf_man_tone4\"] = \"🧏🏾‍♂️\",\n        [\"deaf_man_medium_dark_skin_tone\"] = \"🧏🏾‍♂️\",\n        [\"deaf_man_tone5\"] = \"🧏🏿‍♂️\",\n        [\"deaf_man_dark_skin_tone\"] = \"🧏🏿‍♂️\",\n        [\"person_facepalming\"] = \"🤦\",\n        [\"face_palm\"] = \"🤦\",\n        [\"facepalm\"] = \"🤦\",\n        [\"person_facepalming_tone1\"] = \"🤦🏻\",\n        [\"face_palm_tone1\"] = \"🤦🏻\",\n        [\"facepalm_tone1\"] = \"🤦🏻\",\n        [\"person_facepalming_tone2\"] = \"🤦🏼\",\n        [\"face_palm_tone2\"] = \"🤦🏼\",\n        [\"facepalm_tone2\"] = \"🤦🏼\",\n        [\"person_facepalming_tone3\"] = \"🤦🏽\",\n        [\"face_palm_tone3\"] = \"🤦🏽\",\n        [\"facepalm_tone3\"] = \"🤦🏽\",\n        [\"person_facepalming_tone4\"] = \"🤦🏾\",\n        [\"face_palm_tone4\"] = \"🤦🏾\",\n        [\"facepalm_tone4\"] = \"🤦🏾\",\n        [\"person_facepalming_tone5\"] = \"🤦🏿\",\n        [\"face_palm_tone5\"] = \"🤦🏿\",\n        [\"facepalm_tone5\"] = \"🤦🏿\",\n        [\"woman_facepalming\"] = \"🤦‍♀️\",\n        [\"woman_facepalming_tone1\"] = \"🤦🏻‍♀️\",\n        [\"woman_facepalming_light_skin_tone\"] = \"🤦🏻‍♀️\",\n        [\"woman_facepalming_tone2\"] = \"🤦🏼‍♀️\",\n        [\"woman_facepalming_medium_light_skin_tone\"] = \"🤦🏼‍♀️\",\n        [\"woman_facepalming_tone3\"] = \"🤦🏽‍♀️\",\n        [\"woman_facepalming_medium_skin_tone\"] = \"🤦🏽‍♀️\",\n        [\"woman_facepalming_tone4\"] = \"🤦🏾‍♀️\",\n        [\"woman_facepalming_medium_dark_skin_tone\"] = \"🤦🏾‍♀️\",\n        [\"woman_facepalming_tone5\"] = \"🤦🏿‍♀️\",\n        [\"woman_facepalming_dark_skin_tone\"] = \"🤦🏿‍♀️\",\n        [\"man_facepalming\"] = \"🤦‍♂️\",\n        [\"man_facepalming_tone1\"] = \"🤦🏻‍♂️\",\n        [\"man_facepalming_light_skin_tone\"] = \"🤦🏻‍♂️\",\n        [\"man_facepalming_tone2\"] = \"🤦🏼‍♂️\",\n        [\"man_facepalming_medium_light_skin_tone\"] = \"🤦🏼‍♂️\",\n        [\"man_facepalming_tone3\"] = \"🤦🏽‍♂️\",\n        [\"man_facepalming_medium_skin_tone\"] = \"🤦🏽‍♂️\",\n        [\"man_facepalming_tone4\"] = \"🤦🏾‍♂️\",\n        [\"man_facepalming_medium_dark_skin_tone\"] = \"🤦🏾‍♂️\",\n        [\"man_facepalming_tone5\"] = \"🤦🏿‍♂️\",\n        [\"man_facepalming_dark_skin_tone\"] = \"🤦🏿‍♂️\",\n        [\"person_shrugging\"] = \"🤷\",\n        [\"shrug\"] = \"🤷\",\n        [\"person_shrugging_tone1\"] = \"🤷🏻\",\n        [\"shrug_tone1\"] = \"🤷🏻\",\n        [\"person_shrugging_tone2\"] = \"🤷🏼\",\n        [\"shrug_tone2\"] = \"🤷🏼\",\n        [\"person_shrugging_tone3\"] = \"🤷🏽\",\n        [\"shrug_tone3\"] = \"🤷🏽\",\n        [\"person_shrugging_tone4\"] = \"🤷🏾\",\n        [\"shrug_tone4\"] = \"🤷🏾\",\n        [\"person_shrugging_tone5\"] = \"🤷🏿\",\n        [\"shrug_tone5\"] = \"🤷🏿\",\n        [\"woman_shrugging\"] = \"🤷‍♀️\",\n        [\"woman_shrugging_tone1\"] = \"🤷🏻‍♀️\",\n        [\"woman_shrugging_light_skin_tone\"] = \"🤷🏻‍♀️\",\n        [\"woman_shrugging_tone2\"] = \"🤷🏼‍♀️\",\n        [\"woman_shrugging_medium_light_skin_tone\"] = \"🤷🏼‍♀️\",\n        [\"woman_shrugging_tone3\"] = \"🤷🏽‍♀️\",\n        [\"woman_shrugging_medium_skin_tone\"] = \"🤷🏽‍♀️\",\n        [\"woman_shrugging_tone4\"] = \"🤷🏾‍♀️\",\n        [\"woman_shrugging_medium_dark_skin_tone\"] = \"🤷🏾‍♀️\",\n        [\"woman_shrugging_tone5\"] = \"🤷🏿‍♀️\",\n        [\"woman_shrugging_dark_skin_tone\"] = \"🤷🏿‍♀️\",\n        [\"man_shrugging\"] = \"🤷‍♂️\",\n        [\"man_shrugging_tone1\"] = \"🤷🏻‍♂️\",\n        [\"man_shrugging_light_skin_tone\"] = \"🤷🏻‍♂️\",\n        [\"man_shrugging_tone2\"] = \"🤷🏼‍♂️\",\n        [\"man_shrugging_medium_light_skin_tone\"] = \"🤷🏼‍♂️\",\n        [\"man_shrugging_tone3\"] = \"🤷🏽‍♂️\",\n        [\"man_shrugging_medium_skin_tone\"] = \"🤷🏽‍♂️\",\n        [\"man_shrugging_tone4\"] = \"🤷🏾‍♂️\",\n        [\"man_shrugging_medium_dark_skin_tone\"] = \"🤷🏾‍♂️\",\n        [\"man_shrugging_tone5\"] = \"🤷🏿‍♂️\",\n        [\"man_shrugging_dark_skin_tone\"] = \"🤷🏿‍♂️\",\n        [\"person_pouting\"] = \"🙎\",\n        [\"person_with_pouting_face\"] = \"🙎\",\n        [\"person_pouting_tone1\"] = \"🙎🏻\",\n        [\"person_with_pouting_face_tone1\"] = \"🙎🏻\",\n        [\"person_pouting_tone2\"] = \"🙎🏼\",\n        [\"person_with_pouting_face_tone2\"] = \"🙎🏼\",\n        [\"person_pouting_tone3\"] = \"🙎🏽\",\n        [\"person_with_pouting_face_tone3\"] = \"🙎🏽\",\n        [\"person_pouting_tone4\"] = \"🙎🏾\",\n        [\"person_with_pouting_face_tone4\"] = \"🙎🏾\",\n        [\"person_pouting_tone5\"] = \"🙎🏿\",\n        [\"person_with_pouting_face_tone5\"] = \"🙎🏿\",\n        [\"woman_pouting\"] = \"🙎‍♀️\",\n        [\"woman_pouting_tone1\"] = \"🙎🏻‍♀️\",\n        [\"woman_pouting_light_skin_tone\"] = \"🙎🏻‍♀️\",\n        [\"woman_pouting_tone2\"] = \"🙎🏼‍♀️\",\n        [\"woman_pouting_medium_light_skin_tone\"] = \"🙎🏼‍♀️\",\n        [\"woman_pouting_tone3\"] = \"🙎🏽‍♀️\",\n        [\"woman_pouting_medium_skin_tone\"] = \"🙎🏽‍♀️\",\n        [\"woman_pouting_tone4\"] = \"🙎🏾‍♀️\",\n        [\"woman_pouting_medium_dark_skin_tone\"] = \"🙎🏾‍♀️\",\n        [\"woman_pouting_tone5\"] = \"🙎🏿‍♀️\",\n        [\"woman_pouting_dark_skin_tone\"] = \"🙎🏿‍♀️\",\n        [\"man_pouting\"] = \"🙎‍♂️\",\n        [\"man_pouting_tone1\"] = \"🙎🏻‍♂️\",\n        [\"man_pouting_light_skin_tone\"] = \"🙎🏻‍♂️\",\n        [\"man_pouting_tone2\"] = \"🙎🏼‍♂️\",\n        [\"man_pouting_medium_light_skin_tone\"] = \"🙎🏼‍♂️\",\n        [\"man_pouting_tone3\"] = \"🙎🏽‍♂️\",\n        [\"man_pouting_medium_skin_tone\"] = \"🙎🏽‍♂️\",\n        [\"man_pouting_tone4\"] = \"🙎🏾‍♂️\",\n        [\"man_pouting_medium_dark_skin_tone\"] = \"🙎🏾‍♂️\",\n        [\"man_pouting_tone5\"] = \"🙎🏿‍♂️\",\n        [\"man_pouting_dark_skin_tone\"] = \"🙎🏿‍♂️\",\n        [\"person_frowning\"] = \"🙍\",\n        [\"person_frowning_tone1\"] = \"🙍🏻\",\n        [\"person_frowning_tone2\"] = \"🙍🏼\",\n        [\"person_frowning_tone3\"] = \"🙍🏽\",\n        [\"person_frowning_tone4\"] = \"🙍🏾\",\n        [\"person_frowning_tone5\"] = \"🙍🏿\",\n        [\"woman_frowning\"] = \"🙍‍♀️\",\n        [\"woman_frowning_tone1\"] = \"🙍🏻‍♀️\",\n        [\"woman_frowning_light_skin_tone\"] = \"🙍🏻‍♀️\",\n        [\"woman_frowning_tone2\"] = \"🙍🏼‍♀️\",\n        [\"woman_frowning_medium_light_skin_tone\"] = \"🙍🏼‍♀️\",\n        [\"woman_frowning_tone3\"] = \"🙍🏽‍♀️\",\n        [\"woman_frowning_medium_skin_tone\"] = \"🙍🏽‍♀️\",\n        [\"woman_frowning_tone4\"] = \"🙍🏾‍♀️\",\n        [\"woman_frowning_medium_dark_skin_tone\"] = \"🙍🏾‍♀️\",\n        [\"woman_frowning_tone5\"] = \"🙍🏿‍♀️\",\n        [\"woman_frowning_dark_skin_tone\"] = \"🙍🏿‍♀️\",\n        [\"man_frowning\"] = \"🙍‍♂️\",\n        [\"man_frowning_tone1\"] = \"🙍🏻‍♂️\",\n        [\"man_frowning_light_skin_tone\"] = \"🙍🏻‍♂️\",\n        [\"man_frowning_tone2\"] = \"🙍🏼‍♂️\",\n        [\"man_frowning_medium_light_skin_tone\"] = \"🙍🏼‍♂️\",\n        [\"man_frowning_tone3\"] = \"🙍🏽‍♂️\",\n        [\"man_frowning_medium_skin_tone\"] = \"🙍🏽‍♂️\",\n        [\"man_frowning_tone4\"] = \"🙍🏾‍♂️\",\n        [\"man_frowning_medium_dark_skin_tone\"] = \"🙍🏾‍♂️\",\n        [\"man_frowning_tone5\"] = \"🙍🏿‍♂️\",\n        [\"man_frowning_dark_skin_tone\"] = \"🙍🏿‍♂️\",\n        [\"person_getting_haircut\"] = \"💇\",\n        [\"haircut\"] = \"💇\",\n        [\"person_getting_haircut_tone1\"] = \"💇🏻\",\n        [\"haircut_tone1\"] = \"💇🏻\",\n        [\"person_getting_haircut_tone2\"] = \"💇🏼\",\n        [\"haircut_tone2\"] = \"💇🏼\",\n        [\"person_getting_haircut_tone3\"] = \"💇🏽\",\n        [\"haircut_tone3\"] = \"💇🏽\",\n        [\"person_getting_haircut_tone4\"] = \"💇🏾\",\n        [\"haircut_tone4\"] = \"💇🏾\",\n        [\"person_getting_haircut_tone5\"] = \"💇🏿\",\n        [\"haircut_tone5\"] = \"💇🏿\",\n        [\"woman_getting_haircut\"] = \"💇‍♀️\",\n        [\"woman_getting_haircut_tone1\"] = \"💇🏻‍♀️\",\n        [\"woman_getting_haircut_light_skin_tone\"] = \"💇🏻‍♀️\",\n        [\"woman_getting_haircut_tone2\"] = \"💇🏼‍♀️\",\n        [\"woman_getting_haircut_medium_light_skin_tone\"] = \"💇🏼‍♀️\",\n        [\"woman_getting_haircut_tone3\"] = \"💇🏽‍♀️\",\n        [\"woman_getting_haircut_medium_skin_tone\"] = \"💇🏽‍♀️\",\n        [\"woman_getting_haircut_tone4\"] = \"💇🏾‍♀️\",\n        [\"woman_getting_haircut_medium_dark_skin_tone\"] = \"💇🏾‍♀️\",\n        [\"woman_getting_haircut_tone5\"] = \"💇🏿‍♀️\",\n        [\"woman_getting_haircut_dark_skin_tone\"] = \"💇🏿‍♀️\",\n        [\"man_getting_haircut\"] = \"💇‍♂️\",\n        [\"man_getting_haircut_tone1\"] = \"💇🏻‍♂️\",\n        [\"man_getting_haircut_light_skin_tone\"] = \"💇🏻‍♂️\",\n        [\"man_getting_haircut_tone2\"] = \"💇🏼‍♂️\",\n        [\"man_getting_haircut_medium_light_skin_tone\"] = \"💇🏼‍♂️\",\n        [\"man_getting_haircut_tone3\"] = \"💇🏽‍♂️\",\n        [\"man_getting_haircut_medium_skin_tone\"] = \"💇🏽‍♂️\",\n        [\"man_getting_haircut_tone4\"] = \"💇🏾‍♂️\",\n        [\"man_getting_haircut_medium_dark_skin_tone\"] = \"💇🏾‍♂️\",\n        [\"man_getting_haircut_tone5\"] = \"💇🏿‍♂️\",\n        [\"man_getting_haircut_dark_skin_tone\"] = \"💇🏿‍♂️\",\n        [\"person_getting_massage\"] = \"💆\",\n        [\"massage\"] = \"💆\",\n        [\"person_getting_massage_tone1\"] = \"💆🏻\",\n        [\"massage_tone1\"] = \"💆🏻\",\n        [\"person_getting_massage_tone2\"] = \"💆🏼\",\n        [\"massage_tone2\"] = \"💆🏼\",\n        [\"person_getting_massage_tone3\"] = \"💆🏽\",\n        [\"massage_tone3\"] = \"💆🏽\",\n        [\"person_getting_massage_tone4\"] = \"💆🏾\",\n        [\"massage_tone4\"] = \"💆🏾\",\n        [\"person_getting_massage_tone5\"] = \"💆🏿\",\n        [\"massage_tone5\"] = \"💆🏿\",\n        [\"woman_getting_face_massage\"] = \"💆‍♀️\",\n        [\"woman_getting_face_massage_tone1\"] = \"💆🏻‍♀️\",\n        [\"woman_getting_face_massage_light_skin_tone\"] = \"💆🏻‍♀️\",\n        [\"woman_getting_face_massage_tone2\"] = \"💆🏼‍♀️\",\n        [\"woman_getting_face_massage_medium_light_skin_tone\"] = \"💆🏼‍♀️\",\n        [\"woman_getting_face_massage_tone3\"] = \"💆🏽‍♀️\",\n        [\"woman_getting_face_massage_medium_skin_tone\"] = \"💆🏽‍♀️\",\n        [\"woman_getting_face_massage_tone4\"] = \"💆🏾‍♀️\",\n        [\"woman_getting_face_massage_medium_dark_skin_tone\"] = \"💆🏾‍♀️\",\n        [\"woman_getting_face_massage_tone5\"] = \"💆🏿‍♀️\",\n        [\"woman_getting_face_massage_dark_skin_tone\"] = \"💆🏿‍♀️\",\n        [\"man_getting_face_massage\"] = \"💆‍♂️\",\n        [\"man_getting_face_massage_tone1\"] = \"💆🏻‍♂️\",\n        [\"man_getting_face_massage_light_skin_tone\"] = \"💆🏻‍♂️\",\n        [\"man_getting_face_massage_tone2\"] = \"💆🏼‍♂️\",\n        [\"man_getting_face_massage_medium_light_skin_tone\"] = \"💆🏼‍♂️\",\n        [\"man_getting_face_massage_tone3\"] = \"💆🏽‍♂️\",\n        [\"man_getting_face_massage_medium_skin_tone\"] = \"💆🏽‍♂️\",\n        [\"man_getting_face_massage_tone4\"] = \"💆🏾‍♂️\",\n        [\"man_getting_face_massage_medium_dark_skin_tone\"] = \"💆🏾‍♂️\",\n        [\"man_getting_face_massage_tone5\"] = \"💆🏿‍♂️\",\n        [\"man_getting_face_massage_dark_skin_tone\"] = \"💆🏿‍♂️\",\n        [\"person_in_steamy_room\"] = \"🧖\",\n        [\"person_in_steamy_room_tone1\"] = \"🧖🏻\",\n        [\"person_in_steamy_room_light_skin_tone\"] = \"🧖🏻\",\n        [\"person_in_steamy_room_tone2\"] = \"🧖🏼\",\n        [\"person_in_steamy_room_medium_light_skin_tone\"] = \"🧖🏼\",\n        [\"person_in_steamy_room_tone3\"] = \"🧖🏽\",\n        [\"person_in_steamy_room_medium_skin_tone\"] = \"🧖🏽\",\n        [\"person_in_steamy_room_tone4\"] = \"🧖🏾\",\n        [\"person_in_steamy_room_medium_dark_skin_tone\"] = \"🧖🏾\",\n        [\"person_in_steamy_room_tone5\"] = \"🧖🏿\",\n        [\"person_in_steamy_room_dark_skin_tone\"] = \"🧖🏿\",\n        [\"woman_in_steamy_room\"] = \"🧖‍♀️\",\n        [\"woman_in_steamy_room_tone1\"] = \"🧖🏻‍♀️\",\n        [\"woman_in_steamy_room_light_skin_tone\"] = \"🧖🏻‍♀️\",\n        [\"woman_in_steamy_room_tone2\"] = \"🧖🏼‍♀️\",\n        [\"woman_in_steamy_room_medium_light_skin_tone\"] = \"🧖🏼‍♀️\",\n        [\"woman_in_steamy_room_tone3\"] = \"🧖🏽‍♀️\",\n        [\"woman_in_steamy_room_medium_skin_tone\"] = \"🧖🏽‍♀️\",\n        [\"woman_in_steamy_room_tone4\"] = \"🧖🏾‍♀️\",\n        [\"woman_in_steamy_room_medium_dark_skin_tone\"] = \"🧖🏾‍♀️\",\n        [\"woman_in_steamy_room_tone5\"] = \"🧖🏿‍♀️\",\n        [\"woman_in_steamy_room_dark_skin_tone\"] = \"🧖🏿‍♀️\",\n        [\"man_in_steamy_room\"] = \"🧖‍♂️\",\n        [\"man_in_steamy_room_tone1\"] = \"🧖🏻‍♂️\",\n        [\"man_in_steamy_room_light_skin_tone\"] = \"🧖🏻‍♂️\",\n        [\"man_in_steamy_room_tone2\"] = \"🧖🏼‍♂️\",\n        [\"man_in_steamy_room_medium_light_skin_tone\"] = \"🧖🏼‍♂️\",\n        [\"man_in_steamy_room_tone3\"] = \"🧖🏽‍♂️\",\n        [\"man_in_steamy_room_medium_skin_tone\"] = \"🧖🏽‍♂️\",\n        [\"man_in_steamy_room_tone4\"] = \"🧖🏾‍♂️\",\n        [\"man_in_steamy_room_medium_dark_skin_tone\"] = \"🧖🏾‍♂️\",\n        [\"man_in_steamy_room_tone5\"] = \"🧖🏿‍♂️\",\n        [\"man_in_steamy_room_dark_skin_tone\"] = \"🧖🏿‍♂️\",\n        [\"nail_care\"] = \"💅\",\n        [\"nail_care_tone1\"] = \"💅🏻\",\n        [\"nail_care_tone2\"] = \"💅🏼\",\n        [\"nail_care_tone3\"] = \"💅🏽\",\n        [\"nail_care_tone4\"] = \"💅🏾\",\n        [\"nail_care_tone5\"] = \"💅🏿\",\n        [\"selfie\"] = \"🤳\",\n        [\"selfie_tone1\"] = \"🤳🏻\",\n        [\"selfie_tone2\"] = \"🤳🏼\",\n        [\"selfie_tone3\"] = \"🤳🏽\",\n        [\"selfie_tone4\"] = \"🤳🏾\",\n        [\"selfie_tone5\"] = \"🤳🏿\",\n        [\"dancer\"] = \"💃\",\n        [\"dancer_tone1\"] = \"💃🏻\",\n        [\"dancer_tone2\"] = \"💃🏼\",\n        [\"dancer_tone3\"] = \"💃🏽\",\n        [\"dancer_tone4\"] = \"💃🏾\",\n        [\"dancer_tone5\"] = \"💃🏿\",\n        [\"man_dancing\"] = \"🕺\",\n        [\"male_dancer\"] = \"🕺\",\n        [\"man_dancing_tone1\"] = \"🕺🏻\",\n        [\"male_dancer_tone1\"] = \"🕺🏻\",\n        [\"man_dancing_tone2\"] = \"🕺🏼\",\n        [\"male_dancer_tone2\"] = \"🕺🏼\",\n        [\"man_dancing_tone3\"] = \"🕺🏽\",\n        [\"male_dancer_tone3\"] = \"🕺🏽\",\n        [\"man_dancing_tone5\"] = \"🕺🏿\",\n        [\"male_dancer_tone5\"] = \"🕺🏿\",\n        [\"man_dancing_tone4\"] = \"🕺🏾\",\n        [\"male_dancer_tone4\"] = \"🕺🏾\",\n        [\"people_with_bunny_ears_partying\"] = \"👯\",\n        [\"dancers\"] = \"👯\",\n        [\"women_with_bunny_ears_partying\"] = \"👯‍♀️\",\n        [\"men_with_bunny_ears_partying\"] = \"👯‍♂️\",\n        [\"levitate\"] = \"🕴️\",\n        [\"man_in_business_suit_levitating\"] = \"🕴️\",\n        [\"levitate_tone1\"] = \"🕴🏻\",\n        [\"man_in_business_suit_levitating_tone1\"] = \"🕴🏻\",\n        [\"man_in_business_suit_levitating_light_skin_tone\"] = \"🕴🏻\",\n        [\"levitate_tone2\"] = \"🕴🏼\",\n        [\"man_in_business_suit_levitating_tone2\"] = \"🕴🏼\",\n        [\"man_in_business_suit_levitating_medium_light_skin_tone\"] = \"🕴🏼\",\n        [\"levitate_tone3\"] = \"🕴🏽\",\n        [\"man_in_business_suit_levitating_tone3\"] = \"🕴🏽\",\n        [\"man_in_business_suit_levitating_medium_skin_tone\"] = \"🕴🏽\",\n        [\"levitate_tone4\"] = \"🕴🏾\",\n        [\"man_in_business_suit_levitating_tone4\"] = \"🕴🏾\",\n        [\"man_in_business_suit_levitating_medium_dark_skin_tone\"] = \"🕴🏾\",\n        [\"levitate_tone5\"] = \"🕴🏿\",\n        [\"man_in_business_suit_levitating_tone5\"] = \"🕴🏿\",\n        [\"man_in_business_suit_levitating_dark_skin_tone\"] = \"🕴🏿\",\n        [\"person_in_manual_wheelchair\"] = \"🧑‍🦽\",\n        [\"person_in_manual_wheelchair_tone1\"] = \"🧑🏻‍🦽\",\n        [\"person_in_manual_wheelchair_light_skin_tone\"] = \"🧑🏻‍🦽\",\n        [\"person_in_manual_wheelchair_tone2\"] = \"🧑🏼‍🦽\",\n        [\"person_in_manual_wheelchair_medium_light_skin_tone\"] = \"🧑🏼‍🦽\",\n        [\"person_in_manual_wheelchair_tone3\"] = \"🧑🏽‍🦽\",\n        [\"person_in_manual_wheelchair_medium_skin_tone\"] = \"🧑🏽‍🦽\",\n        [\"person_in_manual_wheelchair_tone4\"] = \"🧑🏾‍🦽\",\n        [\"person_in_manual_wheelchair_medium_dark_skin_tone\"] = \"🧑🏾‍🦽\",\n        [\"person_in_manual_wheelchair_tone5\"] = \"🧑🏿‍🦽\",\n        [\"person_in_manual_wheelchair_dark_skin_tone\"] = \"🧑🏿‍🦽\",\n        [\"woman_in_manual_wheelchair\"] = \"👩‍🦽\",\n        [\"woman_in_manual_wheelchair_tone1\"] = \"👩🏻‍🦽\",\n        [\"woman_in_manual_wheelchair_light_skin_tone\"] = \"👩🏻‍🦽\",\n        [\"woman_in_manual_wheelchair_tone2\"] = \"👩🏼‍🦽\",\n        [\"woman_in_manual_wheelchair_medium_light_skin_tone\"] = \"👩🏼‍🦽\",\n        [\"woman_in_manual_wheelchair_tone3\"] = \"👩🏽‍🦽\",\n        [\"woman_in_manual_wheelchair_medium_skin_tone\"] = \"👩🏽‍🦽\",\n        [\"woman_in_manual_wheelchair_tone4\"] = \"👩🏾‍🦽\",\n        [\"woman_in_manual_wheelchair_medium_dark_skin_tone\"] = \"👩🏾‍🦽\",\n        [\"woman_in_manual_wheelchair_tone5\"] = \"👩🏿‍🦽\",\n        [\"woman_in_manual_wheelchair_dark_skin_tone\"] = \"👩🏿‍🦽\",\n        [\"man_in_manual_wheelchair\"] = \"👨‍🦽\",\n        [\"man_in_manual_wheelchair_tone1\"] = \"👨🏻‍🦽\",\n        [\"man_in_manual_wheelchair_light_skin_tone\"] = \"👨🏻‍🦽\",\n        [\"man_in_manual_wheelchair_tone2\"] = \"👨🏼‍🦽\",\n        [\"man_in_manual_wheelchair_medium_light_skin_tone\"] = \"👨🏼‍🦽\",\n        [\"man_in_manual_wheelchair_tone3\"] = \"👨🏽‍🦽\",\n        [\"man_in_manual_wheelchair_medium_skin_tone\"] = \"👨🏽‍🦽\",\n        [\"man_in_manual_wheelchair_tone4\"] = \"👨🏾‍🦽\",\n        [\"man_in_manual_wheelchair_medium_dark_skin_tone\"] = \"👨🏾‍🦽\",\n        [\"man_in_manual_wheelchair_tone5\"] = \"👨🏿‍🦽\",\n        [\"man_in_manual_wheelchair_dark_skin_tone\"] = \"👨🏿‍🦽\",\n        [\"person_in_motorized_wheelchair\"] = \"🧑‍🦼\",\n        [\"person_in_motorized_wheelchair_tone1\"] = \"🧑🏻‍🦼\",\n        [\"person_in_motorized_wheelchair_light_skin_tone\"] = \"🧑🏻‍🦼\",\n        [\"person_in_motorized_wheelchair_tone2\"] = \"🧑🏼‍🦼\",\n        [\"person_in_motorized_wheelchair_medium_light_skin_tone\"] = \"🧑🏼‍🦼\",\n        [\"person_in_motorized_wheelchair_tone3\"] = \"🧑🏽‍🦼\",\n        [\"person_in_motorized_wheelchair_medium_skin_tone\"] = \"🧑🏽‍🦼\",\n        [\"person_in_motorized_wheelchair_tone4\"] = \"🧑🏾‍🦼\",\n        [\"person_in_motorized_wheelchair_medium_dark_skin_tone\"] = \"🧑🏾‍🦼\",\n        [\"person_in_motorized_wheelchair_tone5\"] = \"🧑🏿‍🦼\",\n        [\"person_in_motorized_wheelchair_dark_skin_tone\"] = \"🧑🏿‍🦼\",\n        [\"woman_in_motorized_wheelchair\"] = \"👩‍🦼\",\n        [\"woman_in_motorized_wheelchair_tone1\"] = \"👩🏻‍🦼\",\n        [\"woman_in_motorized_wheelchair_light_skin_tone\"] = \"👩🏻‍🦼\",\n        [\"woman_in_motorized_wheelchair_tone2\"] = \"👩🏼‍🦼\",\n        [\"woman_in_motorized_wheelchair_medium_light_skin_tone\"] = \"👩🏼‍🦼\",\n        [\"woman_in_motorized_wheelchair_tone3\"] = \"👩🏽‍🦼\",\n        [\"woman_in_motorized_wheelchair_medium_skin_tone\"] = \"👩🏽‍🦼\",\n        [\"woman_in_motorized_wheelchair_tone4\"] = \"👩🏾‍🦼\",\n        [\"woman_in_motorized_wheelchair_medium_dark_skin_tone\"] = \"👩🏾‍🦼\",\n        [\"woman_in_motorized_wheelchair_tone5\"] = \"👩🏿‍🦼\",\n        [\"woman_in_motorized_wheelchair_dark_skin_tone\"] = \"👩🏿‍🦼\",\n        [\"man_in_motorized_wheelchair\"] = \"👨‍🦼\",\n        [\"man_in_motorized_wheelchair_tone1\"] = \"👨🏻‍🦼\",\n        [\"man_in_motorized_wheelchair_light_skin_tone\"] = \"👨🏻‍🦼\",\n        [\"man_in_motorized_wheelchair_tone2\"] = \"👨🏼‍🦼\",\n        [\"man_in_motorized_wheelchair_medium_light_skin_tone\"] = \"👨🏼‍🦼\",\n        [\"man_in_motorized_wheelchair_tone3\"] = \"👨🏽‍🦼\",\n        [\"man_in_motorized_wheelchair_medium_skin_tone\"] = \"👨🏽‍🦼\",\n        [\"man_in_motorized_wheelchair_tone4\"] = \"👨🏾‍🦼\",\n        [\"man_in_motorized_wheelchair_medium_dark_skin_tone\"] = \"👨🏾‍🦼\",\n        [\"man_in_motorized_wheelchair_tone5\"] = \"👨🏿‍🦼\",\n        [\"man_in_motorized_wheelchair_dark_skin_tone\"] = \"👨🏿‍🦼\",\n        [\"person_walking\"] = \"🚶\",\n        [\"walking\"] = \"🚶\",\n        [\"person_walking_tone1\"] = \"🚶🏻\",\n        [\"walking_tone1\"] = \"🚶🏻\",\n        [\"person_walking_tone2\"] = \"🚶🏼\",\n        [\"walking_tone2\"] = \"🚶🏼\",\n        [\"person_walking_tone3\"] = \"🚶🏽\",\n        [\"walking_tone3\"] = \"🚶🏽\",\n        [\"person_walking_tone4\"] = \"🚶🏾\",\n        [\"walking_tone4\"] = \"🚶🏾\",\n        [\"person_walking_tone5\"] = \"🚶🏿\",\n        [\"walking_tone5\"] = \"🚶🏿\",\n        [\"woman_walking\"] = \"🚶‍♀️\",\n        [\"woman_walking_tone1\"] = \"🚶🏻‍♀️\",\n        [\"woman_walking_light_skin_tone\"] = \"🚶🏻‍♀️\",\n        [\"woman_walking_tone2\"] = \"🚶🏼‍♀️\",\n        [\"woman_walking_medium_light_skin_tone\"] = \"🚶🏼‍♀️\",\n        [\"woman_walking_tone3\"] = \"🚶🏽‍♀️\",\n        [\"woman_walking_medium_skin_tone\"] = \"🚶🏽‍♀️\",\n        [\"woman_walking_tone4\"] = \"🚶🏾‍♀️\",\n        [\"woman_walking_medium_dark_skin_tone\"] = \"🚶🏾‍♀️\",\n        [\"woman_walking_tone5\"] = \"🚶🏿‍♀️\",\n        [\"woman_walking_dark_skin_tone\"] = \"🚶🏿‍♀️\",\n        [\"man_walking\"] = \"🚶‍♂️\",\n        [\"man_walking_tone1\"] = \"🚶🏻‍♂️\",\n        [\"man_walking_light_skin_tone\"] = \"🚶🏻‍♂️\",\n        [\"man_walking_tone2\"] = \"🚶🏼‍♂️\",\n        [\"man_walking_medium_light_skin_tone\"] = \"🚶🏼‍♂️\",\n        [\"man_walking_tone3\"] = \"🚶🏽‍♂️\",\n        [\"man_walking_medium_skin_tone\"] = \"🚶🏽‍♂️\",\n        [\"man_walking_tone4\"] = \"🚶🏾‍♂️\",\n        [\"man_walking_medium_dark_skin_tone\"] = \"🚶🏾‍♂️\",\n        [\"man_walking_tone5\"] = \"🚶🏿‍♂️\",\n        [\"man_walking_dark_skin_tone\"] = \"🚶🏿‍♂️\",\n        [\"person_with_probing_cane\"] = \"🧑‍🦯\",\n        [\"person_with_probing_cane_tone1\"] = \"🧑🏻‍🦯\",\n        [\"person_with_probing_cane_light_skin_tone\"] = \"🧑🏻‍🦯\",\n        [\"person_with_probing_cane_tone2\"] = \"🧑🏼‍🦯\",\n        [\"person_with_probing_cane_medium_light_skin_tone\"] = \"🧑🏼‍🦯\",\n        [\"person_with_probing_cane_tone3\"] = \"🧑🏽‍🦯\",\n        [\"person_with_probing_cane_medium_skin_tone\"] = \"🧑🏽‍🦯\",\n        [\"person_with_probing_cane_tone4\"] = \"🧑🏾‍🦯\",\n        [\"person_with_probing_cane_medium_dark_skin_tone\"] = \"🧑🏾‍🦯\",\n        [\"person_with_probing_cane_tone5\"] = \"🧑🏿‍🦯\",\n        [\"person_with_probing_cane_dark_skin_tone\"] = \"🧑🏿‍🦯\",\n        [\"woman_with_probing_cane\"] = \"👩‍🦯\",\n        [\"woman_with_probing_cane_tone1\"] = \"👩🏻‍🦯\",\n        [\"woman_with_probing_cane_light_skin_tone\"] = \"👩🏻‍🦯\",\n        [\"woman_with_probing_cane_tone2\"] = \"👩🏼‍🦯\",\n        [\"woman_with_probing_cane_medium_light_skin_tone\"] = \"👩🏼‍🦯\",\n        [\"woman_with_probing_cane_tone3\"] = \"👩🏽‍🦯\",\n        [\"woman_with_probing_cane_medium_skin_tone\"] = \"👩🏽‍🦯\",\n        [\"woman_with_probing_cane_tone4\"] = \"👩🏾‍🦯\",\n        [\"woman_with_probing_cane_medium_dark_skin_tone\"] = \"👩🏾‍🦯\",\n        [\"woman_with_probing_cane_tone5\"] = \"👩🏿‍🦯\",\n        [\"woman_with_probing_cane_dark_skin_tone\"] = \"👩🏿‍🦯\",\n        [\"man_with_probing_cane\"] = \"👨‍🦯\",\n        [\"man_with_probing_cane_tone1\"] = \"👨🏻‍🦯\",\n        [\"man_with_probing_cane_light_skin_tone\"] = \"👨🏻‍🦯\",\n        [\"man_with_probing_cane_tone3\"] = \"👨🏽‍🦯\",\n        [\"man_with_probing_cane_medium_skin_tone\"] = \"👨🏽‍🦯\",\n        [\"man_with_probing_cane_tone2\"] = \"👨🏼‍🦯\",\n        [\"man_with_probing_cane_medium_light_skin_tone\"] = \"👨🏼‍🦯\",\n        [\"man_with_probing_cane_tone4\"] = \"👨🏾‍🦯\",\n        [\"man_with_probing_cane_medium_dark_skin_tone\"] = \"👨🏾‍🦯\",\n        [\"man_with_probing_cane_tone5\"] = \"👨🏿‍🦯\",\n        [\"man_with_probing_cane_dark_skin_tone\"] = \"👨🏿‍🦯\",\n        [\"person_kneeling\"] = \"🧎\",\n        [\"person_kneeling_tone1\"] = \"🧎🏻\",\n        [\"person_kneeling_light_skin_tone\"] = \"🧎🏻\",\n        [\"person_kneeling_tone2\"] = \"🧎🏼\",\n        [\"person_kneeling_medium_light_skin_tone\"] = \"🧎🏼\",\n        [\"person_kneeling_tone3\"] = \"🧎🏽\",\n        [\"person_kneeling_medium_skin_tone\"] = \"🧎🏽\",\n        [\"person_kneeling_tone4\"] = \"🧎🏾\",\n        [\"person_kneeling_medium_dark_skin_tone\"] = \"🧎🏾\",\n        [\"person_kneeling_tone5\"] = \"🧎🏿\",\n        [\"person_kneeling_dark_skin_tone\"] = \"🧎🏿\",\n        [\"woman_kneeling\"] = \"🧎‍♀️\",\n        [\"woman_kneeling_tone1\"] = \"🧎🏻‍♀️\",\n        [\"woman_kneeling_light_skin_tone\"] = \"🧎🏻‍♀️\",\n        [\"woman_kneeling_tone2\"] = \"🧎🏼‍♀️\",\n        [\"woman_kneeling_medium_light_skin_tone\"] = \"🧎🏼‍♀️\",\n        [\"woman_kneeling_tone3\"] = \"🧎🏽‍♀️\",\n        [\"woman_kneeling_medium_skin_tone\"] = \"🧎🏽‍♀️\",\n        [\"woman_kneeling_tone4\"] = \"🧎🏾‍♀️\",\n        [\"woman_kneeling_medium_dark_skin_tone\"] = \"🧎🏾‍♀️\",\n        [\"woman_kneeling_tone5\"] = \"🧎🏿‍♀️\",\n        [\"woman_kneeling_dark_skin_tone\"] = \"🧎🏿‍♀️\",\n        [\"man_kneeling\"] = \"🧎‍♂️\",\n        [\"man_kneeling_tone1\"] = \"🧎🏻‍♂️\",\n        [\"man_kneeling_light_skin_tone\"] = \"🧎🏻‍♂️\",\n        [\"man_kneeling_tone2\"] = \"🧎🏼‍♂️\",\n        [\"man_kneeling_medium_light_skin_tone\"] = \"🧎🏼‍♂️\",\n        [\"man_kneeling_tone3\"] = \"🧎🏽‍♂️\",\n        [\"man_kneeling_medium_skin_tone\"] = \"🧎🏽‍♂️\",\n        [\"man_kneeling_tone4\"] = \"🧎🏾‍♂️\",\n        [\"man_kneeling_medium_dark_skin_tone\"] = \"🧎🏾‍♂️\",\n        [\"man_kneeling_tone5\"] = \"🧎🏿‍♂️\",\n        [\"man_kneeling_dark_skin_tone\"] = \"🧎🏿‍♂️\",\n        [\"person_running\"] = \"🏃\",\n        [\"runner\"] = \"🏃\",\n        [\"person_running_tone1\"] = \"🏃🏻\",\n        [\"runner_tone1\"] = \"🏃🏻\",\n        [\"person_running_tone2\"] = \"🏃🏼\",\n        [\"runner_tone2\"] = \"🏃🏼\",\n        [\"person_running_tone3\"] = \"🏃🏽\",\n        [\"runner_tone3\"] = \"🏃🏽\",\n        [\"person_running_tone4\"] = \"🏃🏾\",\n        [\"runner_tone4\"] = \"🏃🏾\",\n        [\"person_running_tone5\"] = \"🏃🏿\",\n        [\"runner_tone5\"] = \"🏃🏿\",\n        [\"woman_running\"] = \"🏃‍♀️\",\n        [\"woman_running_tone1\"] = \"🏃🏻‍♀️\",\n        [\"woman_running_light_skin_tone\"] = \"🏃🏻‍♀️\",\n        [\"woman_running_tone2\"] = \"🏃🏼‍♀️\",\n        [\"woman_running_medium_light_skin_tone\"] = \"🏃🏼‍♀️\",\n        [\"woman_running_tone3\"] = \"🏃🏽‍♀️\",\n        [\"woman_running_medium_skin_tone\"] = \"🏃🏽‍♀️\",\n        [\"woman_running_tone4\"] = \"🏃🏾‍♀️\",\n        [\"woman_running_medium_dark_skin_tone\"] = \"🏃🏾‍♀️\",\n        [\"woman_running_tone5\"] = \"🏃🏿‍♀️\",\n        [\"woman_running_dark_skin_tone\"] = \"🏃🏿‍♀️\",\n        [\"man_running\"] = \"🏃‍♂️\",\n        [\"man_running_tone1\"] = \"🏃🏻‍♂️\",\n        [\"man_running_light_skin_tone\"] = \"🏃🏻‍♂️\",\n        [\"man_running_tone2\"] = \"🏃🏼‍♂️\",\n        [\"man_running_medium_light_skin_tone\"] = \"🏃🏼‍♂️\",\n        [\"man_running_tone3\"] = \"🏃🏽‍♂️\",\n        [\"man_running_medium_skin_tone\"] = \"🏃🏽‍♂️\",\n        [\"man_running_tone4\"] = \"🏃🏾‍♂️\",\n        [\"man_running_medium_dark_skin_tone\"] = \"🏃🏾‍♂️\",\n        [\"man_running_tone5\"] = \"🏃🏿‍♂️\",\n        [\"man_running_dark_skin_tone\"] = \"🏃🏿‍♂️\",\n        [\"person_standing\"] = \"🧍\",\n        [\"person_standing_tone1\"] = \"🧍🏻\",\n        [\"person_standing_light_skin_tone\"] = \"🧍🏻\",\n        [\"person_standing_tone2\"] = \"🧍🏼\",\n        [\"person_standing_medium_light_skin_tone\"] = \"🧍🏼\",\n        [\"person_standing_tone3\"] = \"🧍🏽\",\n        [\"person_standing_medium_skin_tone\"] = \"🧍🏽\",\n        [\"person_standing_tone4\"] = \"🧍🏾\",\n        [\"person_standing_medium_dark_skin_tone\"] = \"🧍🏾\",\n        [\"person_standing_tone5\"] = \"🧍🏿\",\n        [\"person_standing_dark_skin_tone\"] = \"🧍🏿\",\n        [\"woman_standing\"] = \"🧍‍♀️\",\n        [\"woman_standing_tone1\"] = \"🧍🏻‍♀️\",\n        [\"woman_standing_light_skin_tone\"] = \"🧍🏻‍♀️\",\n        [\"woman_standing_tone2\"] = \"🧍🏼‍♀️\",\n        [\"woman_standing_medium_light_skin_tone\"] = \"🧍🏼‍♀️\",\n        [\"woman_standing_tone3\"] = \"🧍🏽‍♀️\",\n        [\"woman_standing_medium_skin_tone\"] = \"🧍🏽‍♀️\",\n        [\"woman_standing_tone4\"] = \"🧍🏾‍♀️\",\n        [\"woman_standing_medium_dark_skin_tone\"] = \"🧍🏾‍♀️\",\n        [\"woman_standing_tone5\"] = \"🧍🏿‍♀️\",\n        [\"woman_standing_dark_skin_tone\"] = \"🧍🏿‍♀️\",\n        [\"man_standing\"] = \"🧍‍♂️\",\n        [\"man_standing_tone1\"] = \"🧍🏻‍♂️\",\n        [\"man_standing_light_skin_tone\"] = \"🧍🏻‍♂️\",\n        [\"man_standing_tone2\"] = \"🧍🏼‍♂️\",\n        [\"man_standing_medium_light_skin_tone\"] = \"🧍🏼‍♂️\",\n        [\"man_standing_tone3\"] = \"🧍🏽‍♂️\",\n        [\"man_standing_medium_skin_tone\"] = \"🧍🏽‍♂️\",\n        [\"man_standing_tone4\"] = \"🧍🏾‍♂️\",\n        [\"man_standing_medium_dark_skin_tone\"] = \"🧍🏾‍♂️\",\n        [\"man_standing_tone5\"] = \"🧍🏿‍♂️\",\n        [\"man_standing_dark_skin_tone\"] = \"🧍🏿‍♂️\",\n        [\"people_holding_hands\"] = \"🧑‍🤝‍🧑\",\n        [\"people_holding_hands_tone1\"] = \"🧑🏻‍🤝‍🧑🏻\",\n        [\"people_holding_hands_light_skin_tone\"] = \"🧑🏻‍🤝‍🧑🏻\",\n        [\"people_holding_hands_tone1_tone2\"] = \"🧑🏻‍🤝‍🧑🏼\",\n        [\"people_holding_hands_light_skin_tone_medium_light_skin_tone\"] = \"🧑🏻‍🤝‍🧑🏼\",\n        [\"people_holding_hands_tone1_tone3\"] = \"🧑🏻‍🤝‍🧑🏽\",\n        [\"people_holding_hands_light_skin_tone_medium_skin_tone\"] = \"🧑🏻‍🤝‍🧑🏽\",\n        [\"people_holding_hands_tone1_tone4\"] = \"🧑🏻‍🤝‍🧑🏾\",\n        [\"people_holding_hands_light_skin_tone_medium_dark_skin_tone\"] = \"🧑🏻‍🤝‍🧑🏾\",\n        [\"people_holding_hands_tone1_tone5\"] = \"🧑🏻‍🤝‍🧑🏿\",\n        [\"people_holding_hands_light_skin_tone_dark_skin_tone\"] = \"🧑🏻‍🤝‍🧑🏿\",\n        [\"people_holding_hands_tone2_tone1\"] = \"🧑🏼‍🤝‍🧑🏻\",\n        [\"people_holding_hands_medium_light_skin_tone_light_skin_tone\"] = \"🧑🏼‍🤝‍🧑🏻\",\n        [\"people_holding_hands_tone2\"] = \"🧑🏼‍🤝‍🧑🏼\",\n        [\"people_holding_hands_medium_light_skin_tone\"] = \"🧑🏼‍🤝‍🧑🏼\",\n        [\"people_holding_hands_tone2_tone3\"] = \"🧑🏼‍🤝‍🧑🏽\",\n        [\"people_holding_hands_medium_light_skin_tone_medium_skin_tone\"] = \"🧑🏼‍🤝‍🧑🏽\",\n        [\"people_holding_hands_tone2_tone4\"] = \"🧑🏼‍🤝‍🧑🏾\",\n        [\"people_holding_hands_medium_light_skin_tone_medium_dark_skin_tone\"] = \"🧑🏼‍🤝‍🧑🏾\",\n        [\"people_holding_hands_tone2_tone5\"] = \"🧑🏼‍🤝‍🧑🏿\",\n        [\"people_holding_hands_medium_light_skin_tone_dark_skin_tone\"] = \"🧑🏼‍🤝‍🧑🏿\",\n        [\"people_holding_hands_tone3_tone1\"] = \"🧑🏽‍🤝‍🧑🏻\",\n        [\"people_holding_hands_medium_skin_tone_light_skin_tone\"] = \"🧑🏽‍🤝‍🧑🏻\",\n        [\"people_holding_hands_tone3_tone2\"] = \"🧑🏽‍🤝‍🧑🏼\",\n        [\"people_holding_hands_medium_skin_tone_medium_light_skin_tone\"] = \"🧑🏽‍🤝‍🧑🏼\",\n        [\"people_holding_hands_tone3\"] = \"🧑🏽‍🤝‍🧑🏽\",\n        [\"people_holding_hands_medium_skin_tone\"] = \"🧑🏽‍🤝‍🧑🏽\",\n        [\"people_holding_hands_tone3_tone4\"] = \"🧑🏽‍🤝‍🧑🏾\",\n        [\"people_holding_hands_medium_skin_tone_medium_dark_skin_tone\"] = \"🧑🏽‍🤝‍🧑🏾\",\n        [\"people_holding_hands_tone3_tone5\"] = \"🧑🏽‍🤝‍🧑🏿\",\n        [\"people_holding_hands_medium_skin_tone_dark_skin_tone\"] = \"🧑🏽‍🤝‍🧑🏿\",\n        [\"people_holding_hands_tone4_tone1\"] = \"🧑🏾‍🤝‍🧑🏻\",\n        [\"people_holding_hands_medium_dark_skin_tone_light_skin_tone\"] = \"🧑🏾‍🤝‍🧑🏻\",\n        [\"people_holding_hands_tone4_tone2\"] = \"🧑🏾‍🤝‍🧑🏼\",\n        [\"people_holding_hands_medium_dark_skin_tone_medium_light_skin_tone\"] = \"🧑🏾‍🤝‍🧑🏼\",\n        [\"people_holding_hands_tone4_tone3\"] = \"🧑🏾‍🤝‍🧑🏽\",\n        [\"people_holding_hands_medium_dark_skin_tone_medium_skin_tone\"] = \"🧑🏾‍🤝‍🧑🏽\",\n        [\"people_holding_hands_tone4\"] = \"🧑🏾‍🤝‍🧑🏾\",\n        [\"people_holding_hands_medium_dark_skin_tone\"] = \"🧑🏾‍🤝‍🧑🏾\",\n        [\"people_holding_hands_tone4_tone5\"] = \"🧑🏾‍🤝‍🧑🏿\",\n        [\"people_holding_hands_medium_dark_skin_tone_dark_skin_tone\"] = \"🧑🏾‍🤝‍🧑🏿\",\n        [\"people_holding_hands_tone5_tone1\"] = \"🧑🏿‍🤝‍🧑🏻\",\n        [\"people_holding_hands_dark_skin_tone_light_skin_tone\"] = \"🧑🏿‍🤝‍🧑🏻\",\n        [\"people_holding_hands_tone5_tone2\"] = \"🧑🏿‍🤝‍🧑🏼\",\n        [\"people_holding_hands_dark_skin_tone_medium_light_skin_tone\"] = \"🧑🏿‍🤝‍🧑🏼\",\n        [\"people_holding_hands_tone5_tone3\"] = \"🧑🏿‍🤝‍🧑🏽\",\n        [\"people_holding_hands_dark_skin_tone_medium_skin_tone\"] = \"🧑🏿‍🤝‍🧑🏽\",\n        [\"people_holding_hands_tone5_tone4\"] = \"🧑🏿‍🤝‍🧑🏾\",\n        [\"people_holding_hands_dark_skin_tone_medium_dark_skin_tone\"] = \"🧑🏿‍🤝‍🧑🏾\",\n        [\"people_holding_hands_tone5\"] = \"🧑🏿‍🤝‍🧑🏿\",\n        [\"people_holding_hands_dark_skin_tone\"] = \"🧑🏿‍🤝‍🧑🏿\",\n        [\"couple\"] = \"👫\",\n        [\"woman_and_man_holding_hands_tone1\"] = \"👫🏻\",\n        [\"woman_and_man_holding_hands_light_skin_tone\"] = \"👫🏻\",\n        [\"woman_and_man_holding_hands_tone1_tone2\"] = \"👩🏻‍🤝‍👨🏼\",\n        [\"woman_and_man_holding_hands_light_skin_tone_medium_light_skin_tone\"] = \"👩🏻‍🤝‍👨🏼\",\n        [\"woman_and_man_holding_hands_tone1_tone3\"] = \"👩🏻‍🤝‍👨🏽\",\n        [\"woman_and_man_holding_hands_light_skin_tone_medium_skin_tone\"] = \"👩🏻‍🤝‍👨🏽\",\n        [\"woman_and_man_holding_hands_tone1_tone4\"] = \"👩🏻‍🤝‍👨🏾\",\n        [\"woman_and_man_holding_hands_light_skin_tone_medium_dark_skin_tone\"] = \"👩🏻‍🤝‍👨🏾\",\n        [\"woman_and_man_holding_hands_tone1_tone5\"] = \"👩🏻‍🤝‍👨🏿\",\n        [\"woman_and_man_holding_hands_light_skin_tone_dark_skin_tone\"] = \"👩🏻‍🤝‍👨🏿\",\n        [\"woman_and_man_holding_hands_tone2_tone1\"] = \"👩🏼‍🤝‍👨🏻\",\n        [\"woman_and_man_holding_hands_medium_light_skin_tone_light_skin_tone\"] = \"👩🏼‍🤝‍👨🏻\",\n        [\"woman_and_man_holding_hands_tone2\"] = \"👫🏼\",\n        [\"woman_and_man_holding_hands_medium_light_skin_tone\"] = \"👫🏼\",\n        [\"woman_and_man_holding_hands_tone2_tone3\"] = \"👩🏼‍🤝‍👨🏽\",\n        [\"woman_and_man_holding_hands_medium_light_skin_tone_medium_skin_tone\"] = \"👩🏼‍🤝‍👨🏽\",\n        [\"woman_and_man_holding_hands_tone2_tone4\"] = \"👩🏼‍🤝‍👨🏾\",\n        [\"woman_and_man_holding_hands_medium_light_skin_tone_medium_dark_skin_tone\"] =\n            \"👩🏼‍🤝‍👨🏾\",\n        [\"woman_and_man_holding_hands_tone2_tone5\"] = \"👩🏼‍🤝‍👨🏿\",\n        [\"woman_and_man_holding_hands_medium_light_skin_tone_dark_skin_tone\"] = \"👩🏼‍🤝‍👨🏿\",\n        [\"woman_and_man_holding_hands_tone3_tone1\"] = \"👩🏽‍🤝‍👨🏻\",\n        [\"woman_and_man_holding_hands_medium_skin_tone_light_skin_tone\"] = \"👩🏽‍🤝‍👨🏻\",\n        [\"woman_and_man_holding_hands_tone3_tone2\"] = \"👩🏽‍🤝‍👨🏼\",\n        [\"woman_and_man_holding_hands_medium_skin_tone_medium_light_skin_tone\"] = \"👩🏽‍🤝‍👨🏼\",\n        [\"woman_and_man_holding_hands_tone3\"] = \"👫🏽\",\n        [\"woman_and_man_holding_hands_medium_skin_tone\"] = \"👫🏽\",\n        [\"woman_and_man_holding_hands_tone3_tone4\"] = \"👩🏽‍🤝‍👨🏾\",\n        [\"woman_and_man_holding_hands_medium_skin_tone_medium_dark_skin_tone\"] = \"👩🏽‍🤝‍👨🏾\",\n        [\"woman_and_man_holding_hands_tone3_tone5\"] = \"👩🏽‍🤝‍👨🏿\",\n        [\"woman_and_man_holding_hands_medium_skin_tone_dark_skin_tone\"] = \"👩🏽‍🤝‍👨🏿\",\n        [\"woman_and_man_holding_hands_tone4_tone1\"] = \"👩🏾‍🤝‍👨🏻\",\n        [\"woman_and_man_holding_hands_medium_dark_skin_tone_light_skin_tone\"] = \"👩🏾‍🤝‍👨🏻\",\n        [\"woman_and_man_holding_hands_tone4_tone2\"] = \"👩🏾‍🤝‍👨🏼\",\n        [\"woman_and_man_holding_hands_medium_dark_skin_tone_medium_light_skin_tone\"] =\n            \"👩🏾‍🤝‍👨🏼\",\n        [\"woman_and_man_holding_hands_tone4_tone3\"] = \"👩🏾‍🤝‍👨🏽\",\n        [\"woman_and_man_holding_hands_medium_dark_skin_tone_medium_skin_tone\"] = \"👩🏾‍🤝‍👨🏽\",\n        [\"woman_and_man_holding_hands_tone4\"] = \"👫🏾\",\n        [\"woman_and_man_holding_hands_medium_dark_skin_tone\"] = \"👫🏾\",\n        [\"woman_and_man_holding_hands_tone4_tone5\"] = \"👩🏾‍🤝‍👨🏿\",\n        [\"woman_and_man_holding_hands_medium_dark_skin_tone_dark_skin_tone\"] = \"👩🏾‍🤝‍👨🏿\",\n        [\"woman_and_man_holding_hands_tone5_tone1\"] = \"👩🏿‍🤝‍👨🏻\",\n        [\"woman_and_man_holding_hands_dark_skin_tone_light_skin_tone\"] = \"👩🏿‍🤝‍👨🏻\",\n        [\"woman_and_man_holding_hands_tone5_tone2\"] = \"👩🏿‍🤝‍👨🏼\",\n        [\"woman_and_man_holding_hands_dark_skin_tone_medium_light_skin_tone\"] = \"👩🏿‍🤝‍👨🏼\",\n        [\"woman_and_man_holding_hands_tone5_tone3\"] = \"👩🏿‍🤝‍👨🏽\",\n        [\"woman_and_man_holding_hands_dark_skin_tone_medium_skin_tone\"] = \"👩🏿‍🤝‍👨🏽\",\n        [\"woman_and_man_holding_hands_tone5_tone4\"] = \"👩🏿‍🤝‍👨🏾\",\n        [\"woman_and_man_holding_hands_dark_skin_tone_medium_dark_skin_tone\"] = \"👩🏿‍🤝‍👨🏾\",\n        [\"woman_and_man_holding_hands_tone5\"] = \"👫🏿\",\n        [\"woman_and_man_holding_hands_dark_skin_tone\"] = \"👫🏿\",\n        [\"two_women_holding_hands\"] = \"👭\",\n        [\"women_holding_hands_tone1\"] = \"👭🏻\",\n        [\"women_holding_hands_light_skin_tone\"] = \"👭🏻\",\n        [\"women_holding_hands_tone1_tone2\"] = \"👩🏻‍🤝‍👩🏼\",\n        [\"women_holding_hands_light_skin_tone_medium_light_skin_tone\"] = \"👩🏻‍🤝‍👩🏼\",\n        [\"women_holding_hands_tone1_tone3\"] = \"👩🏻‍🤝‍👩🏽\",\n        [\"women_holding_hands_light_skin_tone_medium_skin_tone\"] = \"👩🏻‍🤝‍👩🏽\",\n        [\"women_holding_hands_tone1_tone4\"] = \"👩🏻‍🤝‍👩🏾\",\n        [\"women_holding_hands_light_skin_tone_medium_dark_skin_tone\"] = \"👩🏻‍🤝‍👩🏾\",\n        [\"women_holding_hands_tone1_tone5\"] = \"👩🏻‍🤝‍👩🏿\",\n        [\"women_holding_hands_light_skin_tone_dark_skin_tone\"] = \"👩🏻‍🤝‍👩🏿\",\n        [\"women_holding_hands_tone2_tone1\"] = \"👩🏼‍🤝‍👩🏻\",\n        [\"women_holding_hands_medium_light_skin_tone_light_skin_tone\"] = \"👩🏼‍🤝‍👩🏻\",\n        [\"women_holding_hands_tone2\"] = \"👭🏼\",\n        [\"women_holding_hands_medium_light_skin_tone\"] = \"👭🏼\",\n        [\"women_holding_hands_tone2_tone3\"] = \"👩🏼‍🤝‍👩🏽\",\n        [\"women_holding_hands_medium_light_skin_tone_medium_skin_tone\"] = \"👩🏼‍🤝‍👩🏽\",\n        [\"women_holding_hands_tone2_tone4\"] = \"👩🏼‍🤝‍👩🏾\",\n        [\"women_holding_hands_medium_light_skin_tone_medium_dark_skin_tone\"] = \"👩🏼‍🤝‍👩🏾\",\n        [\"women_holding_hands_tone2_tone5\"] = \"👩🏼‍🤝‍👩🏿\",\n        [\"women_holding_hands_medium_light_skin_tone_dark_skin_tone\"] = \"👩🏼‍🤝‍👩🏿\",\n        [\"women_holding_hands_tone3_tone1\"] = \"👩🏽‍🤝‍👩🏻\",\n        [\"women_holding_hands_medium_skin_tone_light_skin_tone\"] = \"👩🏽‍🤝‍👩🏻\",\n        [\"women_holding_hands_tone3_tone2\"] = \"👩🏽‍🤝‍👩🏼\",\n        [\"women_holding_hands_medium_skin_tone_medium_light_skin_tone\"] = \"👩🏽‍🤝‍👩🏼\",\n        [\"women_holding_hands_tone3\"] = \"👭🏽\",\n        [\"women_holding_hands_medium_skin_tone\"] = \"👭🏽\",\n        [\"women_holding_hands_tone3_tone4\"] = \"👩🏽‍🤝‍👩🏾\",\n        [\"women_holding_hands_medium_skin_tone_medium_dark_skin_tone\"] = \"👩🏽‍🤝‍👩🏾\",\n        [\"women_holding_hands_tone3_tone5\"] = \"👩🏽‍🤝‍👩🏿\",\n        [\"women_holding_hands_medium_skin_tone_dark_skin_tone\"] = \"👩🏽‍🤝‍👩🏿\",\n        [\"women_holding_hands_tone4_tone1\"] = \"👩🏾‍🤝‍👩🏻\",\n        [\"women_holding_hands_medium_dark_skin_tone_light_skin_tone\"] = \"👩🏾‍🤝‍👩🏻\",\n        [\"women_holding_hands_tone4_tone2\"] = \"👩🏾‍🤝‍👩🏼\",\n        [\"women_holding_hands_medium_dark_skin_tone_medium_light_skin_tone\"] = \"👩🏾‍🤝‍👩🏼\",\n        [\"women_holding_hands_tone4_tone3\"] = \"👩🏾‍🤝‍👩🏽\",\n        [\"women_holding_hands_medium_dark_skin_tone_medium_skin_tone\"] = \"👩🏾‍🤝‍👩🏽\",\n        [\"women_holding_hands_tone4\"] = \"👭🏾\",\n        [\"women_holding_hands_medium_dark_skin_tone\"] = \"👭🏾\",\n        [\"women_holding_hands_tone4_tone5\"] = \"👩🏾‍🤝‍👩🏿\",\n        [\"women_holding_hands_medium_dark_skin_tone_dark_skin_tone\"] = \"👩🏾‍🤝‍👩🏿\",\n        [\"women_holding_hands_tone5_tone1\"] = \"👩🏿‍🤝‍👩🏻\",\n        [\"women_holding_hands_dark_skin_tone_light_skin_tone\"] = \"👩🏿‍🤝‍👩🏻\",\n        [\"women_holding_hands_tone5_tone2\"] = \"👩🏿‍🤝‍👩🏼\",\n        [\"women_holding_hands_dark_skin_tone_medium_light_skin_tone\"] = \"👩🏿‍🤝‍👩🏼\",\n        [\"women_holding_hands_tone5_tone3\"] = \"👩🏿‍🤝‍👩🏽\",\n        [\"women_holding_hands_dark_skin_tone_medium_skin_tone\"] = \"👩🏿‍🤝‍👩🏽\",\n        [\"women_holding_hands_tone5_tone4\"] = \"👩🏿‍🤝‍👩🏾\",\n        [\"women_holding_hands_dark_skin_tone_medium_dark_skin_tone\"] = \"👩🏿‍🤝‍👩🏾\",\n        [\"women_holding_hands_tone5\"] = \"👭🏿\",\n        [\"women_holding_hands_dark_skin_tone\"] = \"👭🏿\",\n        [\"two_men_holding_hands\"] = \"👬\",\n        [\"men_holding_hands_tone1\"] = \"👬🏻\",\n        [\"men_holding_hands_light_skin_tone\"] = \"👬🏻\",\n        [\"men_holding_hands_tone1_tone2\"] = \"👨🏻‍🤝‍👨🏼\",\n        [\"men_holding_hands_light_skin_tone_medium_light_skin_tone\"] = \"👨🏻‍🤝‍👨🏼\",\n        [\"men_holding_hands_tone1_tone3\"] = \"👨🏻‍🤝‍👨🏽\",\n        [\"men_holding_hands_light_skin_tone_medium_skin_tone\"] = \"👨🏻‍🤝‍👨🏽\",\n        [\"men_holding_hands_tone1_tone4\"] = \"👨🏻‍🤝‍👨🏾\",\n        [\"men_holding_hands_light_skin_tone_medium_dark_skin_tone\"] = \"👨🏻‍🤝‍👨🏾\",\n        [\"men_holding_hands_tone1_tone5\"] = \"👨🏻‍🤝‍👨🏿\",\n        [\"men_holding_hands_light_skin_tone_dark_skin_tone\"] = \"👨🏻‍🤝‍👨🏿\",\n        [\"men_holding_hands_tone2_tone1\"] = \"👨🏼‍🤝‍👨🏻\",\n        [\"men_holding_hands_medium_light_skin_tone_light_skin_tone\"] = \"👨🏼‍🤝‍👨🏻\",\n        [\"men_holding_hands_tone2\"] = \"👬🏼\",\n        [\"men_holding_hands_medium_light_skin_tone\"] = \"👬🏼\",\n        [\"men_holding_hands_tone2_tone3\"] = \"👨🏼‍🤝‍👨🏽\",\n        [\"men_holding_hands_medium_light_skin_tone_medium_skin_tone\"] = \"👨🏼‍🤝‍👨🏽\",\n        [\"men_holding_hands_tone2_tone4\"] = \"👨🏼‍🤝‍👨🏾\",\n        [\"men_holding_hands_medium_light_skin_tone_medium_dark_skin_tone\"] = \"👨🏼‍🤝‍👨🏾\",\n        [\"men_holding_hands_tone2_tone5\"] = \"👨🏼‍🤝‍👨🏿\",\n        [\"men_holding_hands_medium_light_skin_tone_dark_skin_tone\"] = \"👨🏼‍🤝‍👨🏿\",\n        [\"men_holding_hands_tone3_tone1\"] = \"👨🏽‍🤝‍👨🏻\",\n        [\"men_holding_hands_medium_skin_tone_light_skin_tone\"] = \"👨🏽‍🤝‍👨🏻\",\n        [\"men_holding_hands_tone3_tone2\"] = \"👨🏽‍🤝‍👨🏼\",\n        [\"men_holding_hands_medium_skin_tone_medium_light_skin_tone\"] = \"👨🏽‍🤝‍👨🏼\",\n        [\"men_holding_hands_tone3\"] = \"👬🏽\",\n        [\"men_holding_hands_medium_skin_tone\"] = \"👬🏽\",\n        [\"men_holding_hands_tone3_tone4\"] = \"👨🏽‍🤝‍👨🏾\",\n        [\"men_holding_hands_medium_skin_tone_medium_dark_skin_tone\"] = \"👨🏽‍🤝‍👨🏾\",\n        [\"men_holding_hands_tone3_tone5\"] = \"👨🏽‍🤝‍👨🏿\",\n        [\"men_holding_hands_medium_skin_tone_dark_skin_tone\"] = \"👨🏽‍🤝‍👨🏿\",\n        [\"men_holding_hands_tone4_tone1\"] = \"👨🏾‍🤝‍👨🏻\",\n        [\"men_holding_hands_medium_dark_skin_tone_light_skin_tone\"] = \"👨🏾‍🤝‍👨🏻\",\n        [\"men_holding_hands_tone4_tone2\"] = \"👨🏾‍🤝‍👨🏼\",\n        [\"men_holding_hands_medium_dark_skin_tone_medium_light_skin_tone\"] = \"👨🏾‍🤝‍👨🏼\",\n        [\"men_holding_hands_tone4_tone3\"] = \"👨🏾‍🤝‍👨🏽\",\n        [\"men_holding_hands_medium_dark_skin_tone_medium_skin_tone\"] = \"👨🏾‍🤝‍👨🏽\",\n        [\"men_holding_hands_tone4\"] = \"👬🏾\",\n        [\"men_holding_hands_medium_dark_skin_tone\"] = \"👬🏾\",\n        [\"men_holding_hands_tone4_tone5\"] = \"👨🏾‍🤝‍👨🏿\",\n        [\"men_holding_hands_medium_dark_skin_tone_dark_skin_tone\"] = \"👨🏾‍🤝‍👨🏿\",\n        [\"men_holding_hands_tone5_tone1\"] = \"👨🏿‍🤝‍👨🏻\",\n        [\"men_holding_hands_dark_skin_tone_light_skin_tone\"] = \"👨🏿‍🤝‍👨🏻\",\n        [\"men_holding_hands_tone5_tone2\"] = \"👨🏿‍🤝‍👨🏼\",\n        [\"men_holding_hands_dark_skin_tone_medium_light_skin_tone\"] = \"👨🏿‍🤝‍👨🏼\",\n        [\"men_holding_hands_tone5_tone3\"] = \"👨🏿‍🤝‍👨🏽\",\n        [\"men_holding_hands_dark_skin_tone_medium_skin_tone\"] = \"👨🏿‍🤝‍👨🏽\",\n        [\"men_holding_hands_tone5_tone4\"] = \"👨🏿‍🤝‍👨🏾\",\n        [\"men_holding_hands_dark_skin_tone_medium_dark_skin_tone\"] = \"👨🏿‍🤝‍👨🏾\",\n        [\"men_holding_hands_tone5\"] = \"👬🏿\",\n        [\"men_holding_hands_dark_skin_tone\"] = \"👬🏿\",\n        [\"couple_with_heart\"] = \"💑\",\n        [\"couple_with_heart_tone1\"] = \"💑🏻\",\n        [\"couple_with_heart_light_skin_tone\"] = \"💑🏻\",\n        [\"couple_with_heart_person_person_tone1_tone2\"] = \"🧑🏻‍❤️‍🧑🏼\",\n        [\"couple_with_heart_person_person_light_skin_tone_medium_light_skin_tone\"] = \"🧑🏻‍❤️‍🧑🏼\",\n        [\"couple_with_heart_person_person_tone1_tone3\"] = \"🧑🏻‍❤️‍🧑🏽\",\n        [\"couple_with_heart_person_person_light_skin_tone_medium_skin_tone\"] = \"🧑🏻‍❤️‍🧑🏽\",\n        [\"couple_with_heart_person_person_tone1_tone4\"] = \"🧑🏻‍❤️‍🧑🏾\",\n        [\"couple_with_heart_person_person_light_skin_tone_medium_dark_skin_tone\"] = \"🧑🏻‍❤️‍🧑🏾\",\n        [\"couple_with_heart_person_person_tone1_tone5\"] = \"🧑🏻‍❤️‍🧑🏿\",\n        [\"couple_with_heart_person_person_light_skin_tone_dark_skin_tone\"] = \"🧑🏻‍❤️‍🧑🏿\",\n        [\"couple_with_heart_person_person_tone2_tone1\"] = \"🧑🏼‍❤️‍🧑🏻\",\n        [\"couple_with_heart_person_person_medium_light_skin_tone_light_skin_tone\"] = \"🧑🏼‍❤️‍🧑🏻\",\n        [\"couple_with_heart_tone2\"] = \"💑🏼\",\n        [\"couple_with_heart_medium_light_skin_tone\"] = \"💑🏼\",\n        [\"couple_with_heart_person_person_tone2_tone3\"] = \"🧑🏼‍❤️‍🧑🏽\",\n        [\"couple_with_heart_person_person_medium_light_skin_tone_medium_skin_tone\"] =\n            \"🧑🏼‍❤️‍🧑🏽\",\n        [\"couple_with_heart_person_person_tone2_tone4\"] = \"🧑🏼‍❤️‍🧑🏾\",\n        [\"couple_with_heart_person_person_medium_light_skin_tone_medium_dark_skin_tone\"] =\n            \"🧑🏼‍❤️‍🧑🏾\",\n        [\"couple_with_heart_person_person_tone2_tone5\"] = \"🧑🏼‍❤️‍🧑🏿\",\n        [\"couple_with_heart_person_person_medium_light_skin_tone_dark_skin_tone\"] = \"🧑🏼‍❤️‍🧑🏿\",\n        [\"couple_with_heart_person_person_tone3_tone1\"] = \"🧑🏽‍❤️‍🧑🏻\",\n        [\"couple_with_heart_person_person_medium_skin_tone_light_skin_tone\"] = \"🧑🏽‍❤️‍🧑🏻\",\n        [\"couple_with_heart_person_person_tone3_tone2\"] = \"🧑🏽‍❤️‍🧑🏼\",\n        [\"couple_with_heart_person_person_medium_skin_tone_medium_light_skin_tone\"] =\n            \"🧑🏽‍❤️‍🧑🏼\",\n        [\"couple_with_heart_tone3\"] = \"💑🏽\",\n        [\"couple_with_heart_medium_skin_tone\"] = \"💑🏽\",\n        [\"couple_with_heart_person_person_tone3_tone4\"] = \"🧑🏽‍❤️‍🧑🏾\",\n        [\"couple_with_heart_person_person_medium_skin_tone_medium_dark_skin_tone\"] = \"🧑🏽‍❤️‍🧑🏾\",\n        [\"couple_with_heart_person_person_tone3_tone5\"] = \"🧑🏽‍❤️‍🧑🏿\",\n        [\"couple_with_heart_person_person_medium_skin_tone_dark_skin_tone\"] = \"🧑🏽‍❤️‍🧑🏿\",\n        [\"couple_with_heart_person_person_tone4_tone1\"] = \"🧑🏾‍❤️‍🧑🏻\",\n        [\"couple_with_heart_person_person_medium_dark_skin_tone_light_skin_tone\"] = \"🧑🏾‍❤️‍🧑🏻\",\n        [\"couple_with_heart_person_person_tone4_tone2\"] = \"🧑🏾‍❤️‍🧑🏼\",\n        [\"couple_with_heart_person_person_medium_dark_skin_tone_medium_light_skin_tone\"] =\n            \"🧑🏾‍❤️‍🧑🏼\",\n        [\"couple_with_heart_person_person_tone4_tone3\"] = \"🧑🏾‍❤️‍🧑🏽\",\n        [\"couple_with_heart_person_person_medium_dark_skin_tone_medium_skin_tone\"] = \"🧑🏾‍❤️‍🧑🏽\",\n        [\"couple_with_heart_tone4\"] = \"💑🏾\",\n        [\"couple_with_heart_medium_dark_skin_tone\"] = \"💑🏾\",\n        [\"couple_with_heart_person_person_tone4_tone5\"] = \"🧑🏾‍❤️‍🧑🏿\",\n        [\"couple_with_heart_person_person_medium_dark_skin_tone_dark_skin_tone\"] = \"🧑🏾‍❤️‍🧑🏿\",\n        [\"couple_with_heart_person_person_tone5_tone1\"] = \"🧑🏿‍❤️‍🧑🏻\",\n        [\"couple_with_heart_person_person_dark_skin_tone_light_skin_tone\"] = \"🧑🏿‍❤️‍🧑🏻\",\n        [\"couple_with_heart_person_person_tone5_tone2\"] = \"🧑🏿‍❤️‍🧑🏼\",\n        [\"couple_with_heart_person_person_dark_skin_tone_medium_light_skin_tone\"] = \"🧑🏿‍❤️‍🧑🏼\",\n        [\"couple_with_heart_person_person_tone5_tone3\"] = \"🧑🏿‍❤️‍🧑🏽\",\n        [\"couple_with_heart_person_person_dark_skin_tone_medium_skin_tone\"] = \"🧑🏿‍❤️‍🧑🏽\",\n        [\"couple_with_heart_person_person_tone5_tone4\"] = \"🧑🏿‍❤️‍🧑🏾\",\n        [\"couple_with_heart_person_person_dark_skin_tone_medium_dark_skin_tone\"] = \"🧑🏿‍❤️‍🧑🏾\",\n        [\"couple_with_heart_tone5\"] = \"💑🏿\",\n        [\"couple_with_heart_dark_skin_tone\"] = \"💑🏿\",\n        [\"couple_with_heart_woman_man\"] = \"👩‍❤️‍👨\",\n        [\"couple_with_heart_woman_man_tone1\"] = \"👩🏻‍❤️‍👨🏻\",\n        [\"couple_with_heart_woman_man_light_skin_tone\"] = \"👩🏻‍❤️‍👨🏻\",\n        [\"couple_with_heart_woman_man_tone1_tone2\"] = \"👩🏻‍❤️‍👨🏼\",\n        [\"couple_with_heart_woman_man_light_skin_tone_medium_light_skin_tone\"] = \"👩🏻‍❤️‍👨🏼\",\n        [\"couple_with_heart_woman_man_tone1_tone3\"] = \"👩🏻‍❤️‍👨🏽\",\n        [\"couple_with_heart_woman_man_light_skin_tone_medium_skin_tone\"] = \"👩🏻‍❤️‍👨🏽\",\n        [\"couple_with_heart_woman_man_tone1_tone4\"] = \"👩🏻‍❤️‍👨🏾\",\n        [\"couple_with_heart_woman_man_light_skin_tone_medium_dark_skin_tone\"] = \"👩🏻‍❤️‍👨🏾\",\n        [\"couple_with_heart_woman_man_tone1_tone5\"] = \"👩🏻‍❤️‍👨🏿\",\n        [\"couple_with_heart_woman_man_light_skin_tone_dark_skin_tone\"] = \"👩🏻‍❤️‍👨🏿\",\n        [\"couple_with_heart_woman_man_tone2_tone1\"] = \"👩🏼‍❤️‍👨🏻\",\n        [\"couple_with_heart_woman_man_medium_light_skin_tone_light_skin_tone\"] = \"👩🏼‍❤️‍👨🏻\",\n        [\"couple_with_heart_woman_man_tone2\"] = \"👩🏼‍❤️‍👨🏼\",\n        [\"couple_with_heart_woman_man_medium_light_skin_tone\"] = \"👩🏼‍❤️‍👨🏼\",\n        [\"couple_with_heart_woman_man_tone2_tone3\"] = \"👩🏼‍❤️‍👨🏽\",\n        [\"couple_with_heart_woman_man_medium_light_skin_tone_medium_skin_tone\"] = \"👩🏼‍❤️‍👨🏽\",\n        [\"couple_with_heart_woman_man_tone2_tone4\"] = \"👩🏼‍❤️‍👨🏾\",\n        [\"couple_with_heart_woman_man_medium_light_skin_tone_medium_dark_skin_tone\"] =\n            \"👩🏼‍❤️‍👨🏾\",\n        [\"couple_with_heart_woman_man_tone2_tone5\"] = \"👩🏼‍❤️‍👨🏿\",\n        [\"couple_with_heart_woman_man_medium_light_skin_tone_dark_skin_tone\"] = \"👩🏼‍❤️‍👨🏿\",\n        [\"couple_with_heart_woman_man_tone3_tone1\"] = \"👩🏽‍❤️‍👨🏻\",\n        [\"couple_with_heart_woman_man_medium_skin_tone_light_skin_tone\"] = \"👩🏽‍❤️‍👨🏻\",\n        [\"couple_with_heart_woman_man_tone3_tone2\"] = \"👩🏽‍❤️‍👨🏼\",\n        [\"couple_with_heart_woman_man_medium_skin_tone_medium_light_skin_tone\"] = \"👩🏽‍❤️‍👨🏼\",\n        [\"couple_with_heart_woman_man_tone3\"] = \"👩🏽‍❤️‍👨🏽\",\n        [\"couple_with_heart_woman_man_medium_skin_tone\"] = \"👩🏽‍❤️‍👨🏽\",\n        [\"couple_with_heart_woman_man_tone3_tone4\"] = \"👩🏽‍❤️‍👨🏾\",\n        [\"couple_with_heart_woman_man_medium_skin_tone_medium_dark_skin_tone\"] = \"👩🏽‍❤️‍👨🏾\",\n        [\"couple_with_heart_woman_man_tone3_tone5\"] = \"👩🏽‍❤️‍👨🏿\",\n        [\"couple_with_heart_woman_man_medium_skin_tone_dark_skin_tone\"] = \"👩🏽‍❤️‍👨🏿\",\n        [\"couple_with_heart_woman_man_tone4_tone1\"] = \"👩🏾‍❤️‍👨🏻\",\n        [\"couple_with_heart_woman_man_medium_dark_skin_tone_light_skin_tone\"] = \"👩🏾‍❤️‍👨🏻\",\n        [\"couple_with_heart_woman_man_tone4_tone2\"] = \"👩🏾‍❤️‍👨🏼\",\n        [\"couple_with_heart_woman_man_medium_dark_skin_tone_medium_light_skin_tone\"] =\n            \"👩🏾‍❤️‍👨🏼\",\n        [\"couple_with_heart_woman_man_tone4_tone3\"] = \"👩🏾‍❤️‍👨🏽\",\n        [\"couple_with_heart_woman_man_medium_dark_skin_tone_medium_skin_tone\"] = \"👩🏾‍❤️‍👨🏽\",\n        [\"couple_with_heart_woman_man_tone4\"] = \"👩🏾‍❤️‍👨🏾\",\n        [\"couple_with_heart_woman_man_medium_dark_skin_tone\"] = \"👩🏾‍❤️‍👨🏾\",\n        [\"couple_with_heart_woman_man_tone4_tone5\"] = \"👩🏾‍❤️‍👨🏿\",\n        [\"couple_with_heart_woman_man_medium_dark_skin_tone_dark_skin_tone\"] = \"👩🏾‍❤️‍👨🏿\",\n        [\"couple_with_heart_woman_man_tone5_tone1\"] = \"👩🏿‍❤️‍👨🏻\",\n        [\"couple_with_heart_woman_man_dark_skin_tone_light_skin_tone\"] = \"👩🏿‍❤️‍👨🏻\",\n        [\"couple_with_heart_woman_man_tone5_tone2\"] = \"👩🏿‍❤️‍👨🏼\",\n        [\"couple_with_heart_woman_man_dark_skin_tone_medium_light_skin_tone\"] = \"👩🏿‍❤️‍👨🏼\",\n        [\"couple_with_heart_woman_man_tone5_tone3\"] = \"👩🏿‍❤️‍👨🏽\",\n        [\"couple_with_heart_woman_man_dark_skin_tone_medium_skin_tone\"] = \"👩🏿‍❤️‍👨🏽\",\n        [\"couple_with_heart_woman_man_tone5_tone4\"] = \"👩🏿‍❤️‍👨🏾\",\n        [\"couple_with_heart_woman_man_dark_skin_tone_medium_dark_skin_tone\"] = \"👩🏿‍❤️‍👨🏾\",\n        [\"couple_with_heart_woman_man_tone5\"] = \"👩🏿‍❤️‍👨🏿\",\n        [\"couple_with_heart_woman_man_dark_skin_tone\"] = \"👩🏿‍❤️‍👨🏿\",\n        [\"couple_ww\"] = \"👩‍❤️‍👩\",\n        [\"couple_with_heart_ww\"] = \"👩‍❤️‍👩\",\n        [\"couple_with_heart_woman_woman_tone1\"] = \"👩🏻‍❤️‍👩🏻\",\n        [\"couple_with_heart_woman_woman_light_skin_tone\"] = \"👩🏻‍❤️‍👩🏻\",\n        [\"couple_with_heart_woman_woman_tone1_tone2\"] = \"👩🏻‍❤️‍👩🏼\",\n        [\"couple_with_heart_woman_woman_light_skin_tone_medium_light_skin_tone\"] = \"👩🏻‍❤️‍👩🏼\",\n        [\"couple_with_heart_woman_woman_tone1_tone3\"] = \"👩🏻‍❤️‍👩🏽\",\n        [\"couple_with_heart_woman_woman_light_skin_tone_medium_skin_tone\"] = \"👩🏻‍❤️‍👩🏽\",\n        [\"couple_with_heart_woman_woman_tone1_tone4\"] = \"👩🏻‍❤️‍👩🏾\",\n        [\"couple_with_heart_woman_woman_light_skin_tone_medium_dark_skin_tone\"] = \"👩🏻‍❤️‍👩🏾\",\n        [\"couple_with_heart_woman_woman_tone1_tone5\"] = \"👩🏻‍❤️‍👩🏿\",\n        [\"couple_with_heart_woman_woman_light_skin_tone_dark_skin_tone\"] = \"👩🏻‍❤️‍👩🏿\",\n        [\"couple_with_heart_woman_woman_tone2_tone1\"] = \"👩🏼‍❤️‍👩🏻\",\n        [\"couple_with_heart_woman_woman_medium_light_skin_tone_light_skin_tone\"] = \"👩🏼‍❤️‍👩🏻\",\n        [\"couple_with_heart_woman_woman_tone2\"] = \"👩🏼‍❤️‍👩🏼\",\n        [\"couple_with_heart_woman_woman_medium_light_skin_tone\"] = \"👩🏼‍❤️‍👩🏼\",\n        [\"couple_with_heart_woman_woman_tone2_tone3\"] = \"👩🏼‍❤️‍👩🏽\",\n        [\"couple_with_heart_woman_woman_medium_light_skin_tone_medium_skin_tone\"] = \"👩🏼‍❤️‍👩🏽\",\n        [\"couple_with_heart_woman_woman_tone2_tone4\"] = \"👩🏼‍❤️‍👩🏾\",\n        [\"couple_with_heart_woman_woman_medium_light_skin_tone_medium_dark_skin_tone\"] =\n            \"👩🏼‍❤️‍👩🏾\",\n        [\"couple_with_heart_woman_woman_tone2_tone5\"] = \"👩🏼‍❤️‍👩🏿\",\n        [\"couple_with_heart_woman_woman_medium_light_skin_tone_dark_skin_tone\"] = \"👩🏼‍❤️‍👩🏿\",\n        [\"couple_with_heart_woman_woman_tone3_tone1\"] = \"👩🏽‍❤️‍👩🏻\",\n        [\"couple_with_heart_woman_woman_medium_skin_tone_light_skin_tone\"] = \"👩🏽‍❤️‍👩🏻\",\n        [\"couple_with_heart_woman_woman_tone3_tone2\"] = \"👩🏽‍❤️‍👩🏼\",\n        [\"couple_with_heart_woman_woman_medium_skin_tone_medium_light_skin_tone\"] = \"👩🏽‍❤️‍👩🏼\",\n        [\"couple_with_heart_woman_woman_tone3\"] = \"👩🏽‍❤️‍👩🏽\",\n        [\"couple_with_heart_woman_woman_medium_skin_tone\"] = \"👩🏽‍❤️‍👩🏽\",\n        [\"couple_with_heart_woman_woman_tone3_tone4\"] = \"👩🏽‍❤️‍👩🏾\",\n        [\"couple_with_heart_woman_woman_medium_skin_tone_medium_dark_skin_tone\"] = \"👩🏽‍❤️‍👩🏾\",\n        [\"couple_with_heart_woman_woman_tone3_tone5\"] = \"👩🏽‍❤️‍👩🏿\",\n        [\"couple_with_heart_woman_woman_medium_skin_tone_dark_skin_tone\"] = \"👩🏽‍❤️‍👩🏿\",\n        [\"couple_with_heart_woman_woman_tone4_tone1\"] = \"👩🏾‍❤️‍👩🏻\",\n        [\"couple_with_heart_woman_woman_medium_dark_skin_tone_light_skin_tone\"] = \"👩🏾‍❤️‍👩🏻\",\n        [\"couple_with_heart_woman_woman_tone4_tone2\"] = \"👩🏾‍❤️‍👩🏼\",\n        [\"couple_with_heart_woman_woman_medium_dark_skin_tone_medium_light_skin_tone\"] =\n            \"👩🏾‍❤️‍👩🏼\",\n        [\"couple_with_heart_woman_woman_tone4_tone3\"] = \"👩🏾‍❤️‍👩🏽\",\n        [\"couple_with_heart_woman_woman_medium_dark_skin_tone_medium_skin_tone\"] = \"👩🏾‍❤️‍👩🏽\",\n        [\"couple_with_heart_woman_woman_tone4\"] = \"👩🏾‍❤️‍👩🏾\",\n        [\"couple_with_heart_woman_woman_medium_dark_skin_tone\"] = \"👩🏾‍❤️‍👩🏾\",\n        [\"couple_with_heart_woman_woman_tone4_tone5\"] = \"👩🏾‍❤️‍👩🏿\",\n        [\"couple_with_heart_woman_woman_medium_dark_skin_tone_dark_skin_tone\"] = \"👩🏾‍❤️‍👩🏿\",\n        [\"couple_with_heart_woman_woman_tone5_tone1\"] = \"👩🏿‍❤️‍👩🏻\",\n        [\"couple_with_heart_woman_woman_dark_skin_tone_light_skin_tone\"] = \"👩🏿‍❤️‍👩🏻\",\n        [\"couple_with_heart_woman_woman_tone5_tone2\"] = \"👩🏿‍❤️‍👩🏼\",\n        [\"couple_with_heart_woman_woman_dark_skin_tone_medium_light_skin_tone\"] = \"👩🏿‍❤️‍👩🏼\",\n        [\"couple_with_heart_woman_woman_tone5_tone3\"] = \"👩🏿‍❤️‍👩🏽\",\n        [\"couple_with_heart_woman_woman_dark_skin_tone_medium_skin_tone\"] = \"👩🏿‍❤️‍👩🏽\",\n        [\"couple_with_heart_woman_woman_tone5_tone4\"] = \"👩🏿‍❤️‍👩🏾\",\n        [\"couple_with_heart_woman_woman_dark_skin_tone_medium_dark_skin_tone\"] = \"👩🏿‍❤️‍👩🏾\",\n        [\"couple_with_heart_woman_woman_tone5\"] = \"👩🏿‍❤️‍👩🏿\",\n        [\"couple_with_heart_woman_woman_dark_skin_tone\"] = \"👩🏿‍❤️‍👩🏿\",\n        [\"couple_mm\"] = \"👨‍❤️‍👨\",\n        [\"couple_with_heart_mm\"] = \"👨‍❤️‍👨\",\n        [\"couple_with_heart_man_man_tone1\"] = \"👨🏻‍❤️‍👨🏻\",\n        [\"couple_with_heart_man_man_light_skin_tone\"] = \"👨🏻‍❤️‍👨🏻\",\n        [\"couple_with_heart_man_man_tone1_tone2\"] = \"👨🏻‍❤️‍👨🏼\",\n        [\"couple_with_heart_man_man_light_skin_tone_medium_light_skin_tone\"] = \"👨🏻‍❤️‍👨🏼\",\n        [\"couple_with_heart_man_man_tone1_tone3\"] = \"👨🏻‍❤️‍👨🏽\",\n        [\"couple_with_heart_man_man_light_skin_tone_medium_skin_tone\"] = \"👨🏻‍❤️‍👨🏽\",\n        [\"couple_with_heart_man_man_tone1_tone4\"] = \"👨🏻‍❤️‍👨🏾\",\n        [\"couple_with_heart_man_man_light_skin_tone_medium_dark_skin_tone\"] = \"👨🏻‍❤️‍👨🏾\",\n        [\"couple_with_heart_man_man_tone1_tone5\"] = \"👨🏻‍❤️‍👨🏿\",\n        [\"couple_with_heart_man_man_light_skin_tone_dark_skin_tone\"] = \"👨🏻‍❤️‍👨🏿\",\n        [\"couple_with_heart_man_man_tone2_tone1\"] = \"👨🏼‍❤️‍👨🏻\",\n        [\"couple_with_heart_man_man_medium_light_skin_tone_light_skin_tone\"] = \"👨🏼‍❤️‍👨🏻\",\n        [\"couple_with_heart_man_man_tone2\"] = \"👨🏼‍❤️‍👨🏼\",\n        [\"couple_with_heart_man_man_medium_light_skin_tone\"] = \"👨🏼‍❤️‍👨🏼\",\n        [\"couple_with_heart_man_man_tone2_tone3\"] = \"👨🏼‍❤️‍👨🏽\",\n        [\"couple_with_heart_man_man_medium_light_skin_tone_medium_skin_tone\"] = \"👨🏼‍❤️‍👨🏽\",\n        [\"couple_with_heart_man_man_tone2_tone4\"] = \"👨🏼‍❤️‍👨🏾\",\n        [\"couple_with_heart_man_man_medium_light_skin_tone_medium_dark_skin_tone\"] = \"👨🏼‍❤️‍👨🏾\",\n        [\"couple_with_heart_man_man_tone2_tone5\"] = \"👨🏼‍❤️‍👨🏿\",\n        [\"couple_with_heart_man_man_medium_light_skin_tone_dark_skin_tone\"] = \"👨🏼‍❤️‍👨🏿\",\n        [\"couple_with_heart_man_man_tone3_tone1\"] = \"👨🏽‍❤️‍👨🏻\",\n        [\"couple_with_heart_man_man_medium_skin_tone_light_skin_tone\"] = \"👨🏽‍❤️‍👨🏻\",\n        [\"couple_with_heart_man_man_tone3_tone2\"] = \"👨🏽‍❤️‍👨🏼\",\n        [\"couple_with_heart_man_man_medium_skin_tone_medium_light_skin_tone\"] = \"👨🏽‍❤️‍👨🏼\",\n        [\"couple_with_heart_man_man_tone3\"] = \"👨🏽‍❤️‍👨🏽\",\n        [\"couple_with_heart_man_man_medium_skin_tone\"] = \"👨🏽‍❤️‍👨🏽\",\n        [\"couple_with_heart_man_man_tone3_tone4\"] = \"👨🏽‍❤️‍👨🏾\",\n        [\"couple_with_heart_man_man_medium_skin_tone_medium_dark_skin_tone\"] = \"👨🏽‍❤️‍👨🏾\",\n        [\"couple_with_heart_man_man_tone3_tone5\"] = \"👨🏽‍❤️‍👨🏿\",\n        [\"couple_with_heart_man_man_medium_skin_tone_dark_skin_tone\"] = \"👨🏽‍❤️‍👨🏿\",\n        [\"couple_with_heart_man_man_tone4_tone1\"] = \"👨🏾‍❤️‍👨🏻\",\n        [\"couple_with_heart_man_man_medium_dark_skin_tone_light_skin_tone\"] = \"👨🏾‍❤️‍👨🏻\",\n        [\"couple_with_heart_man_man_tone4_tone2\"] = \"👨🏾‍❤️‍👨🏼\",\n        [\"couple_with_heart_man_man_medium_dark_skin_tone_medium_light_skin_tone\"] = \"👨🏾‍❤️‍👨🏼\",\n        [\"couple_with_heart_man_man_tone4_tone3\"] = \"👨🏾‍❤️‍👨🏽\",\n        [\"couple_with_heart_man_man_medium_dark_skin_tone_medium_skin_tone\"] = \"👨🏾‍❤️‍👨🏽\",\n        [\"couple_with_heart_man_man_tone4\"] = \"👨🏾‍❤️‍👨🏾\",\n        [\"couple_with_heart_man_man_medium_dark_skin_tone\"] = \"👨🏾‍❤️‍👨🏾\",\n        [\"couple_with_heart_man_man_tone4_tone5\"] = \"👨🏾‍❤️‍👨🏿\",\n        [\"couple_with_heart_man_man_medium_dark_skin_tone_dark_skin_tone\"] = \"👨🏾‍❤️‍👨🏿\",\n        [\"couple_with_heart_man_man_tone5_tone1\"] = \"👨🏿‍❤️‍👨🏻\",\n        [\"couple_with_heart_man_man_dark_skin_tone_light_skin_tone\"] = \"👨🏿‍❤️‍👨🏻\",\n        [\"couple_with_heart_man_man_tone5_tone2\"] = \"👨🏿‍❤️‍👨🏼\",\n        [\"couple_with_heart_man_man_dark_skin_tone_medium_light_skin_tone\"] = \"👨🏿‍❤️‍👨🏼\",\n        [\"couple_with_heart_man_man_tone5_tone3\"] = \"👨🏿‍❤️‍👨🏽\",\n        [\"couple_with_heart_man_man_dark_skin_tone_medium_skin_tone\"] = \"👨🏿‍❤️‍👨🏽\",\n        [\"couple_with_heart_man_man_tone5_tone4\"] = \"👨🏿‍❤️‍👨🏾\",\n        [\"couple_with_heart_man_man_dark_skin_tone_medium_dark_skin_tone\"] = \"👨🏿‍❤️‍👨🏾\",\n        [\"couple_with_heart_man_man_tone5\"] = \"👨🏿‍❤️‍👨🏿\",\n        [\"couple_with_heart_man_man_dark_skin_tone\"] = \"👨🏿‍❤️‍👨🏿\",\n        [\"couplekiss\"] = \"💏\",\n        [\"kiss_person_person_tone5_tone4\"] = \"🧑🏿‍❤️‍💋‍🧑🏾\",\n        [\"kiss_person_person_dark_skin_tone_medium_dark_skin_tone\"] = \"🧑🏿‍❤️‍💋‍🧑🏾\",\n        [\"kiss_tone1\"] = \"💏🏻\",\n        [\"kiss_light_skin_tone\"] = \"💏🏻\",\n        [\"kiss_person_person_tone1_tone2\"] = \"🧑🏻‍❤️‍💋‍🧑🏼\",\n        [\"kiss_person_person_light_skin_tone_medium_light_skin_tone\"] = \"🧑🏻‍❤️‍💋‍🧑🏼\",\n        [\"kiss_person_person_tone1_tone3\"] = \"🧑🏻‍❤️‍💋‍🧑🏽\",\n        [\"kiss_person_person_light_skin_tone_medium_skin_tone\"] = \"🧑🏻‍❤️‍💋‍🧑🏽\",\n        [\"kiss_person_person_tone1_tone4\"] = \"🧑🏻‍❤️‍💋‍🧑🏾\",\n        [\"kiss_person_person_light_skin_tone_medium_dark_skin_tone\"] = \"🧑🏻‍❤️‍💋‍🧑🏾\",\n        [\"kiss_person_person_tone1_tone5\"] = \"🧑🏻‍❤️‍💋‍🧑🏿\",\n        [\"kiss_person_person_light_skin_tone_dark_skin_tone\"] = \"🧑🏻‍❤️‍💋‍🧑🏿\",\n        [\"kiss_person_person_tone2_tone1\"] = \"🧑🏼‍❤️‍💋‍🧑🏻\",\n        [\"kiss_person_person_medium_light_skin_tone_light_skin_tone\"] = \"🧑🏼‍❤️‍💋‍🧑🏻\",\n        [\"kiss_tone2\"] = \"💏🏼\",\n        [\"kiss_medium_light_skin_tone\"] = \"💏🏼\",\n        [\"kiss_person_person_tone2_tone3\"] = \"🧑🏼‍❤️‍💋‍🧑🏽\",\n        [\"kiss_person_person_medium_light_skin_tone_medium_skin_tone\"] = \"🧑🏼‍❤️‍💋‍🧑🏽\",\n        [\"kiss_person_person_tone2_tone4\"] = \"🧑🏼‍❤️‍💋‍🧑🏾\",\n        [\"kiss_person_person_medium_light_skin_tone_medium_dark_skin_tone\"] = \"🧑🏼‍❤️‍💋‍🧑🏾\",\n        [\"kiss_person_person_tone2_tone5\"] = \"🧑🏼‍❤️‍💋‍🧑🏿\",\n        [\"kiss_person_person_medium_light_skin_tone_dark_skin_tone\"] = \"🧑🏼‍❤️‍💋‍🧑🏿\",\n        [\"kiss_person_person_tone3_tone1\"] = \"🧑🏽‍❤️‍💋‍🧑🏻\",\n        [\"kiss_person_person_medium_skin_tone_light_skin_tone\"] = \"🧑🏽‍❤️‍💋‍🧑🏻\",\n        [\"kiss_person_person_tone3_tone2\"] = \"🧑🏽‍❤️‍💋‍🧑🏼\",\n        [\"kiss_person_person_medium_skin_tone_medium_light_skin_tone\"] = \"🧑🏽‍❤️‍💋‍🧑🏼\",\n        [\"kiss_tone3\"] = \"💏🏽\",\n        [\"kiss_medium_skin_tone\"] = \"💏🏽\",\n        [\"kiss_person_person_tone3_tone4\"] = \"🧑🏽‍❤️‍💋‍🧑🏾\",\n        [\"kiss_person_person_medium_skin_tone_medium_dark_skin_tone\"] = \"🧑🏽‍❤️‍💋‍🧑🏾\",\n        [\"kiss_person_person_tone3_tone5\"] = \"🧑🏽‍❤️‍💋‍🧑🏿\",\n        [\"kiss_person_person_medium_skin_tone_dark_skin_tone\"] = \"🧑🏽‍❤️‍💋‍🧑🏿\",\n        [\"kiss_person_person_tone4_tone1\"] = \"🧑🏾‍❤️‍💋‍🧑🏻\",\n        [\"kiss_person_person_medium_dark_skin_tone_light_skin_tone\"] = \"🧑🏾‍❤️‍💋‍🧑🏻\",\n        [\"kiss_person_person_tone4_tone2\"] = \"🧑🏾‍❤️‍💋‍🧑🏼\",\n        [\"kiss_person_person_medium_dark_skin_tone_medium_light_skin_tone\"] = \"🧑🏾‍❤️‍💋‍🧑🏼\",\n        [\"kiss_person_person_tone4_tone3\"] = \"🧑🏾‍❤️‍💋‍🧑🏽\",\n        [\"kiss_person_person_medium_dark_skin_tone_medium_skin_tone\"] = \"🧑🏾‍❤️‍💋‍🧑🏽\",\n        [\"kiss_tone4\"] = \"💏🏾\",\n        [\"kiss_medium_dark_skin_tone\"] = \"💏🏾\",\n        [\"kiss_person_person_tone4_tone5\"] = \"🧑🏾‍❤️‍💋‍🧑🏿\",\n        [\"kiss_person_person_medium_dark_skin_tone_dark_skin_tone\"] = \"🧑🏾‍❤️‍💋‍🧑🏿\",\n        [\"kiss_person_person_tone5_tone1\"] = \"🧑🏿‍❤️‍💋‍🧑🏻\",\n        [\"kiss_person_person_dark_skin_tone_light_skin_tone\"] = \"🧑🏿‍❤️‍💋‍🧑🏻\",\n        [\"kiss_person_person_tone5_tone2\"] = \"🧑🏿‍❤️‍💋‍🧑🏼\",\n        [\"kiss_person_person_dark_skin_tone_medium_light_skin_tone\"] = \"🧑🏿‍❤️‍💋‍🧑🏼\",\n        [\"kiss_person_person_tone5_tone3\"] = \"🧑🏿‍❤️‍💋‍🧑🏽\",\n        [\"kiss_person_person_dark_skin_tone_medium_skin_tone\"] = \"🧑🏿‍❤️‍💋‍🧑🏽\",\n        [\"kiss_tone5\"] = \"💏🏿\",\n        [\"kiss_dark_skin_tone\"] = \"💏🏿\",\n        [\"kiss_woman_man\"] = \"👩‍❤️‍💋‍👨\",\n        [\"kiss_woman_man_tone1\"] = \"👩🏻‍❤️‍💋‍👨🏻\",\n        [\"kiss_woman_man_light_skin_tone\"] = \"👩🏻‍❤️‍💋‍👨🏻\",\n        [\"kiss_woman_man_tone1_tone2\"] = \"👩🏻‍❤️‍💋‍👨🏼\",\n        [\"kiss_woman_man_light_skin_tone_medium_light_skin_tone\"] = \"👩🏻‍❤️‍💋‍👨🏼\",\n        [\"kiss_woman_man_tone1_tone3\"] = \"👩🏻‍❤️‍💋‍👨🏽\",\n        [\"kiss_woman_man_light_skin_tone_medium_skin_tone\"] = \"👩🏻‍❤️‍💋‍👨🏽\",\n        [\"kiss_woman_man_tone1_tone4\"] = \"👩🏻‍❤️‍💋‍👨🏾\",\n        [\"kiss_woman_man_light_skin_tone_medium_dark_skin_tone\"] = \"👩🏻‍❤️‍💋‍👨🏾\",\n        [\"kiss_woman_man_tone1_tone5\"] = \"👩🏻‍❤️‍💋‍👨🏿\",\n        [\"kiss_woman_man_light_skin_tone_dark_skin_tone\"] = \"👩🏻‍❤️‍💋‍👨🏿\",\n        [\"kiss_woman_man_tone2_tone1\"] = \"👩🏼‍❤️‍💋‍👨🏻\",\n        [\"kiss_woman_man_medium_light_skin_tone_light_skin_tone\"] = \"👩🏼‍❤️‍💋‍👨🏻\",\n        [\"kiss_woman_man_tone2\"] = \"👩🏼‍❤️‍💋‍👨🏼\",\n        [\"kiss_woman_man_medium_light_skin_tone\"] = \"👩🏼‍❤️‍💋‍👨🏼\",\n        [\"kiss_woman_man_tone2_tone3\"] = \"👩🏼‍❤️‍💋‍👨🏽\",\n        [\"kiss_woman_man_medium_light_skin_tone_medium_skin_tone\"] = \"👩🏼‍❤️‍💋‍👨🏽\",\n        [\"kiss_woman_man_tone2_tone4\"] = \"👩🏼‍❤️‍💋‍👨🏾\",\n        [\"kiss_woman_man_medium_light_skin_tone_medium_dark_skin_tone\"] = \"👩🏼‍❤️‍💋‍👨🏾\",\n        [\"kiss_woman_man_tone2_tone5\"] = \"👩🏼‍❤️‍💋‍👨🏿\",\n        [\"kiss_woman_man_medium_light_skin_tone_dark_skin_tone\"] = \"👩🏼‍❤️‍💋‍👨🏿\",\n        [\"kiss_woman_man_tone3_tone1\"] = \"👩🏽‍❤️‍💋‍👨🏻\",\n        [\"kiss_woman_man_medium_skin_tone_light_skin_tone\"] = \"👩🏽‍❤️‍💋‍👨🏻\",\n        [\"kiss_woman_man_tone3_tone2\"] = \"👩🏽‍❤️‍💋‍👨🏼\",\n        [\"kiss_woman_man_medium_skin_tone_medium_light_skin_tone\"] = \"👩🏽‍❤️‍💋‍👨🏼\",\n        [\"kiss_woman_man_tone3\"] = \"👩🏽‍❤️‍💋‍👨🏽\",\n        [\"kiss_woman_man_medium_skin_tone\"] = \"👩🏽‍❤️‍💋‍👨🏽\",\n        [\"kiss_woman_man_tone3_tone4\"] = \"👩🏽‍❤️‍💋‍👨🏾\",\n        [\"kiss_woman_man_medium_skin_tone_medium_dark_skin_tone\"] = \"👩🏽‍❤️‍💋‍👨🏾\",\n        [\"kiss_woman_man_tone3_tone5\"] = \"👩🏽‍❤️‍💋‍👨🏿\",\n        [\"kiss_woman_man_medium_skin_tone_dark_skin_tone\"] = \"👩🏽‍❤️‍💋‍👨🏿\",\n        [\"kiss_woman_man_tone4_tone1\"] = \"👩🏾‍❤️‍💋‍👨🏻\",\n        [\"kiss_woman_man_medium_dark_skin_tone_light_skin_tone\"] = \"👩🏾‍❤️‍💋‍👨🏻\",\n        [\"kiss_woman_man_tone4_tone2\"] = \"👩🏾‍❤️‍💋‍👨🏼\",\n        [\"kiss_woman_man_medium_dark_skin_tone_medium_light_skin_tone\"] = \"👩🏾‍❤️‍💋‍👨🏼\",\n        [\"kiss_woman_man_tone4_tone3\"] = \"👩🏾‍❤️‍💋‍👨🏽\",\n        [\"kiss_woman_man_medium_dark_skin_tone_medium_skin_tone\"] = \"👩🏾‍❤️‍💋‍👨🏽\",\n        [\"kiss_woman_man_tone4\"] = \"👩🏾‍❤️‍💋‍👨🏾\",\n        [\"kiss_woman_man_medium_dark_skin_tone\"] = \"👩🏾‍❤️‍💋‍👨🏾\",\n        [\"kiss_woman_man_tone4_tone5\"] = \"👩🏾‍❤️‍💋‍👨🏿\",\n        [\"kiss_woman_man_medium_dark_skin_tone_dark_skin_tone\"] = \"👩🏾‍❤️‍💋‍👨🏿\",\n        [\"kiss_woman_man_tone5_tone1\"] = \"👩🏿‍❤️‍💋‍👨🏻\",\n        [\"kiss_woman_man_dark_skin_tone_light_skin_tone\"] = \"👩🏿‍❤️‍💋‍👨🏻\",\n        [\"kiss_woman_man_tone5_tone2\"] = \"👩🏿‍❤️‍💋‍👨🏼\",\n        [\"kiss_woman_man_dark_skin_tone_medium_light_skin_tone\"] = \"👩🏿‍❤️‍💋‍👨🏼\",\n        [\"kiss_woman_man_tone5_tone3\"] = \"👩🏿‍❤️‍💋‍👨🏽\",\n        [\"kiss_woman_man_dark_skin_tone_medium_skin_tone\"] = \"👩🏿‍❤️‍💋‍👨🏽\",\n        [\"kiss_woman_man_tone5_tone4\"] = \"👩🏿‍❤️‍💋‍👨🏾\",\n        [\"kiss_woman_man_dark_skin_tone_medium_dark_skin_tone\"] = \"👩🏿‍❤️‍💋‍👨🏾\",\n        [\"kiss_woman_man_tone5\"] = \"👩🏿‍❤️‍💋‍👨🏿\",\n        [\"kiss_woman_man_dark_skin_tone\"] = \"👩🏿‍❤️‍💋‍👨🏿\",\n        [\"kiss_ww\"] = \"👩‍❤️‍💋‍👩\",\n        [\"couplekiss_ww\"] = \"👩‍❤️‍💋‍👩\",\n        [\"kiss_woman_woman_tone1\"] = \"👩🏻‍❤️‍💋‍👩🏻\",\n        [\"kiss_woman_woman_light_skin_tone\"] = \"👩🏻‍❤️‍💋‍👩🏻\",\n        [\"kiss_woman_woman_tone1_tone2\"] = \"👩🏻‍❤️‍💋‍👩🏼\",\n        [\"kiss_woman_woman_light_skin_tone_medium_light_skin_tone\"] = \"👩🏻‍❤️‍💋‍👩🏼\",\n        [\"kiss_woman_woman_tone1_tone3\"] = \"👩🏻‍❤️‍💋‍👩🏽\",\n        [\"kiss_woman_woman_light_skin_tone_medium_skin_tone\"] = \"👩🏻‍❤️‍💋‍👩🏽\",\n        [\"kiss_woman_woman_tone1_tone4\"] = \"👩🏻‍❤️‍💋‍👩🏾\",\n        [\"kiss_woman_woman_light_skin_tone_medium_dark_skin_tone\"] = \"👩🏻‍❤️‍💋‍👩🏾\",\n        [\"kiss_woman_woman_tone1_tone5\"] = \"👩🏻‍❤️‍💋‍👩🏿\",\n        [\"kiss_woman_woman_light_skin_tone_dark_skin_tone\"] = \"👩🏻‍❤️‍💋‍👩🏿\",\n        [\"kiss_woman_woman_tone2_tone1\"] = \"👩🏼‍❤️‍💋‍👩🏻\",\n        [\"kiss_woman_woman_medium_light_skin_tone_light_skin_tone\"] = \"👩🏼‍❤️‍💋‍👩🏻\",\n        [\"kiss_woman_woman_tone2\"] = \"👩🏼‍❤️‍💋‍👩🏼\",\n        [\"kiss_woman_woman_medium_light_skin_tone\"] = \"👩🏼‍❤️‍💋‍👩🏼\",\n        [\"kiss_woman_woman_tone2_tone3\"] = \"👩🏼‍❤️‍💋‍👩🏽\",\n        [\"kiss_woman_woman_medium_light_skin_tone_medium_skin_tone\"] = \"👩🏼‍❤️‍💋‍👩🏽\",\n        [\"kiss_woman_woman_tone2_tone4\"] = \"👩🏼‍❤️‍💋‍👩🏾\",\n        [\"kiss_woman_woman_medium_light_skin_tone_medium_dark_skin_tone\"] = \"👩🏼‍❤️‍💋‍👩🏾\",\n        [\"kiss_woman_woman_tone2_tone5\"] = \"👩🏼‍❤️‍💋‍👩🏿\",\n        [\"kiss_woman_woman_medium_light_skin_tone_dark_skin_tone\"] = \"👩🏼‍❤️‍💋‍👩🏿\",\n        [\"kiss_woman_woman_tone3_tone1\"] = \"👩🏽‍❤️‍💋‍👩🏻\",\n        [\"kiss_woman_woman_medium_skin_tone_light_skin_tone\"] = \"👩🏽‍❤️‍💋‍👩🏻\",\n        [\"kiss_woman_woman_tone3_tone2\"] = \"👩🏽‍❤️‍💋‍👩🏼\",\n        [\"kiss_woman_woman_medium_skin_tone_medium_light_skin_tone\"] = \"👩🏽‍❤️‍💋‍👩🏼\",\n        [\"kiss_woman_woman_tone3\"] = \"👩🏽‍❤️‍💋‍👩🏽\",\n        [\"kiss_woman_woman_medium_skin_tone\"] = \"👩🏽‍❤️‍💋‍👩🏽\",\n        [\"kiss_woman_woman_tone3_tone4\"] = \"👩🏽‍❤️‍💋‍👩🏾\",\n        [\"kiss_woman_woman_medium_skin_tone_medium_dark_skin_tone\"] = \"👩🏽‍❤️‍💋‍👩🏾\",\n        [\"kiss_woman_woman_tone3_tone5\"] = \"👩🏽‍❤️‍💋‍👩🏿\",\n        [\"kiss_woman_woman_medium_skin_tone_dark_skin_tone\"] = \"👩🏽‍❤️‍💋‍👩🏿\",\n        [\"kiss_woman_woman_tone4_tone1\"] = \"👩🏾‍❤️‍💋‍👩🏻\",\n        [\"kiss_woman_woman_medium_dark_skin_tone_light_skin_tone\"] = \"👩🏾‍❤️‍💋‍👩🏻\",\n        [\"kiss_woman_woman_tone4_tone2\"] = \"👩🏾‍❤️‍💋‍👩🏼\",\n        [\"kiss_woman_woman_medium_dark_skin_tone_medium_light_skin_tone\"] = \"👩🏾‍❤️‍💋‍👩🏼\",\n        [\"kiss_woman_woman_tone4_tone3\"] = \"👩🏾‍❤️‍💋‍👩🏽\",\n        [\"kiss_woman_woman_medium_dark_skin_tone_medium_skin_tone\"] = \"👩🏾‍❤️‍💋‍👩🏽\",\n        [\"kiss_woman_woman_tone4\"] = \"👩🏾‍❤️‍💋‍👩🏾\",\n        [\"kiss_woman_woman_medium_dark_skin_tone\"] = \"👩🏾‍❤️‍💋‍👩🏾\",\n        [\"kiss_woman_woman_tone4_tone5\"] = \"👩🏾‍❤️‍💋‍👩🏿\",\n        [\"kiss_woman_woman_medium_dark_skin_tone_dark_skin_tone\"] = \"👩🏾‍❤️‍💋‍👩🏿\",\n        [\"kiss_woman_woman_tone5_tone1\"] = \"👩🏿‍❤️‍💋‍👩🏻\",\n        [\"kiss_woman_woman_dark_skin_tone_light_skin_tone\"] = \"👩🏿‍❤️‍💋‍👩🏻\",\n        [\"kiss_woman_woman_tone5_tone2\"] = \"👩🏿‍❤️‍💋‍👩🏼\",\n        [\"kiss_woman_woman_dark_skin_tone_medium_light_skin_tone\"] = \"👩🏿‍❤️‍💋‍👩🏼\",\n        [\"kiss_woman_woman_tone5_tone3\"] = \"👩🏿‍❤️‍💋‍👩🏽\",\n        [\"kiss_woman_woman_dark_skin_tone_medium_skin_tone\"] = \"👩🏿‍❤️‍💋‍👩🏽\",\n        [\"kiss_woman_woman_tone5_tone4\"] = \"👩🏿‍❤️‍💋‍👩🏾\",\n        [\"kiss_woman_woman_dark_skin_tone_medium_dark_skin_tone\"] = \"👩🏿‍❤️‍💋‍👩🏾\",\n        [\"kiss_woman_woman_tone5\"] = \"👩🏿‍❤️‍💋‍👩🏿\",\n        [\"kiss_woman_woman_dark_skin_tone\"] = \"👩🏿‍❤️‍💋‍👩🏿\",\n        [\"kiss_mm\"] = \"👨‍❤️‍💋‍👨\",\n        [\"couplekiss_mm\"] = \"👨‍❤️‍💋‍👨\",\n        [\"kiss_man_man_tone1\"] = \"👨🏻‍❤️‍💋‍👨🏻\",\n        [\"kiss_man_man_light_skin_tone\"] = \"👨🏻‍❤️‍💋‍👨🏻\",\n        [\"kiss_man_man_tone1_tone2\"] = \"👨🏻‍❤️‍💋‍👨🏼\",\n        [\"kiss_man_man_light_skin_tone_medium_light_skin_tone\"] = \"👨🏻‍❤️‍💋‍👨🏼\",\n        [\"kiss_man_man_tone1_tone3\"] = \"👨🏻‍❤️‍💋‍👨🏽\",\n        [\"kiss_man_man_light_skin_tone_medium_skin_tone\"] = \"👨🏻‍❤️‍💋‍👨🏽\",\n        [\"kiss_man_man_tone1_tone4\"] = \"👨🏻‍❤️‍💋‍👨🏾\",\n        [\"kiss_man_man_light_skin_tone_medium_dark_skin_tone\"] = \"👨🏻‍❤️‍💋‍👨🏾\",\n        [\"kiss_man_man_tone1_tone5\"] = \"👨🏻‍❤️‍💋‍👨🏿\",\n        [\"kiss_man_man_light_skin_tone_dark_skin_tone\"] = \"👨🏻‍❤️‍💋‍👨🏿\",\n        [\"kiss_man_man_tone2_tone1\"] = \"👨🏼‍❤️‍💋‍👨🏻\",\n        [\"kiss_man_man_medium_light_skin_tone_light_skin_tone\"] = \"👨🏼‍❤️‍💋‍👨🏻\",\n        [\"kiss_man_man_tone2\"] = \"👨🏼‍❤️‍💋‍👨🏼\",\n        [\"kiss_man_man_medium_light_skin_tone\"] = \"👨🏼‍❤️‍💋‍👨🏼\",\n        [\"kiss_man_man_tone2_tone3\"] = \"👨🏼‍❤️‍💋‍👨🏽\",\n        [\"kiss_man_man_medium_light_skin_tone_medium_skin_tone\"] = \"👨🏼‍❤️‍💋‍👨🏽\",\n        [\"kiss_man_man_tone2_tone4\"] = \"👨🏼‍❤️‍💋‍👨🏾\",\n        [\"kiss_man_man_medium_light_skin_tone_medium_dark_skin_tone\"] = \"👨🏼‍❤️‍💋‍👨🏾\",\n        [\"kiss_man_man_tone2_tone5\"] = \"👨🏼‍❤️‍💋‍👨🏿\",\n        [\"kiss_man_man_medium_light_skin_tone_dark_skin_tone\"] = \"👨🏼‍❤️‍💋‍👨🏿\",\n        [\"kiss_man_man_tone3_tone1\"] = \"👨🏽‍❤️‍💋‍👨🏻\",\n        [\"kiss_man_man_medium_skin_tone_light_skin_tone\"] = \"👨🏽‍❤️‍💋‍👨🏻\",\n        [\"kiss_man_man_tone3_tone2\"] = \"👨🏽‍❤️‍💋‍👨🏼\",\n        [\"kiss_man_man_medium_skin_tone_medium_light_skin_tone\"] = \"👨🏽‍❤️‍💋‍👨🏼\",\n        [\"kiss_man_man_tone3\"] = \"👨🏽‍❤️‍💋‍👨🏽\",\n        [\"kiss_man_man_medium_skin_tone\"] = \"👨🏽‍❤️‍💋‍👨🏽\",\n        [\"kiss_man_man_tone3_tone4\"] = \"👨🏽‍❤️‍💋‍👨🏾\",\n        [\"kiss_man_man_medium_skin_tone_medium_dark_skin_tone\"] = \"👨🏽‍❤️‍💋‍👨🏾\",\n        [\"kiss_man_man_tone3_tone5\"] = \"👨🏽‍❤️‍💋‍👨🏿\",\n        [\"kiss_man_man_medium_skin_tone_dark_skin_tone\"] = \"👨🏽‍❤️‍💋‍👨🏿\",\n        [\"kiss_man_man_tone4_tone1\"] = \"👨🏾‍❤️‍💋‍👨🏻\",\n        [\"kiss_man_man_medium_dark_skin_tone_light_skin_tone\"] = \"👨🏾‍❤️‍💋‍👨🏻\",\n        [\"kiss_man_man_tone4_tone2\"] = \"👨🏾‍❤️‍💋‍👨🏼\",\n        [\"kiss_man_man_medium_dark_skin_tone_medium_light_skin_tone\"] = \"👨🏾‍❤️‍💋‍👨🏼\",\n        [\"kiss_man_man_tone4_tone3\"] = \"👨🏾‍❤️‍💋‍👨🏽\",\n        [\"kiss_man_man_medium_dark_skin_tone_medium_skin_tone\"] = \"👨🏾‍❤️‍💋‍👨🏽\",\n        [\"kiss_man_man_tone4\"] = \"👨🏾‍❤️‍💋‍👨🏾\",\n        [\"kiss_man_man_medium_dark_skin_tone\"] = \"👨🏾‍❤️‍💋‍👨🏾\",\n        [\"kiss_man_man_tone4_tone5\"] = \"👨🏾‍❤️‍💋‍👨🏿\",\n        [\"kiss_man_man_medium_dark_skin_tone_dark_skin_tone\"] = \"👨🏾‍❤️‍💋‍👨🏿\",\n        [\"kiss_man_man_tone5_tone1\"] = \"👨🏿‍❤️‍💋‍👨🏻\",\n        [\"kiss_man_man_dark_skin_tone_light_skin_tone\"] = \"👨🏿‍❤️‍💋‍👨🏻\",\n        [\"kiss_man_man_tone5_tone2\"] = \"👨🏿‍❤️‍💋‍👨🏼\",\n        [\"kiss_man_man_dark_skin_tone_medium_light_skin_tone\"] = \"👨🏿‍❤️‍💋‍👨🏼\",\n        [\"kiss_man_man_tone5_tone3\"] = \"👨🏿‍❤️‍💋‍👨🏽\",\n        [\"kiss_man_man_dark_skin_tone_medium_skin_tone\"] = \"👨🏿‍❤️‍💋‍👨🏽\",\n        [\"kiss_man_man_tone5_tone4\"] = \"👨🏿‍❤️‍💋‍👨🏾\",\n        [\"kiss_man_man_dark_skin_tone_medium_dark_skin_tone\"] = \"👨🏿‍❤️‍💋‍👨🏾\",\n        [\"kiss_man_man_tone5\"] = \"👨🏿‍❤️‍💋‍👨🏿\",\n        [\"kiss_man_man_dark_skin_tone\"] = \"👨🏿‍❤️‍💋‍👨🏿\",\n        [\"family\"] = \"👪\",\n        [\"family_man_woman_boy\"] = \"👨‍👩‍👦\",\n        [\"family_mwg\"] = \"👨‍👩‍👧\",\n        [\"family_mwgb\"] = \"👨‍👩‍👧‍👦\",\n        [\"family_mwbb\"] = \"👨‍👩‍👦‍👦\",\n        [\"family_mwgg\"] = \"👨‍👩‍👧‍👧\",\n        [\"family_wwb\"] = \"👩‍👩‍👦\",\n        [\"family_wwg\"] = \"👩‍👩‍👧\",\n        [\"family_wwgb\"] = \"👩‍👩‍👧‍👦\",\n        [\"family_wwbb\"] = \"👩‍👩‍👦‍👦\",\n        [\"family_wwgg\"] = \"👩‍👩‍👧‍👧\",\n        [\"family_mmb\"] = \"👨‍👨‍👦\",\n        [\"family_mmg\"] = \"👨‍👨‍👧\",\n        [\"family_mmgb\"] = \"👨‍👨‍👧‍👦\",\n        [\"family_mmbb\"] = \"👨‍👨‍👦‍👦\",\n        [\"family_mmgg\"] = \"👨‍👨‍👧‍👧\",\n        [\"family_woman_boy\"] = \"👩‍👦\",\n        [\"family_woman_girl\"] = \"👩‍👧\",\n        [\"family_woman_girl_boy\"] = \"👩‍👧‍👦\",\n        [\"family_woman_boy_boy\"] = \"👩‍👦‍👦\",\n        [\"family_woman_girl_girl\"] = \"👩‍👧‍👧\",\n        [\"family_man_boy\"] = \"👨‍👦\",\n        [\"family_man_girl\"] = \"👨‍👧\",\n        [\"family_man_girl_boy\"] = \"👨‍👧‍👦\",\n        [\"family_man_boy_boy\"] = \"👨‍👦‍👦\",\n        [\"family_man_girl_girl\"] = \"👨‍👧‍👧\",\n        [\"yarn\"] = \"🧶\",\n        [\"thread\"] = \"🧵\",\n        [\"coat\"] = \"🧥\",\n        [\"lab_coat\"] = \"🥼\",\n        [\"safety_vest\"] = \"🦺\",\n        [\"womans_clothes\"] = \"👚\",\n        [\"shirt\"] = \"👕\",\n        [\"jeans\"] = \"👖\",\n        [\"briefs\"] = \"🩲\",\n        [\"shorts\"] = \"🩳\",\n        [\"necktie\"] = \"👔\",\n        [\"dress\"] = \"👗\",\n        [\"bikini\"] = \"👙\",\n        [\"one_piece_swimsuit\"] = \"🩱\",\n        [\"kimono\"] = \"👘\",\n        [\"sari\"] = \"🥻\",\n        [\"womans_flat_shoe\"] = \"🥿\",\n        [\"high_heel\"] = \"👠\",\n        [\"sandal\"] = \"👡\",\n        [\"boot\"] = \"👢\",\n        [\"mans_shoe\"] = \"👞\",\n        [\"athletic_shoe\"] = \"👟\",\n        [\"hiking_boot\"] = \"🥾\",\n        [\"thong_sandal\"] = \"🩴\",\n        [\"socks\"] = \"🧦\",\n        [\"gloves\"] = \"🧤\",\n        [\"scarf\"] = \"🧣\",\n        [\"tophat\"] = \"🎩\",\n        [\"billed_cap\"] = \"🧢\",\n        [\"womans_hat\"] = \"👒\",\n        [\"mortar_board\"] = \"🎓\",\n        [\"helmet_with_cross\"] = \"⛑️\",\n        [\"helmet_with_white_cross\"] = \"⛑️\",\n        [\"military_helmet\"] = \"🪖\",\n        [\"crown\"] = \"👑\",\n        [\"ring\"] = \"💍\",\n        [\"pouch\"] = \"👝\",\n        [\"purse\"] = \"👛\",\n        [\"handbag\"] = \"👜\",\n        [\"briefcase\"] = \"💼\",\n        [\"school_satchel\"] = \"🎒\",\n        [\"luggage\"] = \"🧳\",\n        [\"eyeglasses\"] = \"👓\",\n        [\"dark_sunglasses\"] = \"🕶️\",\n        [\"goggles\"] = \"🥽\",\n        [\"closed_umbrella\"] = \"🌂\",\n        [\"dog\"] = \"🐶\",\n        [\"cat\"] = \"🐱\",\n        [\"mouse\"] = \"🐭\",\n        [\"hamster\"] = \"🐹\",\n        [\"rabbit\"] = \"🐰\",\n        [\"fox\"] = \"🦊\",\n        [\"fox_face\"] = \"🦊\",\n        [\"bear\"] = \"🐻\",\n        [\"panda_face\"] = \"🐼\",\n        [\"polar_bear\"] = \"🐻‍❄️\",\n        [\"koala\"] = \"🐨\",\n        [\"tiger\"] = \"🐯\",\n        [\"lion_face\"] = \"🦁\",\n        [\"lion\"] = \"🦁\",\n        [\"cow\"] = \"🐮\",\n        [\"pig\"] = \"🐷\",\n        [\"pig_nose\"] = \"🐽\",\n        [\"frog\"] = \"🐸\",\n        [\"monkey_face\"] = \"🐵\",\n        [\"see_no_evil\"] = \"🙈\",\n        [\"hear_no_evil\"] = \"🙉\",\n        [\"speak_no_evil\"] = \"🙊\",\n        [\"monkey\"] = \"🐒\",\n        [\"chicken\"] = \"🐔\",\n        [\"penguin\"] = \"🐧\",\n        [\"bird\"] = \"🐦\",\n        [\"baby_chick\"] = \"🐤\",\n        [\"hatching_chick\"] = \"🐣\",\n        [\"hatched_chick\"] = \"🐥\",\n        [\"duck\"] = \"🦆\",\n        [\"dodo\"] = \"🦤\",\n        [\"eagle\"] = \"🦅\",\n        [\"owl\"] = \"🦉\",\n        [\"bat\"] = \"🦇\",\n        [\"wolf\"] = \"🐺\",\n        [\"boar\"] = \"🐗\",\n        [\"horse\"] = \"🐴\",\n        [\"unicorn\"] = \"🦄\",\n        [\"unicorn_face\"] = \"🦄\",\n        [\"bee\"] = \"🐝\",\n        [\"bug\"] = \"🐛\",\n        [\"butterfly\"] = \"🦋\",\n        [\"snail\"] = \"🐌\",\n        [\"worm\"] = \"🪱\",\n        [\"lady_beetle\"] = \"🐞\",\n        [\"ant\"] = \"🐜\",\n        [\"fly\"] = \"🪰\",\n        [\"mosquito\"] = \"🦟\",\n        [\"cockroach\"] = \"🪳\",\n        [\"beetle\"] = \"🪲\",\n        [\"cricket\"] = \"🦗\",\n        [\"spider\"] = \"🕷️\",\n        [\"spider_web\"] = \"🕸️\",\n        [\"scorpion\"] = \"🦂\",\n        [\"turtle\"] = \"🐢\",\n        [\"snake\"] = \"🐍\",\n        [\"lizard\"] = \"🦎\",\n        [\"t_rex\"] = \"🦖\",\n        [\"sauropod\"] = \"🦕\",\n        [\"octopus\"] = \"🐙\",\n        [\"squid\"] = \"🦑\",\n        [\"shrimp\"] = \"🦐\",\n        [\"lobster\"] = \"🦞\",\n        [\"crab\"] = \"🦀\",\n        [\"blowfish\"] = \"🐡\",\n        [\"tropical_fish\"] = \"🐠\",\n        [\"fish\"] = \"🐟\",\n        [\"seal\"] = \"🦭\",\n        [\"dolphin\"] = \"🐬\",\n        [\"whale\"] = \"🐳\",\n        [\"whale2\"] = \"🐋\",\n        [\"shark\"] = \"🦈\",\n        [\"crocodile\"] = \"🐊\",\n        [\"tiger2\"] = \"🐅\",\n        [\"leopard\"] = \"🐆\",\n        [\"zebra\"] = \"🦓\",\n        [\"gorilla\"] = \"🦍\",\n        [\"orangutan\"] = \"🦧\",\n        [\"elephant\"] = \"🐘\",\n        [\"mammoth\"] = \"🦣\",\n        [\"bison\"] = \"🦬\",\n        [\"hippopotamus\"] = \"🦛\",\n        [\"rhino\"] = \"🦏\",\n        [\"rhinoceros\"] = \"🦏\",\n        [\"dromedary_camel\"] = \"🐪\",\n        [\"camel\"] = \"🐫\",\n        [\"giraffe\"] = \"🦒\",\n        [\"kangaroo\"] = \"🦘\",\n        [\"water_buffalo\"] = \"🐃\",\n        [\"ox\"] = \"🐂\",\n        [\"cow2\"] = \"🐄\",\n        [\"racehorse\"] = \"🐎\",\n        [\"pig2\"] = \"🐖\",\n        [\"ram\"] = \"🐏\",\n        [\"sheep\"] = \"🐑\",\n        [\"llama\"] = \"🦙\",\n        [\"goat\"] = \"🐐\",\n        [\"deer\"] = \"🦌\",\n        [\"dog2\"] = \"🐕\",\n        [\"poodle\"] = \"🐩\",\n        [\"guide_dog\"] = \"🦮\",\n        [\"service_dog\"] = \"🐕‍🦺\",\n        [\"cat2\"] = \"🐈\",\n        [\"black_cat\"] = \"🐈‍⬛\",\n        [\"rooster\"] = \"🐓\",\n        [\"turkey\"] = \"🦃\",\n        [\"peacock\"] = \"🦚\",\n        [\"parrot\"] = \"🦜\",\n        [\"swan\"] = \"🦢\",\n        [\"flamingo\"] = \"🦩\",\n        [\"dove\"] = \"🕊️\",\n        [\"dove_of_peace\"] = \"🕊️\",\n        [\"rabbit2\"] = \"🐇\",\n        [\"raccoon\"] = \"🦝\",\n        [\"skunk\"] = \"🦨\",\n        [\"badger\"] = \"🦡\",\n        [\"beaver\"] = \"🦫\",\n        [\"otter\"] = \"🦦\",\n        [\"sloth\"] = \"🦥\",\n        [\"mouse2\"] = \"🐁\",\n        [\"rat\"] = \"🐀\",\n        [\"chipmunk\"] = \"🐿️\",\n        [\"hedgehog\"] = \"🦔\",\n        [\"feet\"] = \"🐾\",\n        [\"paw_prints\"] = \"🐾\",\n        [\"dragon\"] = \"🐉\",\n        [\"dragon_face\"] = \"🐲\",\n        [\"cactus\"] = \"🌵\",\n        [\"christmas_tree\"] = \"🎄\",\n        [\"evergreen_tree\"] = \"🌲\",\n        [\"deciduous_tree\"] = \"🌳\",\n        [\"palm_tree\"] = \"🌴\",\n        [\"seedling\"] = \"🌱\",\n        [\"herb\"] = \"🌿\",\n        [\"shamrock\"] = \"☘️\",\n        [\"four_leaf_clover\"] = \"🍀\",\n        [\"bamboo\"] = \"🎍\",\n        [\"tanabata_tree\"] = \"🎋\",\n        [\"leaves\"] = \"🍃\",\n        [\"fallen_leaf\"] = \"🍂\",\n        [\"maple_leaf\"] = \"🍁\",\n        [\"feather\"] = \"🪶\",\n        [\"mushroom\"] = \"🍄\",\n        [\"shell\"] = \"🐚\",\n        [\"rock\"] = \"🪨\",\n        [\"wood\"] = \"🪵\",\n        [\"ear_of_rice\"] = \"🌾\",\n        [\"potted_plant\"] = \"🪴\",\n        [\"bouquet\"] = \"💐\",\n        [\"tulip\"] = \"🌷\",\n        [\"rose\"] = \"🌹\",\n        [\"wilted_rose\"] = \"🥀\",\n        [\"wilted_flower\"] = \"🥀\",\n        [\"hibiscus\"] = \"🌺\",\n        [\"cherry_blossom\"] = \"🌸\",\n        [\"blossom\"] = \"🌼\",\n        [\"sunflower\"] = \"🌻\",\n        [\"sun_with_face\"] = \"🌞\",\n        [\"full_moon_with_face\"] = \"🌝\",\n        [\"first_quarter_moon_with_face\"] = \"🌛\",\n        [\"last_quarter_moon_with_face\"] = \"🌜\",\n        [\"new_moon_with_face\"] = \"🌚\",\n        [\"full_moon\"] = \"🌕\",\n        [\"waning_gibbous_moon\"] = \"🌖\",\n        [\"last_quarter_moon\"] = \"🌗\",\n        [\"waning_crescent_moon\"] = \"🌘\",\n        [\"new_moon\"] = \"🌑\",\n        [\"waxing_crescent_moon\"] = \"🌒\",\n        [\"first_quarter_moon\"] = \"🌓\",\n        [\"waxing_gibbous_moon\"] = \"🌔\",\n        [\"crescent_moon\"] = \"🌙\",\n        [\"earth_americas\"] = \"🌎\",\n        [\"earth_africa\"] = \"🌍\",\n        [\"earth_asia\"] = \"🌏\",\n        [\"ringed_planet\"] = \"🪐\",\n        [\"dizzy\"] = \"💫\",\n        [\"star\"] = \"⭐\",\n        [\"star2\"] = \"🌟\",\n        [\"sparkles\"] = \"✨\",\n        [\"zap\"] = \"⚡\",\n        [\"comet\"] = \"☄️\",\n        [\"boom\"] = \"💥\",\n        [\"fire\"] = \"🔥\",\n        [\"flame\"] = \"🔥\",\n        [\"cloud_tornado\"] = \"🌪️\",\n        [\"cloud_with_tornado\"] = \"🌪️\",\n        [\"rainbow\"] = \"🌈\",\n        [\"sunny\"] = \"☀️\",\n        [\"white_sun_small_cloud\"] = \"🌤️\",\n        [\"white_sun_with_small_cloud\"] = \"🌤️\",\n        [\"partly_sunny\"] = \"⛅\",\n        [\"white_sun_cloud\"] = \"🌥️\",\n        [\"white_sun_behind_cloud\"] = \"🌥️\",\n        [\"cloud\"] = \"☁️\",\n        [\"white_sun_rain_cloud\"] = \"🌦️\",\n        [\"white_sun_behind_cloud_with_rain\"] = \"🌦️\",\n        [\"cloud_rain\"] = \"🌧️\",\n        [\"cloud_with_rain\"] = \"🌧️\",\n        [\"thunder_cloud_rain\"] = \"⛈️\",\n        [\"thunder_cloud_and_rain\"] = \"⛈️\",\n        [\"cloud_lightning\"] = \"🌩️\",\n        [\"cloud_with_lightning\"] = \"🌩️\",\n        [\"cloud_snow\"] = \"🌨️\",\n        [\"cloud_with_snow\"] = \"🌨️\",\n        [\"snowflake\"] = \"❄️\",\n        [\"snowman2\"] = \"☃️\",\n        [\"snowman\"] = \"⛄\",\n        [\"wind_blowing_face\"] = \"🌬️\",\n        [\"dash\"] = \"💨\",\n        [\"droplet\"] = \"💧\",\n        [\"sweat_drops\"] = \"💦\",\n        [\"umbrella\"] = \"☔\",\n        [\"umbrella2\"] = \"☂️\",\n        [\"ocean\"] = \"🌊\",\n        [\"fog\"] = \"🌫️\",\n        [\"green_apple\"] = \"🍏\",\n        [\"apple\"] = \"🍎\",\n        [\"pear\"] = \"🍐\",\n        [\"tangerine\"] = \"🍊\",\n        [\"lemon\"] = \"🍋\",\n        [\"banana\"] = \"🍌\",\n        [\"watermelon\"] = \"🍉\",\n        [\"grapes\"] = \"🍇\",\n        [\"blueberries\"] = \"🫐\",\n        [\"strawberry\"] = \"🍓\",\n        [\"melon\"] = \"🍈\",\n        [\"cherries\"] = \"🍒\",\n        [\"peach\"] = \"🍑\",\n        [\"mango\"] = \"🥭\",\n        [\"pineapple\"] = \"🍍\",\n        [\"coconut\"] = \"🥥\",\n        [\"kiwi\"] = \"🥝\",\n        [\"kiwifruit\"] = \"🥝\",\n        [\"tomato\"] = \"🍅\",\n        [\"eggplant\"] = \"🍆\",\n        [\"avocado\"] = \"🥑\",\n        [\"olive\"] = \"🫒\",\n        [\"broccoli\"] = \"🥦\",\n        [\"leafy_green\"] = \"🥬\",\n        [\"bell_pepper\"] = \"🫑\",\n        [\"cucumber\"] = \"🥒\",\n        [\"hot_pepper\"] = \"🌶️\",\n        [\"corn\"] = \"🌽\",\n        [\"carrot\"] = \"🥕\",\n        [\"garlic\"] = \"🧄\",\n        [\"onion\"] = \"🧅\",\n        [\"potato\"] = \"🥔\",\n        [\"sweet_potato\"] = \"🍠\",\n        [\"croissant\"] = \"🥐\",\n        [\"bagel\"] = \"🥯\",\n        [\"bread\"] = \"🍞\",\n        [\"french_bread\"] = \"🥖\",\n        [\"baguette_bread\"] = \"🥖\",\n        [\"flatbread\"] = \"🫓\",\n        [\"pretzel\"] = \"🥨\",\n        [\"cheese\"] = \"🧀\",\n        [\"cheese_wedge\"] = \"🧀\",\n        [\"egg\"] = \"🥚\",\n        [\"cooking\"] = \"🍳\",\n        [\"butter\"] = \"🧈\",\n        [\"pancakes\"] = \"🥞\",\n        [\"waffle\"] = \"🧇\",\n        [\"bacon\"] = \"🥓\",\n        [\"cut_of_meat\"] = \"🥩\",\n        [\"poultry_leg\"] = \"🍗\",\n        [\"meat_on_bone\"] = \"🍖\",\n        [\"hotdog\"] = \"🌭\",\n        [\"hot_dog\"] = \"🌭\",\n        [\"hamburger\"] = \"🍔\",\n        [\"fries\"] = \"🍟\",\n        [\"pizza\"] = \"🍕\",\n        [\"sandwich\"] = \"🥪\",\n        [\"stuffed_flatbread\"] = \"🥙\",\n        [\"stuffed_pita\"] = \"🥙\",\n        [\"falafel\"] = \"🧆\",\n        [\"taco\"] = \"🌮\",\n        [\"burrito\"] = \"🌯\",\n        [\"tamale\"] = \"🫔\",\n        [\"salad\"] = \"🥗\",\n        [\"green_salad\"] = \"🥗\",\n        [\"shallow_pan_of_food\"] = \"🥘\",\n        [\"paella\"] = \"🥘\",\n        [\"fondue\"] = \"🫕\",\n        [\"canned_food\"] = \"🥫\",\n        [\"spaghetti\"] = \"🍝\",\n        [\"ramen\"] = \"🍜\",\n        [\"stew\"] = \"🍲\",\n        [\"curry\"] = \"🍛\",\n        [\"sushi\"] = \"🍣\",\n        [\"bento\"] = \"🍱\",\n        [\"dumpling\"] = \"🥟\",\n        [\"oyster\"] = \"🦪\",\n        [\"fried_shrimp\"] = \"🍤\",\n        [\"rice_ball\"] = \"🍙\",\n        [\"rice\"] = \"🍚\",\n        [\"rice_cracker\"] = \"🍘\",\n        [\"fish_cake\"] = \"🍥\",\n        [\"fortune_cookie\"] = \"🥠\",\n        [\"moon_cake\"] = \"🥮\",\n        [\"oden\"] = \"🍢\",\n        [\"dango\"] = \"🍡\",\n        [\"shaved_ice\"] = \"🍧\",\n        [\"ice_cream\"] = \"🍨\",\n        [\"icecream\"] = \"🍦\",\n        [\"pie\"] = \"🥧\",\n        [\"cupcake\"] = \"🧁\",\n        [\"cake\"] = \"🍰\",\n        [\"birthday\"] = \"🎂\",\n        [\"custard\"] = \"🍮\",\n        [\"pudding\"] = \"🍮\",\n        [\"flan\"] = \"🍮\",\n        [\"lollipop\"] = \"🍭\",\n        [\"candy\"] = \"🍬\",\n        [\"chocolate_bar\"] = \"🍫\",\n        [\"popcorn\"] = \"🍿\",\n        [\"doughnut\"] = \"🍩\",\n        [\"cookie\"] = \"🍪\",\n        [\"chestnut\"] = \"🌰\",\n        [\"peanuts\"] = \"🥜\",\n        [\"shelled_peanut\"] = \"🥜\",\n        [\"honey_pot\"] = \"🍯\",\n        [\"milk\"] = \"🥛\",\n        [\"glass_of_milk\"] = \"🥛\",\n        [\"baby_bottle\"] = \"🍼\",\n        [\"coffee\"] = \"☕\",\n        [\"tea\"] = \"🍵\",\n        [\"teapot\"] = \"🫖\",\n        [\"mate\"] = \"🧉\",\n        [\"bubble_tea\"] = \"🧋\",\n        [\"beverage_box\"] = \"🧃\",\n        [\"cup_with_straw\"] = \"🥤\",\n        [\"sake\"] = \"🍶\",\n        [\"beer\"] = \"🍺\",\n        [\"beers\"] = \"🍻\",\n        [\"champagne_glass\"] = \"🥂\",\n        [\"clinking_glass\"] = \"🥂\",\n        [\"wine_glass\"] = \"🍷\",\n        [\"tumbler_glass\"] = \"🥃\",\n        [\"whisky\"] = \"🥃\",\n        [\"cocktail\"] = \"🍸\",\n        [\"tropical_drink\"] = \"🍹\",\n        [\"champagne\"] = \"🍾\",\n        [\"bottle_with_popping_cork\"] = \"🍾\",\n        [\"ice_cube\"] = \"🧊\",\n        [\"spoon\"] = \"🥄\",\n        [\"fork_and_knife\"] = \"🍴\",\n        [\"fork_knife_plate\"] = \"🍽️\",\n        [\"fork_and_knife_with_plate\"] = \"🍽️\",\n        [\"bowl_with_spoon\"] = \"🥣\",\n        [\"takeout_box\"] = \"🥡\",\n        [\"chopsticks\"] = \"🥢\",\n        [\"salt\"] = \"🧂\",\n        [\"soccer\"] = \"⚽\",\n        [\"basketball\"] = \"🏀\",\n        [\"football\"] = \"🏈\",\n        [\"baseball\"] = \"⚾\",\n        [\"softball\"] = \"🥎\",\n        [\"tennis\"] = \"🎾\",\n        [\"volleyball\"] = \"🏐\",\n        [\"rugby_football\"] = \"🏉\",\n        [\"flying_disc\"] = \"🥏\",\n        [\"boomerang\"] = \"🪃\",\n        [\"8ball\"] = \"🎱\",\n        [\"yo_yo\"] = \"🪀\",\n        [\"ping_pong\"] = \"🏓\",\n        [\"table_tennis\"] = \"🏓\",\n        [\"badminton\"] = \"🏸\",\n        [\"hockey\"] = \"🏒\",\n        [\"field_hockey\"] = \"🏑\",\n        [\"lacrosse\"] = \"🥍\",\n        [\"cricket_game\"] = \"🏏\",\n        [\"cricket_bat_ball\"] = \"🏏\",\n        [\"goal\"] = \"🥅\",\n        [\"goal_net\"] = \"🥅\",\n        [\"golf\"] = \"⛳\",\n        [\"kite\"] = \"🪁\",\n        [\"bow_and_arrow\"] = \"🏹\",\n        [\"archery\"] = \"🏹\",\n        [\"fishing_pole_and_fish\"] = \"🎣\",\n        [\"diving_mask\"] = \"🤿\",\n        [\"boxing_glove\"] = \"🥊\",\n        [\"boxing_gloves\"] = \"🥊\",\n        [\"martial_arts_uniform\"] = \"🥋\",\n        [\"karate_uniform\"] = \"🥋\",\n        [\"running_shirt_with_sash\"] = \"🎽\",\n        [\"skateboard\"] = \"🛹\",\n        [\"roller_skate\"] = \"🛼\",\n        [\"sled\"] = \"🛷\",\n        [\"ice_skate\"] = \"⛸️\",\n        [\"curling_stone\"] = \"🥌\",\n        [\"ski\"] = \"🎿\",\n        [\"skier\"] = \"⛷️\",\n        [\"snowboarder\"] = \"🏂\",\n        [\"snowboarder_tone1\"] = \"🏂🏻\",\n        [\"snowboarder_light_skin_tone\"] = \"🏂🏻\",\n        [\"snowboarder_tone2\"] = \"🏂🏼\",\n        [\"snowboarder_medium_light_skin_tone\"] = \"🏂🏼\",\n        [\"snowboarder_tone3\"] = \"🏂🏽\",\n        [\"snowboarder_medium_skin_tone\"] = \"🏂🏽\",\n        [\"snowboarder_tone4\"] = \"🏂🏾\",\n        [\"snowboarder_medium_dark_skin_tone\"] = \"🏂🏾\",\n        [\"snowboarder_tone5\"] = \"🏂🏿\",\n        [\"snowboarder_dark_skin_tone\"] = \"🏂🏿\",\n        [\"parachute\"] = \"🪂\",\n        [\"person_lifting_weights\"] = \"🏋️\",\n        [\"lifter\"] = \"🏋️\",\n        [\"weight_lifter\"] = \"🏋️\",\n        [\"person_lifting_weights_tone1\"] = \"🏋🏻\",\n        [\"lifter_tone1\"] = \"🏋🏻\",\n        [\"weight_lifter_tone1\"] = \"🏋🏻\",\n        [\"person_lifting_weights_tone2\"] = \"🏋🏼\",\n        [\"lifter_tone2\"] = \"🏋🏼\",\n        [\"weight_lifter_tone2\"] = \"🏋🏼\",\n        [\"person_lifting_weights_tone3\"] = \"🏋🏽\",\n        [\"lifter_tone3\"] = \"🏋🏽\",\n        [\"weight_lifter_tone3\"] = \"🏋🏽\",\n        [\"person_lifting_weights_tone4\"] = \"🏋🏾\",\n        [\"lifter_tone4\"] = \"🏋🏾\",\n        [\"weight_lifter_tone4\"] = \"🏋🏾\",\n        [\"person_lifting_weights_tone5\"] = \"🏋🏿\",\n        [\"lifter_tone5\"] = \"🏋🏿\",\n        [\"weight_lifter_tone5\"] = \"🏋🏿\",\n        [\"woman_lifting_weights\"] = \"🏋️‍♀️\",\n        [\"woman_lifting_weights_tone1\"] = \"🏋🏻‍♀️\",\n        [\"woman_lifting_weights_light_skin_tone\"] = \"🏋🏻‍♀️\",\n        [\"woman_lifting_weights_tone2\"] = \"🏋🏼‍♀️\",\n        [\"woman_lifting_weights_medium_light_skin_tone\"] = \"🏋🏼‍♀️\",\n        [\"woman_lifting_weights_tone3\"] = \"🏋🏽‍♀️\",\n        [\"woman_lifting_weights_medium_skin_tone\"] = \"🏋🏽‍♀️\",\n        [\"woman_lifting_weights_tone4\"] = \"🏋🏾‍♀️\",\n        [\"woman_lifting_weights_medium_dark_skin_tone\"] = \"🏋🏾‍♀️\",\n        [\"woman_lifting_weights_tone5\"] = \"🏋🏿‍♀️\",\n        [\"woman_lifting_weights_dark_skin_tone\"] = \"🏋🏿‍♀️\",\n        [\"man_lifting_weights\"] = \"🏋️‍♂️\",\n        [\"man_lifting_weights_tone1\"] = \"🏋🏻‍♂️\",\n        [\"man_lifting_weights_light_skin_tone\"] = \"🏋🏻‍♂️\",\n        [\"man_lifting_weights_tone2\"] = \"🏋🏼‍♂️\",\n        [\"man_lifting_weights_medium_light_skin_tone\"] = \"🏋🏼‍♂️\",\n        [\"man_lifting_weights_tone3\"] = \"🏋🏽‍♂️\",\n        [\"man_lifting_weights_medium_skin_tone\"] = \"🏋🏽‍♂️\",\n        [\"man_lifting_weights_tone4\"] = \"🏋🏾‍♂️\",\n        [\"man_lifting_weights_medium_dark_skin_tone\"] = \"🏋🏾‍♂️\",\n        [\"man_lifting_weights_tone5\"] = \"🏋🏿‍♂️\",\n        [\"man_lifting_weights_dark_skin_tone\"] = \"🏋🏿‍♂️\",\n        [\"people_wrestling\"] = \"🤼\",\n        [\"wrestlers\"] = \"🤼\",\n        [\"wrestling\"] = \"🤼\",\n        [\"women_wrestling\"] = \"🤼‍♀️\",\n        [\"men_wrestling\"] = \"🤼‍♂️\",\n        [\"person_doing_cartwheel\"] = \"🤸\",\n        [\"cartwheel\"] = \"🤸\",\n        [\"person_doing_cartwheel_tone1\"] = \"🤸🏻\",\n        [\"cartwheel_tone1\"] = \"🤸🏻\",\n        [\"person_doing_cartwheel_tone2\"] = \"🤸🏼\",\n        [\"cartwheel_tone2\"] = \"🤸🏼\",\n        [\"person_doing_cartwheel_tone3\"] = \"🤸🏽\",\n        [\"cartwheel_tone3\"] = \"🤸🏽\",\n        [\"person_doing_cartwheel_tone4\"] = \"🤸🏾\",\n        [\"cartwheel_tone4\"] = \"🤸🏾\",\n        [\"person_doing_cartwheel_tone5\"] = \"🤸🏿\",\n        [\"cartwheel_tone5\"] = \"🤸🏿\",\n        [\"woman_cartwheeling\"] = \"🤸‍♀️\",\n        [\"woman_cartwheeling_tone1\"] = \"🤸🏻‍♀️\",\n        [\"woman_cartwheeling_light_skin_tone\"] = \"🤸🏻‍♀️\",\n        [\"woman_cartwheeling_tone2\"] = \"🤸🏼‍♀️\",\n        [\"woman_cartwheeling_medium_light_skin_tone\"] = \"🤸🏼‍♀️\",\n        [\"woman_cartwheeling_tone3\"] = \"🤸🏽‍♀️\",\n        [\"woman_cartwheeling_medium_skin_tone\"] = \"🤸🏽‍♀️\",\n        [\"woman_cartwheeling_tone4\"] = \"🤸🏾‍♀️\",\n        [\"woman_cartwheeling_medium_dark_skin_tone\"] = \"🤸🏾‍♀️\",\n        [\"woman_cartwheeling_tone5\"] = \"🤸🏿‍♀️\",\n        [\"woman_cartwheeling_dark_skin_tone\"] = \"🤸🏿‍♀️\",\n        [\"man_cartwheeling\"] = \"🤸‍♂️\",\n        [\"man_cartwheeling_tone1\"] = \"🤸🏻‍♂️\",\n        [\"man_cartwheeling_light_skin_tone\"] = \"🤸🏻‍♂️\",\n        [\"man_cartwheeling_tone2\"] = \"🤸🏼‍♂️\",\n        [\"man_cartwheeling_medium_light_skin_tone\"] = \"🤸🏼‍♂️\",\n        [\"man_cartwheeling_tone3\"] = \"🤸🏽‍♂️\",\n        [\"man_cartwheeling_medium_skin_tone\"] = \"🤸🏽‍♂️\",\n        [\"man_cartwheeling_tone4\"] = \"🤸🏾‍♂️\",\n        [\"man_cartwheeling_medium_dark_skin_tone\"] = \"🤸🏾‍♂️\",\n        [\"man_cartwheeling_tone5\"] = \"🤸🏿‍♂️\",\n        [\"man_cartwheeling_dark_skin_tone\"] = \"🤸🏿‍♂️\",\n        [\"person_bouncing_ball\"] = \"⛹️\",\n        [\"basketball_player\"] = \"⛹️\",\n        [\"person_with_ball\"] = \"⛹️\",\n        [\"person_bouncing_ball_tone1\"] = \"⛹🏻\",\n        [\"basketball_player_tone1\"] = \"⛹🏻\",\n        [\"person_with_ball_tone1\"] = \"⛹🏻\",\n        [\"person_bouncing_ball_tone2\"] = \"⛹🏼\",\n        [\"basketball_player_tone2\"] = \"⛹🏼\",\n        [\"person_with_ball_tone2\"] = \"⛹🏼\",\n        [\"person_bouncing_ball_tone3\"] = \"⛹🏽\",\n        [\"basketball_player_tone3\"] = \"⛹🏽\",\n        [\"person_with_ball_tone3\"] = \"⛹🏽\",\n        [\"person_bouncing_ball_tone4\"] = \"⛹🏾\",\n        [\"basketball_player_tone4\"] = \"⛹🏾\",\n        [\"person_with_ball_tone4\"] = \"⛹🏾\",\n        [\"person_bouncing_ball_tone5\"] = \"⛹🏿\",\n        [\"basketball_player_tone5\"] = \"⛹🏿\",\n        [\"person_with_ball_tone5\"] = \"⛹🏿\",\n        [\"woman_bouncing_ball\"] = \"⛹️‍♀️\",\n        [\"woman_bouncing_ball_tone1\"] = \"⛹🏻‍♀️\",\n        [\"woman_bouncing_ball_light_skin_tone\"] = \"⛹🏻‍♀️\",\n        [\"woman_bouncing_ball_tone2\"] = \"⛹🏼‍♀️\",\n        [\"woman_bouncing_ball_medium_light_skin_tone\"] = \"⛹🏼‍♀️\",\n        [\"woman_bouncing_ball_tone3\"] = \"⛹🏽‍♀️\",\n        [\"woman_bouncing_ball_medium_skin_tone\"] = \"⛹🏽‍♀️\",\n        [\"woman_bouncing_ball_tone4\"] = \"⛹🏾‍♀️\",\n        [\"woman_bouncing_ball_medium_dark_skin_tone\"] = \"⛹🏾‍♀️\",\n        [\"woman_bouncing_ball_tone5\"] = \"⛹🏿‍♀️\",\n        [\"woman_bouncing_ball_dark_skin_tone\"] = \"⛹🏿‍♀️\",\n        [\"man_bouncing_ball\"] = \"⛹️‍♂️\",\n        [\"man_bouncing_ball_tone1\"] = \"⛹🏻‍♂️\",\n        [\"man_bouncing_ball_light_skin_tone\"] = \"⛹🏻‍♂️\",\n        [\"man_bouncing_ball_tone2\"] = \"⛹🏼‍♂️\",\n        [\"man_bouncing_ball_medium_light_skin_tone\"] = \"⛹🏼‍♂️\",\n        [\"man_bouncing_ball_tone3\"] = \"⛹🏽‍♂️\",\n        [\"man_bouncing_ball_medium_skin_tone\"] = \"⛹🏽‍♂️\",\n        [\"man_bouncing_ball_tone4\"] = \"⛹🏾‍♂️\",\n        [\"man_bouncing_ball_medium_dark_skin_tone\"] = \"⛹🏾‍♂️\",\n        [\"man_bouncing_ball_tone5\"] = \"⛹🏿‍♂️\",\n        [\"man_bouncing_ball_dark_skin_tone\"] = \"⛹🏿‍♂️\",\n        [\"person_fencing\"] = \"🤺\",\n        [\"fencer\"] = \"🤺\",\n        [\"fencing\"] = \"🤺\",\n        [\"person_playing_handball\"] = \"🤾\",\n        [\"handball\"] = \"🤾\",\n        [\"person_playing_handball_tone1\"] = \"🤾🏻\",\n        [\"handball_tone1\"] = \"🤾🏻\",\n        [\"person_playing_handball_tone2\"] = \"🤾🏼\",\n        [\"handball_tone2\"] = \"🤾🏼\",\n        [\"person_playing_handball_tone3\"] = \"🤾🏽\",\n        [\"handball_tone3\"] = \"🤾🏽\",\n        [\"person_playing_handball_tone4\"] = \"🤾🏾\",\n        [\"handball_tone4\"] = \"🤾🏾\",\n        [\"person_playing_handball_tone5\"] = \"🤾🏿\",\n        [\"handball_tone5\"] = \"🤾🏿\",\n        [\"woman_playing_handball\"] = \"🤾‍♀️\",\n        [\"woman_playing_handball_tone1\"] = \"🤾🏻‍♀️\",\n        [\"woman_playing_handball_light_skin_tone\"] = \"🤾🏻‍♀️\",\n        [\"woman_playing_handball_tone2\"] = \"🤾🏼‍♀️\",\n        [\"woman_playing_handball_medium_light_skin_tone\"] = \"🤾🏼‍♀️\",\n        [\"woman_playing_handball_tone3\"] = \"🤾🏽‍♀️\",\n        [\"woman_playing_handball_medium_skin_tone\"] = \"🤾🏽‍♀️\",\n        [\"woman_playing_handball_tone4\"] = \"🤾🏾‍♀️\",\n        [\"woman_playing_handball_medium_dark_skin_tone\"] = \"🤾🏾‍♀️\",\n        [\"woman_playing_handball_tone5\"] = \"🤾🏿‍♀️\",\n        [\"woman_playing_handball_dark_skin_tone\"] = \"🤾🏿‍♀️\",\n        [\"man_playing_handball\"] = \"🤾‍♂️\",\n        [\"man_playing_handball_tone1\"] = \"🤾🏻‍♂️\",\n        [\"man_playing_handball_light_skin_tone\"] = \"🤾🏻‍♂️\",\n        [\"man_playing_handball_tone2\"] = \"🤾🏼‍♂️\",\n        [\"man_playing_handball_medium_light_skin_tone\"] = \"🤾🏼‍♂️\",\n        [\"man_playing_handball_tone3\"] = \"🤾🏽‍♂️\",\n        [\"man_playing_handball_medium_skin_tone\"] = \"🤾🏽‍♂️\",\n        [\"man_playing_handball_tone4\"] = \"🤾🏾‍♂️\",\n        [\"man_playing_handball_medium_dark_skin_tone\"] = \"🤾🏾‍♂️\",\n        [\"man_playing_handball_tone5\"] = \"🤾🏿‍♂️\",\n        [\"man_playing_handball_dark_skin_tone\"] = \"🤾🏿‍♂️\",\n        [\"person_golfing\"] = \"🏌️\",\n        [\"golfer\"] = \"🏌️\",\n        [\"person_golfing_tone1\"] = \"🏌🏻\",\n        [\"person_golfing_light_skin_tone\"] = \"🏌🏻\",\n        [\"person_golfing_tone2\"] = \"🏌🏼\",\n        [\"person_golfing_medium_light_skin_tone\"] = \"🏌🏼\",\n        [\"person_golfing_tone3\"] = \"🏌🏽\",\n        [\"person_golfing_medium_skin_tone\"] = \"🏌🏽\",\n        [\"person_golfing_tone4\"] = \"🏌🏾\",\n        [\"person_golfing_medium_dark_skin_tone\"] = \"🏌🏾\",\n        [\"person_golfing_tone5\"] = \"🏌🏿\",\n        [\"person_golfing_dark_skin_tone\"] = \"🏌🏿\",\n        [\"woman_golfing\"] = \"🏌️‍♀️\",\n        [\"woman_golfing_tone1\"] = \"🏌🏻‍♀️\",\n        [\"woman_golfing_light_skin_tone\"] = \"🏌🏻‍♀️\",\n        [\"woman_golfing_tone2\"] = \"🏌🏼‍♀️\",\n        [\"woman_golfing_medium_light_skin_tone\"] = \"🏌🏼‍♀️\",\n        [\"woman_golfing_tone3\"] = \"🏌🏽‍♀️\",\n        [\"woman_golfing_medium_skin_tone\"] = \"🏌🏽‍♀️\",\n        [\"woman_golfing_tone4\"] = \"🏌🏾‍♀️\",\n        [\"woman_golfing_medium_dark_skin_tone\"] = \"🏌🏾‍♀️\",\n        [\"woman_golfing_tone5\"] = \"🏌🏿‍♀️\",\n        [\"woman_golfing_dark_skin_tone\"] = \"🏌🏿‍♀️\",\n        [\"man_golfing\"] = \"🏌️‍♂️\",\n        [\"man_golfing_tone1\"] = \"🏌🏻‍♂️\",\n        [\"man_golfing_light_skin_tone\"] = \"🏌🏻‍♂️\",\n        [\"man_golfing_tone2\"] = \"🏌🏼‍♂️\",\n        [\"man_golfing_medium_light_skin_tone\"] = \"🏌🏼‍♂️\",\n        [\"man_golfing_tone3\"] = \"🏌🏽‍♂️\",\n        [\"man_golfing_medium_skin_tone\"] = \"🏌🏽‍♂️\",\n        [\"man_golfing_tone4\"] = \"🏌🏾‍♂️\",\n        [\"man_golfing_medium_dark_skin_tone\"] = \"🏌🏾‍♂️\",\n        [\"man_golfing_tone5\"] = \"🏌🏿‍♂️\",\n        [\"man_golfing_dark_skin_tone\"] = \"🏌🏿‍♂️\",\n        [\"horse_racing\"] = \"🏇\",\n        [\"horse_racing_tone1\"] = \"🏇🏻\",\n        [\"horse_racing_tone2\"] = \"🏇🏼\",\n        [\"horse_racing_tone3\"] = \"🏇🏽\",\n        [\"horse_racing_tone4\"] = \"🏇🏾\",\n        [\"horse_racing_tone5\"] = \"🏇🏿\",\n        [\"person_in_lotus_position\"] = \"🧘\",\n        [\"person_in_lotus_position_tone1\"] = \"🧘🏻\",\n        [\"person_in_lotus_position_light_skin_tone\"] = \"🧘🏻\",\n        [\"person_in_lotus_position_tone2\"] = \"🧘🏼\",\n        [\"person_in_lotus_position_medium_light_skin_tone\"] = \"🧘🏼\",\n        [\"person_in_lotus_position_tone3\"] = \"🧘🏽\",\n        [\"person_in_lotus_position_medium_skin_tone\"] = \"🧘🏽\",\n        [\"person_in_lotus_position_tone4\"] = \"🧘🏾\",\n        [\"person_in_lotus_position_medium_dark_skin_tone\"] = \"🧘🏾\",\n        [\"person_in_lotus_position_tone5\"] = \"🧘🏿\",\n        [\"person_in_lotus_position_dark_skin_tone\"] = \"🧘🏿\",\n        [\"woman_in_lotus_position\"] = \"🧘‍♀️\",\n        [\"woman_in_lotus_position_tone1\"] = \"🧘🏻‍♀️\",\n        [\"woman_in_lotus_position_light_skin_tone\"] = \"🧘🏻‍♀️\",\n        [\"woman_in_lotus_position_tone2\"] = \"🧘🏼‍♀️\",\n        [\"woman_in_lotus_position_medium_light_skin_tone\"] = \"🧘🏼‍♀️\",\n        [\"woman_in_lotus_position_tone3\"] = \"🧘🏽‍♀️\",\n        [\"woman_in_lotus_position_medium_skin_tone\"] = \"🧘🏽‍♀️\",\n        [\"woman_in_lotus_position_tone4\"] = \"🧘🏾‍♀️\",\n        [\"woman_in_lotus_position_medium_dark_skin_tone\"] = \"🧘🏾‍♀️\",\n        [\"woman_in_lotus_position_tone5\"] = \"🧘🏿‍♀️\",\n        [\"woman_in_lotus_position_dark_skin_tone\"] = \"🧘🏿‍♀️\",\n        [\"man_in_lotus_position\"] = \"🧘‍♂️\",\n        [\"man_in_lotus_position_tone1\"] = \"🧘🏻‍♂️\",\n        [\"man_in_lotus_position_light_skin_tone\"] = \"🧘🏻‍♂️\",\n        [\"man_in_lotus_position_tone2\"] = \"🧘🏼‍♂️\",\n        [\"man_in_lotus_position_medium_light_skin_tone\"] = \"🧘🏼‍♂️\",\n        [\"man_in_lotus_position_tone3\"] = \"🧘🏽‍♂️\",\n        [\"man_in_lotus_position_medium_skin_tone\"] = \"🧘🏽‍♂️\",\n        [\"man_in_lotus_position_tone4\"] = \"🧘🏾‍♂️\",\n        [\"man_in_lotus_position_medium_dark_skin_tone\"] = \"🧘🏾‍♂️\",\n        [\"man_in_lotus_position_tone5\"] = \"🧘🏿‍♂️\",\n        [\"man_in_lotus_position_dark_skin_tone\"] = \"🧘🏿‍♂️\",\n        [\"person_surfing\"] = \"🏄\",\n        [\"surfer\"] = \"🏄\",\n        [\"person_surfing_tone1\"] = \"🏄🏻\",\n        [\"surfer_tone1\"] = \"🏄🏻\",\n        [\"person_surfing_tone2\"] = \"🏄🏼\",\n        [\"surfer_tone2\"] = \"🏄🏼\",\n        [\"person_surfing_tone3\"] = \"🏄🏽\",\n        [\"surfer_tone3\"] = \"🏄🏽\",\n        [\"person_surfing_tone4\"] = \"🏄🏾\",\n        [\"surfer_tone4\"] = \"🏄🏾\",\n        [\"person_surfing_tone5\"] = \"🏄🏿\",\n        [\"surfer_tone5\"] = \"🏄🏿\",\n        [\"woman_surfing\"] = \"🏄‍♀️\",\n        [\"woman_surfing_tone1\"] = \"🏄🏻‍♀️\",\n        [\"woman_surfing_light_skin_tone\"] = \"🏄🏻‍♀️\",\n        [\"woman_surfing_tone2\"] = \"🏄🏼‍♀️\",\n        [\"woman_surfing_medium_light_skin_tone\"] = \"🏄🏼‍♀️\",\n        [\"woman_surfing_tone3\"] = \"🏄🏽‍♀️\",\n        [\"woman_surfing_medium_skin_tone\"] = \"🏄🏽‍♀️\",\n        [\"woman_surfing_tone4\"] = \"🏄🏾‍♀️\",\n        [\"woman_surfing_medium_dark_skin_tone\"] = \"🏄🏾‍♀️\",\n        [\"woman_surfing_tone5\"] = \"🏄🏿‍♀️\",\n        [\"woman_surfing_dark_skin_tone\"] = \"🏄🏿‍♀️\",\n        [\"man_surfing\"] = \"🏄‍♂️\",\n        [\"man_surfing_tone1\"] = \"🏄🏻‍♂️\",\n        [\"man_surfing_light_skin_tone\"] = \"🏄🏻‍♂️\",\n        [\"man_surfing_tone2\"] = \"🏄🏼‍♂️\",\n        [\"man_surfing_medium_light_skin_tone\"] = \"🏄🏼‍♂️\",\n        [\"man_surfing_tone3\"] = \"🏄🏽‍♂️\",\n        [\"man_surfing_medium_skin_tone\"] = \"🏄🏽‍♂️\",\n        [\"man_surfing_tone4\"] = \"🏄🏾‍♂️\",\n        [\"man_surfing_medium_dark_skin_tone\"] = \"🏄🏾‍♂️\",\n        [\"man_surfing_tone5\"] = \"🏄🏿‍♂️\",\n        [\"man_surfing_dark_skin_tone\"] = \"🏄🏿‍♂️\",\n        [\"person_swimming\"] = \"🏊\",\n        [\"swimmer\"] = \"🏊\",\n        [\"person_swimming_tone1\"] = \"🏊🏻\",\n        [\"swimmer_tone1\"] = \"🏊🏻\",\n        [\"person_swimming_tone2\"] = \"🏊🏼\",\n        [\"swimmer_tone2\"] = \"🏊🏼\",\n        [\"person_swimming_tone3\"] = \"🏊🏽\",\n        [\"swimmer_tone3\"] = \"🏊🏽\",\n        [\"person_swimming_tone4\"] = \"🏊🏾\",\n        [\"swimmer_tone4\"] = \"🏊🏾\",\n        [\"person_swimming_tone5\"] = \"🏊🏿\",\n        [\"swimmer_tone5\"] = \"🏊🏿\",\n        [\"woman_swimming\"] = \"🏊‍♀️\",\n        [\"woman_swimming_tone1\"] = \"🏊🏻‍♀️\",\n        [\"woman_swimming_light_skin_tone\"] = \"🏊🏻‍♀️\",\n        [\"woman_swimming_tone2\"] = \"🏊🏼‍♀️\",\n        [\"woman_swimming_medium_light_skin_tone\"] = \"🏊🏼‍♀️\",\n        [\"woman_swimming_tone3\"] = \"🏊🏽‍♀️\",\n        [\"woman_swimming_medium_skin_tone\"] = \"🏊🏽‍♀️\",\n        [\"woman_swimming_tone4\"] = \"🏊🏾‍♀️\",\n        [\"woman_swimming_medium_dark_skin_tone\"] = \"🏊🏾‍♀️\",\n        [\"woman_swimming_tone5\"] = \"🏊🏿‍♀️\",\n        [\"woman_swimming_dark_skin_tone\"] = \"🏊🏿‍♀️\",\n        [\"man_swimming\"] = \"🏊‍♂️\",\n        [\"man_swimming_tone1\"] = \"🏊🏻‍♂️\",\n        [\"man_swimming_light_skin_tone\"] = \"🏊🏻‍♂️\",\n        [\"man_swimming_tone2\"] = \"🏊🏼‍♂️\",\n        [\"man_swimming_medium_light_skin_tone\"] = \"🏊🏼‍♂️\",\n        [\"man_swimming_tone3\"] = \"🏊🏽‍♂️\",\n        [\"man_swimming_medium_skin_tone\"] = \"🏊🏽‍♂️\",\n        [\"man_swimming_tone4\"] = \"🏊🏾‍♂️\",\n        [\"man_swimming_medium_dark_skin_tone\"] = \"🏊🏾‍♂️\",\n        [\"man_swimming_tone5\"] = \"🏊🏿‍♂️\",\n        [\"man_swimming_dark_skin_tone\"] = \"🏊🏿‍♂️\",\n        [\"person_playing_water_polo\"] = \"🤽\",\n        [\"water_polo\"] = \"🤽\",\n        [\"person_playing_water_polo_tone1\"] = \"🤽🏻\",\n        [\"water_polo_tone1\"] = \"🤽🏻\",\n        [\"person_playing_water_polo_tone2\"] = \"🤽🏼\",\n        [\"water_polo_tone2\"] = \"🤽🏼\",\n        [\"person_playing_water_polo_tone3\"] = \"🤽🏽\",\n        [\"water_polo_tone3\"] = \"🤽🏽\",\n        [\"person_playing_water_polo_tone4\"] = \"🤽🏾\",\n        [\"water_polo_tone4\"] = \"🤽🏾\",\n        [\"person_playing_water_polo_tone5\"] = \"🤽🏿\",\n        [\"water_polo_tone5\"] = \"🤽🏿\",\n        [\"woman_playing_water_polo\"] = \"🤽‍♀️\",\n        [\"woman_playing_water_polo_tone1\"] = \"🤽🏻‍♀️\",\n        [\"woman_playing_water_polo_light_skin_tone\"] = \"🤽🏻‍♀️\",\n        [\"woman_playing_water_polo_tone2\"] = \"🤽🏼‍♀️\",\n        [\"woman_playing_water_polo_medium_light_skin_tone\"] = \"🤽🏼‍♀️\",\n        [\"woman_playing_water_polo_tone3\"] = \"🤽🏽‍♀️\",\n        [\"woman_playing_water_polo_medium_skin_tone\"] = \"🤽🏽‍♀️\",\n        [\"woman_playing_water_polo_tone4\"] = \"🤽🏾‍♀️\",\n        [\"woman_playing_water_polo_medium_dark_skin_tone\"] = \"🤽🏾‍♀️\",\n        [\"woman_playing_water_polo_tone5\"] = \"🤽🏿‍♀️\",\n        [\"woman_playing_water_polo_dark_skin_tone\"] = \"🤽🏿‍♀️\",\n        [\"man_playing_water_polo\"] = \"🤽‍♂️\",\n        [\"man_playing_water_polo_tone1\"] = \"🤽🏻‍♂️\",\n        [\"man_playing_water_polo_light_skin_tone\"] = \"🤽🏻‍♂️\",\n        [\"man_playing_water_polo_tone2\"] = \"🤽🏼‍♂️\",\n        [\"man_playing_water_polo_medium_light_skin_tone\"] = \"🤽🏼‍♂️\",\n        [\"man_playing_water_polo_tone3\"] = \"🤽🏽‍♂️\",\n        [\"man_playing_water_polo_medium_skin_tone\"] = \"🤽🏽‍♂️\",\n        [\"man_playing_water_polo_tone4\"] = \"🤽🏾‍♂️\",\n        [\"man_playing_water_polo_medium_dark_skin_tone\"] = \"🤽🏾‍♂️\",\n        [\"man_playing_water_polo_tone5\"] = \"🤽🏿‍♂️\",\n        [\"man_playing_water_polo_dark_skin_tone\"] = \"🤽🏿‍♂️\",\n        [\"person_rowing_boat\"] = \"🚣\",\n        [\"rowboat\"] = \"🚣\",\n        [\"person_rowing_boat_tone1\"] = \"🚣🏻\",\n        [\"rowboat_tone1\"] = \"🚣🏻\",\n        [\"person_rowing_boat_tone2\"] = \"🚣🏼\",\n        [\"rowboat_tone2\"] = \"🚣🏼\",\n        [\"person_rowing_boat_tone3\"] = \"🚣🏽\",\n        [\"rowboat_tone3\"] = \"🚣🏽\",\n        [\"person_rowing_boat_tone4\"] = \"🚣🏾\",\n        [\"rowboat_tone4\"] = \"🚣🏾\",\n        [\"person_rowing_boat_tone5\"] = \"🚣🏿\",\n        [\"rowboat_tone5\"] = \"🚣🏿\",\n        [\"woman_rowing_boat\"] = \"🚣‍♀️\",\n        [\"woman_rowing_boat_tone1\"] = \"🚣🏻‍♀️\",\n        [\"woman_rowing_boat_light_skin_tone\"] = \"🚣🏻‍♀️\",\n        [\"woman_rowing_boat_tone2\"] = \"🚣🏼‍♀️\",\n        [\"woman_rowing_boat_medium_light_skin_tone\"] = \"🚣🏼‍♀️\",\n        [\"woman_rowing_boat_tone3\"] = \"🚣🏽‍♀️\",\n        [\"woman_rowing_boat_medium_skin_tone\"] = \"🚣🏽‍♀️\",\n        [\"woman_rowing_boat_tone4\"] = \"🚣🏾‍♀️\",\n        [\"woman_rowing_boat_medium_dark_skin_tone\"] = \"🚣🏾‍♀️\",\n        [\"woman_rowing_boat_tone5\"] = \"🚣🏿‍♀️\",\n        [\"woman_rowing_boat_dark_skin_tone\"] = \"🚣🏿‍♀️\",\n        [\"man_rowing_boat\"] = \"🚣‍♂️\",\n        [\"man_rowing_boat_tone1\"] = \"🚣🏻‍♂️\",\n        [\"man_rowing_boat_light_skin_tone\"] = \"🚣🏻‍♂️\",\n        [\"man_rowing_boat_tone2\"] = \"🚣🏼‍♂️\",\n        [\"man_rowing_boat_medium_light_skin_tone\"] = \"🚣🏼‍♂️\",\n        [\"man_rowing_boat_tone3\"] = \"🚣🏽‍♂️\",\n        [\"man_rowing_boat_medium_skin_tone\"] = \"🚣🏽‍♂️\",\n        [\"man_rowing_boat_tone4\"] = \"🚣🏾‍♂️\",\n        [\"man_rowing_boat_medium_dark_skin_tone\"] = \"🚣🏾‍♂️\",\n        [\"man_rowing_boat_tone5\"] = \"🚣🏿‍♂️\",\n        [\"man_rowing_boat_dark_skin_tone\"] = \"🚣🏿‍♂️\",\n        [\"person_climbing\"] = \"🧗\",\n        [\"person_climbing_tone1\"] = \"🧗🏻\",\n        [\"person_climbing_light_skin_tone\"] = \"🧗🏻\",\n        [\"person_climbing_tone2\"] = \"🧗🏼\",\n        [\"person_climbing_medium_light_skin_tone\"] = \"🧗🏼\",\n        [\"person_climbing_tone3\"] = \"🧗🏽\",\n        [\"person_climbing_medium_skin_tone\"] = \"🧗🏽\",\n        [\"person_climbing_tone4\"] = \"🧗🏾\",\n        [\"person_climbing_medium_dark_skin_tone\"] = \"🧗🏾\",\n        [\"person_climbing_tone5\"] = \"🧗🏿\",\n        [\"person_climbing_dark_skin_tone\"] = \"🧗🏿\",\n        [\"woman_climbing\"] = \"🧗‍♀️\",\n        [\"woman_climbing_tone1\"] = \"🧗🏻‍♀️\",\n        [\"woman_climbing_light_skin_tone\"] = \"🧗🏻‍♀️\",\n        [\"woman_climbing_tone2\"] = \"🧗🏼‍♀️\",\n        [\"woman_climbing_medium_light_skin_tone\"] = \"🧗🏼‍♀️\",\n        [\"woman_climbing_tone3\"] = \"🧗🏽‍♀️\",\n        [\"woman_climbing_medium_skin_tone\"] = \"🧗🏽‍♀️\",\n        [\"woman_climbing_tone4\"] = \"🧗🏾‍♀️\",\n        [\"woman_climbing_medium_dark_skin_tone\"] = \"🧗🏾‍♀️\",\n        [\"woman_climbing_tone5\"] = \"🧗🏿‍♀️\",\n        [\"woman_climbing_dark_skin_tone\"] = \"🧗🏿‍♀️\",\n        [\"man_climbing\"] = \"🧗‍♂️\",\n        [\"man_climbing_tone1\"] = \"🧗🏻‍♂️\",\n        [\"man_climbing_light_skin_tone\"] = \"🧗🏻‍♂️\",\n        [\"man_climbing_tone2\"] = \"🧗🏼‍♂️\",\n        [\"man_climbing_medium_light_skin_tone\"] = \"🧗🏼‍♂️\",\n        [\"man_climbing_tone3\"] = \"🧗🏽‍♂️\",\n        [\"man_climbing_medium_skin_tone\"] = \"🧗🏽‍♂️\",\n        [\"man_climbing_tone4\"] = \"🧗🏾‍♂️\",\n        [\"man_climbing_medium_dark_skin_tone\"] = \"🧗🏾‍♂️\",\n        [\"man_climbing_tone5\"] = \"🧗🏿‍♂️\",\n        [\"man_climbing_dark_skin_tone\"] = \"🧗🏿‍♂️\",\n        [\"person_mountain_biking\"] = \"🚵\",\n        [\"mountain_bicyclist\"] = \"🚵\",\n        [\"person_mountain_biking_tone1\"] = \"🚵🏻\",\n        [\"mountain_bicyclist_tone1\"] = \"🚵🏻\",\n        [\"person_mountain_biking_tone2\"] = \"🚵🏼\",\n        [\"mountain_bicyclist_tone2\"] = \"🚵🏼\",\n        [\"person_mountain_biking_tone3\"] = \"🚵🏽\",\n        [\"mountain_bicyclist_tone3\"] = \"🚵🏽\",\n        [\"person_mountain_biking_tone4\"] = \"🚵🏾\",\n        [\"mountain_bicyclist_tone4\"] = \"🚵🏾\",\n        [\"person_mountain_biking_tone5\"] = \"🚵🏿\",\n        [\"mountain_bicyclist_tone5\"] = \"🚵🏿\",\n        [\"woman_mountain_biking\"] = \"🚵‍♀️\",\n        [\"woman_mountain_biking_tone1\"] = \"🚵🏻‍♀️\",\n        [\"woman_mountain_biking_light_skin_tone\"] = \"🚵🏻‍♀️\",\n        [\"woman_mountain_biking_tone2\"] = \"🚵🏼‍♀️\",\n        [\"woman_mountain_biking_medium_light_skin_tone\"] = \"🚵🏼‍♀️\",\n        [\"woman_mountain_biking_tone3\"] = \"🚵🏽‍♀️\",\n        [\"woman_mountain_biking_medium_skin_tone\"] = \"🚵🏽‍♀️\",\n        [\"woman_mountain_biking_tone4\"] = \"🚵🏾‍♀️\",\n        [\"woman_mountain_biking_medium_dark_skin_tone\"] = \"🚵🏾‍♀️\",\n        [\"woman_mountain_biking_tone5\"] = \"🚵🏿‍♀️\",\n        [\"woman_mountain_biking_dark_skin_tone\"] = \"🚵🏿‍♀️\",\n        [\"man_mountain_biking\"] = \"🚵‍♂️\",\n        [\"man_mountain_biking_tone1\"] = \"🚵🏻‍♂️\",\n        [\"man_mountain_biking_light_skin_tone\"] = \"🚵🏻‍♂️\",\n        [\"man_mountain_biking_tone2\"] = \"🚵🏼‍♂️\",\n        [\"man_mountain_biking_medium_light_skin_tone\"] = \"🚵🏼‍♂️\",\n        [\"man_mountain_biking_tone3\"] = \"🚵🏽‍♂️\",\n        [\"man_mountain_biking_medium_skin_tone\"] = \"🚵🏽‍♂️\",\n        [\"man_mountain_biking_tone4\"] = \"🚵🏾‍♂️\",\n        [\"man_mountain_biking_medium_dark_skin_tone\"] = \"🚵🏾‍♂️\",\n        [\"man_mountain_biking_tone5\"] = \"🚵🏿‍♂️\",\n        [\"man_mountain_biking_dark_skin_tone\"] = \"🚵🏿‍♂️\",\n        [\"person_biking\"] = \"🚴\",\n        [\"bicyclist\"] = \"🚴\",\n        [\"person_biking_tone1\"] = \"🚴🏻\",\n        [\"bicyclist_tone1\"] = \"🚴🏻\",\n        [\"person_biking_tone2\"] = \"🚴🏼\",\n        [\"bicyclist_tone2\"] = \"🚴🏼\",\n        [\"person_biking_tone3\"] = \"🚴🏽\",\n        [\"bicyclist_tone3\"] = \"🚴🏽\",\n        [\"person_biking_tone4\"] = \"🚴🏾\",\n        [\"bicyclist_tone4\"] = \"🚴🏾\",\n        [\"person_biking_tone5\"] = \"🚴🏿\",\n        [\"bicyclist_tone5\"] = \"🚴🏿\",\n        [\"woman_biking\"] = \"🚴‍♀️\",\n        [\"woman_biking_tone1\"] = \"🚴🏻‍♀️\",\n        [\"woman_biking_light_skin_tone\"] = \"🚴🏻‍♀️\",\n        [\"woman_biking_tone2\"] = \"🚴🏼‍♀️\",\n        [\"woman_biking_medium_light_skin_tone\"] = \"🚴🏼‍♀️\",\n        [\"woman_biking_tone3\"] = \"🚴🏽‍♀️\",\n        [\"woman_biking_medium_skin_tone\"] = \"🚴🏽‍♀️\",\n        [\"woman_biking_tone4\"] = \"🚴🏾‍♀️\",\n        [\"woman_biking_medium_dark_skin_tone\"] = \"🚴🏾‍♀️\",\n        [\"woman_biking_tone5\"] = \"🚴🏿‍♀️\",\n        [\"woman_biking_dark_skin_tone\"] = \"🚴🏿‍♀️\",\n        [\"man_biking\"] = \"🚴‍♂️\",\n        [\"man_biking_tone1\"] = \"🚴🏻‍♂️\",\n        [\"man_biking_light_skin_tone\"] = \"🚴🏻‍♂️\",\n        [\"man_biking_tone2\"] = \"🚴🏼‍♂️\",\n        [\"man_biking_medium_light_skin_tone\"] = \"🚴🏼‍♂️\",\n        [\"man_biking_tone3\"] = \"🚴🏽‍♂️\",\n        [\"man_biking_medium_skin_tone\"] = \"🚴🏽‍♂️\",\n        [\"man_biking_tone4\"] = \"🚴🏾‍♂️\",\n        [\"man_biking_medium_dark_skin_tone\"] = \"🚴🏾‍♂️\",\n        [\"man_biking_tone5\"] = \"🚴🏿‍♂️\",\n        [\"man_biking_dark_skin_tone\"] = \"🚴🏿‍♂️\",\n        [\"trophy\"] = \"🏆\",\n        [\"first_place\"] = \"🥇\",\n        [\"first_place_medal\"] = \"🥇\",\n        [\"second_place\"] = \"🥈\",\n        [\"second_place_medal\"] = \"🥈\",\n        [\"third_place\"] = \"🥉\",\n        [\"third_place_medal\"] = \"🥉\",\n        [\"medal\"] = \"🏅\",\n        [\"sports_medal\"] = \"🏅\",\n        [\"military_medal\"] = \"🎖️\",\n        [\"rosette\"] = \"🏵️\",\n        [\"reminder_ribbon\"] = \"🎗️\",\n        [\"ticket\"] = \"🎫\",\n        [\"tickets\"] = \"🎟️\",\n        [\"admission_tickets\"] = \"🎟️\",\n        [\"circus_tent\"] = \"🎪\",\n        [\"person_juggling\"] = \"🤹\",\n        [\"juggling\"] = \"🤹\",\n        [\"juggler\"] = \"🤹\",\n        [\"person_juggling_tone1\"] = \"🤹🏻\",\n        [\"juggling_tone1\"] = \"🤹🏻\",\n        [\"juggler_tone1\"] = \"🤹🏻\",\n        [\"person_juggling_tone2\"] = \"🤹🏼\",\n        [\"juggling_tone2\"] = \"🤹🏼\",\n        [\"juggler_tone2\"] = \"🤹🏼\",\n        [\"person_juggling_tone3\"] = \"🤹🏽\",\n        [\"juggling_tone3\"] = \"🤹🏽\",\n        [\"juggler_tone3\"] = \"🤹🏽\",\n        [\"person_juggling_tone4\"] = \"🤹🏾\",\n        [\"juggling_tone4\"] = \"🤹🏾\",\n        [\"juggler_tone4\"] = \"🤹🏾\",\n        [\"person_juggling_tone5\"] = \"🤹🏿\",\n        [\"juggling_tone5\"] = \"🤹🏿\",\n        [\"juggler_tone5\"] = \"🤹🏿\",\n        [\"woman_juggling\"] = \"🤹‍♀️\",\n        [\"woman_juggling_tone1\"] = \"🤹🏻‍♀️\",\n        [\"woman_juggling_light_skin_tone\"] = \"🤹🏻‍♀️\",\n        [\"woman_juggling_tone2\"] = \"🤹🏼‍♀️\",\n        [\"woman_juggling_medium_light_skin_tone\"] = \"🤹🏼‍♀️\",\n        [\"woman_juggling_tone3\"] = \"🤹🏽‍♀️\",\n        [\"woman_juggling_medium_skin_tone\"] = \"🤹🏽‍♀️\",\n        [\"woman_juggling_tone4\"] = \"🤹🏾‍♀️\",\n        [\"woman_juggling_medium_dark_skin_tone\"] = \"🤹🏾‍♀️\",\n        [\"woman_juggling_tone5\"] = \"🤹🏿‍♀️\",\n        [\"woman_juggling_dark_skin_tone\"] = \"🤹🏿‍♀️\",\n        [\"man_juggling\"] = \"🤹‍♂️\",\n        [\"man_juggling_tone1\"] = \"🤹🏻‍♂️\",\n        [\"man_juggling_light_skin_tone\"] = \"🤹🏻‍♂️\",\n        [\"man_juggling_tone2\"] = \"🤹🏼‍♂️\",\n        [\"man_juggling_medium_light_skin_tone\"] = \"🤹🏼‍♂️\",\n        [\"man_juggling_tone3\"] = \"🤹🏽‍♂️\",\n        [\"man_juggling_medium_skin_tone\"] = \"🤹🏽‍♂️\",\n        [\"man_juggling_tone4\"] = \"🤹🏾‍♂️\",\n        [\"man_juggling_medium_dark_skin_tone\"] = \"🤹🏾‍♂️\",\n        [\"man_juggling_tone5\"] = \"🤹🏿‍♂️\",\n        [\"man_juggling_dark_skin_tone\"] = \"🤹🏿‍♂️\",\n        [\"performing_arts\"] = \"🎭\",\n        [\"ballet_shoes\"] = \"🩰\",\n        [\"art\"] = \"🎨\",\n        [\"clapper\"] = \"🎬\",\n        [\"microphone\"] = \"🎤\",\n        [\"headphones\"] = \"🎧\",\n        [\"musical_score\"] = \"🎼\",\n        [\"musical_keyboard\"] = \"🎹\",\n        [\"drum\"] = \"🥁\",\n        [\"drum_with_drumsticks\"] = \"🥁\",\n        [\"long_drum\"] = \"🪘\",\n        [\"saxophone\"] = \"🎷\",\n        [\"trumpet\"] = \"🎺\",\n        [\"guitar\"] = \"🎸\",\n        [\"banjo\"] = \"🪕\",\n        [\"violin\"] = \"🎻\",\n        [\"accordion\"] = \"🪗\",\n        [\"game_die\"] = \"🎲\",\n        [\"chess_pawn\"] = \"♟️\",\n        [\"dart\"] = \"🎯\",\n        [\"bowling\"] = \"🎳\",\n        [\"video_game\"] = \"🎮\",\n        [\"slot_machine\"] = \"🎰\",\n        [\"jigsaw\"] = \"🧩\",\n        [\"red_car\"] = \"🚗\",\n        [\"taxi\"] = \"🚕\",\n        [\"blue_car\"] = \"🚙\",\n        [\"pickup_truck\"] = \"🛻\",\n        [\"bus\"] = \"🚌\",\n        [\"trolleybus\"] = \"🚎\",\n        [\"race_car\"] = \"🏎️\",\n        [\"racing_car\"] = \"🏎️\",\n        [\"police_car\"] = \"🚓\",\n        [\"ambulance\"] = \"🚑\",\n        [\"fire_engine\"] = \"🚒\",\n        [\"minibus\"] = \"🚐\",\n        [\"truck\"] = \"🚚\",\n        [\"articulated_lorry\"] = \"🚛\",\n        [\"tractor\"] = \"🚜\",\n        [\"probing_cane\"] = \"🦯\",\n        [\"manual_wheelchair\"] = \"🦽\",\n        [\"motorized_wheelchair\"] = \"🦼\",\n        [\"scooter\"] = \"🛴\",\n        [\"bike\"] = \"🚲\",\n        [\"motor_scooter\"] = \"🛵\",\n        [\"motorbike\"] = \"🛵\",\n        [\"motorcycle\"] = \"🏍️\",\n        [\"racing_motorcycle\"] = \"🏍️\",\n        [\"auto_rickshaw\"] = \"🛺\",\n        [\"rotating_light\"] = \"🚨\",\n        [\"oncoming_police_car\"] = \"🚔\",\n        [\"oncoming_bus\"] = \"🚍\",\n        [\"oncoming_automobile\"] = \"🚘\",\n        [\"oncoming_taxi\"] = \"🚖\",\n        [\"aerial_tramway\"] = \"🚡\",\n        [\"mountain_cableway\"] = \"🚠\",\n        [\"suspension_railway\"] = \"🚟\",\n        [\"railway_car\"] = \"🚃\",\n        [\"train\"] = \"🚋\",\n        [\"mountain_railway\"] = \"🚞\",\n        [\"monorail\"] = \"🚝\",\n        [\"bullettrain_side\"] = \"🚄\",\n        [\"bullettrain_front\"] = \"🚅\",\n        [\"light_rail\"] = \"🚈\",\n        [\"steam_locomotive\"] = \"🚂\",\n        [\"train2\"] = \"🚆\",\n        [\"metro\"] = \"🚇\",\n        [\"tram\"] = \"🚊\",\n        [\"station\"] = \"🚉\",\n        [\"airplane\"] = \"✈️\",\n        [\"airplane_departure\"] = \"🛫\",\n        [\"airplane_arriving\"] = \"🛬\",\n        [\"airplane_small\"] = \"🛩️\",\n        [\"small_airplane\"] = \"🛩️\",\n        [\"seat\"] = \"💺\",\n        [\"satellite_orbital\"] = \"🛰️\",\n        [\"rocket\"] = \"🚀\",\n        [\"flying_saucer\"] = \"🛸\",\n        [\"helicopter\"] = \"🚁\",\n        [\"canoe\"] = \"🛶\",\n        [\"kayak\"] = \"🛶\",\n        [\"sailboat\"] = \"⛵\",\n        [\"speedboat\"] = \"🚤\",\n        [\"motorboat\"] = \"🛥️\",\n        [\"cruise_ship\"] = \"🛳️\",\n        [\"passenger_ship\"] = \"🛳️\",\n        [\"ferry\"] = \"⛴️\",\n        [\"ship\"] = \"🚢\",\n        [\"anchor\"] = \"⚓\",\n        [\"fuelpump\"] = \"⛽\",\n        [\"construction\"] = \"🚧\",\n        [\"vertical_traffic_light\"] = \"🚦\",\n        [\"traffic_light\"] = \"🚥\",\n        [\"busstop\"] = \"🚏\",\n        [\"map\"] = \"🗺️\",\n        [\"world_map\"] = \"🗺️\",\n        [\"moyai\"] = \"🗿\",\n        [\"statue_of_liberty\"] = \"🗽\",\n        [\"tokyo_tower\"] = \"🗼\",\n        [\"european_castle\"] = \"🏰\",\n        [\"japanese_castle\"] = \"🏯\",\n        [\"stadium\"] = \"🏟️\",\n        [\"ferris_wheel\"] = \"🎡\",\n        [\"roller_coaster\"] = \"🎢\",\n        [\"carousel_horse\"] = \"🎠\",\n        [\"fountain\"] = \"⛲\",\n        [\"beach_umbrella\"] = \"⛱️\",\n        [\"umbrella_on_ground\"] = \"⛱️\",\n        [\"beach\"] = \"🏖️\",\n        [\"beach_with_umbrella\"] = \"🏖️\",\n        [\"island\"] = \"🏝️\",\n        [\"desert_island\"] = \"🏝️\",\n        [\"desert\"] = \"🏜️\",\n        [\"volcano\"] = \"🌋\",\n        [\"mountain\"] = \"⛰️\",\n        [\"mountain_snow\"] = \"🏔️\",\n        [\"snow_capped_mountain\"] = \"🏔️\",\n        [\"mount_fuji\"] = \"🗻\",\n        [\"camping\"] = \"🏕️\",\n        [\"tent\"] = \"⛺\",\n        [\"house\"] = \"🏠\",\n        [\"house_with_garden\"] = \"🏡\",\n        [\"homes\"] = \"🏘️\",\n        [\"house_buildings\"] = \"🏘️\",\n        [\"house_abandoned\"] = \"🏚️\",\n        [\"derelict_house_building\"] = \"🏚️\",\n        [\"hut\"] = \"🛖\",\n        [\"construction_site\"] = \"🏗️\",\n        [\"building_construction\"] = \"🏗️\",\n        [\"factory\"] = \"🏭\",\n        [\"office\"] = \"🏢\",\n        [\"department_store\"] = \"🏬\",\n        [\"post_office\"] = \"🏣\",\n        [\"european_post_office\"] = \"🏤\",\n        [\"hospital\"] = \"🏥\",\n        [\"bank\"] = \"🏦\",\n        [\"hotel\"] = \"🏨\",\n        [\"convenience_store\"] = \"🏪\",\n        [\"school\"] = \"🏫\",\n        [\"love_hotel\"] = \"🏩\",\n        [\"wedding\"] = \"💒\",\n        [\"classical_building\"] = \"🏛️\",\n        [\"church\"] = \"⛪\",\n        [\"mosque\"] = \"🕌\",\n        [\"synagogue\"] = \"🕍\",\n        [\"hindu_temple\"] = \"🛕\",\n        [\"kaaba\"] = \"🕋\",\n        [\"shinto_shrine\"] = \"⛩️\",\n        [\"railway_track\"] = \"🛤️\",\n        [\"railroad_track\"] = \"🛤️\",\n        [\"motorway\"] = \"🛣️\",\n        [\"japan\"] = \"🗾\",\n        [\"rice_scene\"] = \"🎑\",\n        [\"park\"] = \"🏞️\",\n        [\"national_park\"] = \"🏞️\",\n        [\"sunrise\"] = \"🌅\",\n        [\"sunrise_over_mountains\"] = \"🌄\",\n        [\"stars\"] = \"🌠\",\n        [\"sparkler\"] = \"🎇\",\n        [\"fireworks\"] = \"🎆\",\n        [\"city_sunset\"] = \"🌇\",\n        [\"city_sunrise\"] = \"🌇\",\n        [\"city_dusk\"] = \"🌆\",\n        [\"cityscape\"] = \"🏙️\",\n        [\"night_with_stars\"] = \"🌃\",\n        [\"milky_way\"] = \"🌌\",\n        [\"bridge_at_night\"] = \"🌉\",\n        [\"foggy\"] = \"🌁\",\n        [\"watch\"] = \"⌚\",\n        [\"mobile_phone\"] = \"📱\",\n        [\"iphone\"] = \"📱\",\n        [\"calling\"] = \"📲\",\n        [\"computer\"] = \"💻\",\n        [\"keyboard\"] = \"⌨️\",\n        [\"desktop\"] = \"🖥️\",\n        [\"desktop_computer\"] = \"🖥️\",\n        [\"printer\"] = \"🖨️\",\n        [\"mouse_three_button\"] = \"🖱️\",\n        [\"three_button_mouse\"] = \"🖱️\",\n        [\"trackball\"] = \"🖲️\",\n        [\"joystick\"] = \"🕹️\",\n        [\"compression\"] = \"🗜️\",\n        [\"minidisc\"] = \"💽\",\n        [\"floppy_disk\"] = \"💾\",\n        [\"cd\"] = \"💿\",\n        [\"dvd\"] = \"📀\",\n        [\"vhs\"] = \"📼\",\n        [\"camera\"] = \"📷\",\n        [\"camera_with_flash\"] = \"📸\",\n        [\"video_camera\"] = \"📹\",\n        [\"movie_camera\"] = \"🎥\",\n        [\"projector\"] = \"📽️\",\n        [\"film_projector\"] = \"📽️\",\n        [\"film_frames\"] = \"🎞️\",\n        [\"telephone_receiver\"] = \"📞\",\n        [\"telephone\"] = \"☎️\",\n        [\"pager\"] = \"📟\",\n        [\"fax\"] = \"📠\",\n        [\"tv\"] = \"📺\",\n        [\"radio\"] = \"📻\",\n        [\"microphone2\"] = \"🎙️\",\n        [\"studio_microphone\"] = \"🎙️\",\n        [\"level_slider\"] = \"🎚️\",\n        [\"control_knobs\"] = \"🎛️\",\n        [\"compass\"] = \"🧭\",\n        [\"stopwatch\"] = \"⏱️\",\n        [\"timer\"] = \"⏲️\",\n        [\"timer_clock\"] = \"⏲️\",\n        [\"alarm_clock\"] = \"⏰\",\n        [\"clock\"] = \"🕰️\",\n        [\"mantlepiece_clock\"] = \"🕰️\",\n        [\"hourglass\"] = \"⌛\",\n        [\"hourglass_flowing_sand\"] = \"⏳\",\n        [\"satellite\"] = \"📡\",\n        [\"battery\"] = \"🔋\",\n        [\"electric_plug\"] = \"🔌\",\n        [\"bulb\"] = \"💡\",\n        [\"flashlight\"] = \"🔦\",\n        [\"candle\"] = \"🕯️\",\n        [\"diya_lamp\"] = \"🪔\",\n        [\"fire_extinguisher\"] = \"🧯\",\n        [\"oil\"] = \"🛢️\",\n        [\"oil_drum\"] = \"🛢️\",\n        [\"money_with_wings\"] = \"💸\",\n        [\"dollar\"] = \"💵\",\n        [\"yen\"] = \"💴\",\n        [\"euro\"] = \"💶\",\n        [\"pound\"] = \"💷\",\n        [\"coin\"] = \"🪙\",\n        [\"moneybag\"] = \"💰\",\n        [\"credit_card\"] = \"💳\",\n        [\"gem\"] = \"💎\",\n        [\"scales\"] = \"⚖️\",\n        [\"ladder\"] = \"🪜\",\n        [\"toolbox\"] = \"🧰\",\n        [\"screwdriver\"] = \"🪛\",\n        [\"wrench\"] = \"🔧\",\n        [\"hammer\"] = \"🔨\",\n        [\"hammer_pick\"] = \"⚒️\",\n        [\"hammer_and_pick\"] = \"⚒️\",\n        [\"tools\"] = \"🛠️\",\n        [\"hammer_and_wrench\"] = \"🛠️\",\n        [\"pick\"] = \"⛏️\",\n        [\"nut_and_bolt\"] = \"🔩\",\n        [\"gear\"] = \"⚙️\",\n        [\"bricks\"] = \"🧱\",\n        [\"chains\"] = \"⛓️\",\n        [\"hook\"] = \"🪝\",\n        [\"knot\"] = \"🪢\",\n        [\"magnet\"] = \"🧲\",\n        [\"gun\"] = \"🔫\",\n        [\"bomb\"] = \"💣\",\n        [\"firecracker\"] = \"🧨\",\n        [\"axe\"] = \"🪓\",\n        [\"carpentry_saw\"] = \"🪚\",\n        [\"knife\"] = \"🔪\",\n        [\"dagger\"] = \"🗡️\",\n        [\"dagger_knife\"] = \"🗡️\",\n        [\"crossed_swords\"] = \"⚔️\",\n        [\"shield\"] = \"🛡️\",\n        [\"smoking\"] = \"🚬\",\n        [\"coffin\"] = \"⚰️\",\n        [\"headstone\"] = \"🪦\",\n        [\"urn\"] = \"⚱️\",\n        [\"funeral_urn\"] = \"⚱️\",\n        [\"amphora\"] = \"🏺\",\n        [\"magic_wand\"] = \"🪄\",\n        [\"crystal_ball\"] = \"🔮\",\n        [\"prayer_beads\"] = \"📿\",\n        [\"nazar_amulet\"] = \"🧿\",\n        [\"barber\"] = \"💈\",\n        [\"alembic\"] = \"⚗️\",\n        [\"telescope\"] = \"🔭\",\n        [\"microscope\"] = \"🔬\",\n        [\"hole\"] = \"🕳️\",\n        [\"window\"] = \"🪟\",\n        [\"adhesive_bandage\"] = \"🩹\",\n        [\"stethoscope\"] = \"🩺\",\n        [\"pill\"] = \"💊\",\n        [\"syringe\"] = \"💉\",\n        [\"drop_of_blood\"] = \"🩸\",\n        [\"dna\"] = \"🧬\",\n        [\"microbe\"] = \"🦠\",\n        [\"petri_dish\"] = \"🧫\",\n        [\"test_tube\"] = \"🧪\",\n        [\"thermometer\"] = \"🌡️\",\n        [\"mouse_trap\"] = \"🪤\",\n        [\"broom\"] = \"🧹\",\n        [\"basket\"] = \"🧺\",\n        [\"sewing_needle\"] = \"🪡\",\n        [\"roll_of_paper\"] = \"🧻\",\n        [\"toilet\"] = \"🚽\",\n        [\"plunger\"] = \"🪠\",\n        [\"bucket\"] = \"🪣\",\n        [\"potable_water\"] = \"🚰\",\n        [\"shower\"] = \"🚿\",\n        [\"bathtub\"] = \"🛁\",\n        [\"bath\"] = \"🛀\",\n        [\"bath_tone1\"] = \"🛀🏻\",\n        [\"bath_tone2\"] = \"🛀🏼\",\n        [\"bath_tone3\"] = \"🛀🏽\",\n        [\"bath_tone4\"] = \"🛀🏾\",\n        [\"bath_tone5\"] = \"🛀🏿\",\n        [\"toothbrush\"] = \"🪥\",\n        [\"soap\"] = \"🧼\",\n        [\"razor\"] = \"🪒\",\n        [\"sponge\"] = \"🧽\",\n        [\"squeeze_bottle\"] = \"🧴\",\n        [\"bellhop\"] = \"🛎️\",\n        [\"bellhop_bell\"] = \"🛎️\",\n        [\"key\"] = \"🔑\",\n        [\"key2\"] = \"🗝️\",\n        [\"old_key\"] = \"🗝️\",\n        [\"door\"] = \"🚪\",\n        [\"chair\"] = \"🪑\",\n        [\"mirror\"] = \"🪞\",\n        [\"couch\"] = \"🛋️\",\n        [\"couch_and_lamp\"] = \"🛋️\",\n        [\"bed\"] = \"🛏️\",\n        [\"sleeping_accommodation\"] = \"🛌\",\n        [\"person_in_bed_tone1\"] = \"🛌🏻\",\n        [\"person_in_bed_light_skin_tone\"] = \"🛌🏻\",\n        [\"person_in_bed_tone2\"] = \"🛌🏼\",\n        [\"person_in_bed_medium_light_skin_tone\"] = \"🛌🏼\",\n        [\"person_in_bed_tone3\"] = \"🛌🏽\",\n        [\"person_in_bed_medium_skin_tone\"] = \"🛌🏽\",\n        [\"person_in_bed_tone4\"] = \"🛌🏾\",\n        [\"person_in_bed_medium_dark_skin_tone\"] = \"🛌🏾\",\n        [\"person_in_bed_tone5\"] = \"🛌🏿\",\n        [\"person_in_bed_dark_skin_tone\"] = \"🛌🏿\",\n        [\"teddy_bear\"] = \"🧸\",\n        [\"frame_photo\"] = \"🖼️\",\n        [\"frame_with_picture\"] = \"🖼️\",\n        [\"shopping_bags\"] = \"🛍️\",\n        [\"shopping_cart\"] = \"🛒\",\n        [\"shopping_trolley\"] = \"🛒\",\n        [\"gift\"] = \"🎁\",\n        [\"balloon\"] = \"🎈\",\n        [\"flags\"] = \"🎏\",\n        [\"ribbon\"] = \"🎀\",\n        [\"confetti_ball\"] = \"🎊\",\n        [\"tada\"] = \"🎉\",\n        [\"piñata\"] = \"🪅\",\n        [\"nesting_dolls\"] = \"🪆\",\n        [\"dolls\"] = \"🎎\",\n        [\"izakaya_lantern\"] = \"🏮\",\n        [\"wind_chime\"] = \"🎐\",\n        [\"red_envelope\"] = \"🧧\",\n        [\"envelope\"] = \"✉️\",\n        [\"envelope_with_arrow\"] = \"📩\",\n        [\"incoming_envelope\"] = \"📨\",\n        [\"e_mail\"] = \"📧\",\n        [\"email\"] = \"📧\",\n        [\"love_letter\"] = \"💌\",\n        [\"inbox_tray\"] = \"📥\",\n        [\"outbox_tray\"] = \"📤\",\n        [\"package\"] = \"📦\",\n        [\"label\"] = \"🏷️\",\n        [\"mailbox_closed\"] = \"📪\",\n        [\"mailbox\"] = \"📫\",\n        [\"mailbox_with_mail\"] = \"📬\",\n        [\"mailbox_with_no_mail\"] = \"📭\",\n        [\"postbox\"] = \"📮\",\n        [\"postal_horn\"] = \"📯\",\n        [\"placard\"] = \"🪧\",\n        [\"scroll\"] = \"📜\",\n        [\"page_with_curl\"] = \"📃\",\n        [\"page_facing_up\"] = \"📄\",\n        [\"bookmark_tabs\"] = \"📑\",\n        [\"receipt\"] = \"🧾\",\n        [\"bar_chart\"] = \"📊\",\n        [\"chart_with_upwards_trend\"] = \"📈\",\n        [\"chart_with_downwards_trend\"] = \"📉\",\n        [\"notepad_spiral\"] = \"🗒️\",\n        [\"spiral_note_pad\"] = \"🗒️\",\n        [\"calendar_spiral\"] = \"🗓️\",\n        [\"spiral_calendar_pad\"] = \"🗓️\",\n        [\"calendar\"] = \"📆\",\n        [\"date\"] = \"📅\",\n        [\"wastebasket\"] = \"🗑️\",\n        [\"card_index\"] = \"📇\",\n        [\"card_box\"] = \"🗃️\",\n        [\"card_file_box\"] = \"🗃️\",\n        [\"ballot_box\"] = \"🗳️\",\n        [\"ballot_box_with_ballot\"] = \"🗳️\",\n        [\"file_cabinet\"] = \"🗄️\",\n        [\"clipboard\"] = \"📋\",\n        [\"file_folder\"] = \"📁\",\n        [\"open_file_folder\"] = \"📂\",\n        [\"dividers\"] = \"🗂️\",\n        [\"card_index_dividers\"] = \"🗂️\",\n        [\"newspaper2\"] = \"🗞️\",\n        [\"rolled_up_newspaper\"] = \"🗞️\",\n        [\"newspaper\"] = \"📰\",\n        [\"notebook\"] = \"📓\",\n        [\"notebook_with_decorative_cover\"] = \"📔\",\n        [\"ledger\"] = \"📒\",\n        [\"closed_book\"] = \"📕\",\n        [\"green_book\"] = \"📗\",\n        [\"blue_book\"] = \"📘\",\n        [\"orange_book\"] = \"📙\",\n        [\"books\"] = \"📚\",\n        [\"book\"] = \"📖\",\n        [\"bookmark\"] = \"🔖\",\n        [\"safety_pin\"] = \"🧷\",\n        [\"link\"] = \"🔗\",\n        [\"paperclip\"] = \"📎\",\n        [\"paperclips\"] = \"🖇️\",\n        [\"linked_paperclips\"] = \"🖇️\",\n        [\"triangular_ruler\"] = \"📐\",\n        [\"straight_ruler\"] = \"📏\",\n        [\"abacus\"] = \"🧮\",\n        [\"pushpin\"] = \"📌\",\n        [\"round_pushpin\"] = \"📍\",\n        [\"scissors\"] = \"✂️\",\n        [\"pen_ballpoint\"] = \"🖊️\",\n        [\"lower_left_ballpoint_pen\"] = \"🖊️\",\n        [\"pen_fountain\"] = \"🖋️\",\n        [\"lower_left_fountain_pen\"] = \"🖋️\",\n        [\"black_nib\"] = \"✒️\",\n        [\"paintbrush\"] = \"🖌️\",\n        [\"lower_left_paintbrush\"] = \"🖌️\",\n        [\"crayon\"] = \"🖍️\",\n        [\"lower_left_crayon\"] = \"🖍️\",\n        [\"pencil\"] = \"📝\",\n        [\"memo\"] = \"📝\",\n        [\"pencil2\"] = \"✏️\",\n        [\"mag\"] = \"🔍\",\n        [\"mag_right\"] = \"🔎\",\n        [\"lock_with_ink_pen\"] = \"🔏\",\n        [\"closed_lock_with_key\"] = \"🔐\",\n        [\"lock\"] = \"🔒\",\n        [\"unlock\"] = \"🔓\",\n        [\"heart\"] = \"❤️\",\n        [\"orange_heart\"] = \"🧡\",\n        [\"yellow_heart\"] = \"💛\",\n        [\"green_heart\"] = \"💚\",\n        [\"blue_heart\"] = \"💙\",\n        [\"purple_heart\"] = \"💜\",\n        [\"black_heart\"] = \"🖤\",\n        [\"brown_heart\"] = \"🤎\",\n        [\"white_heart\"] = \"🤍\",\n        [\"broken_heart\"] = \"💔\",\n        [\"heart_exclamation\"] = \"❣️\",\n        [\"heavy_heart_exclamation_mark_ornament\"] = \"❣️\",\n        [\"two_hearts\"] = \"💕\",\n        [\"revolving_hearts\"] = \"💞\",\n        [\"heartbeat\"] = \"💓\",\n        [\"heartpulse\"] = \"💗\",\n        [\"sparkling_heart\"] = \"💖\",\n        [\"cupid\"] = \"💘\",\n        [\"gift_heart\"] = \"💝\",\n        [\"mending_heart\"] = \"❤️‍🩹\",\n        [\"heart_on_fire\"] = \"❤️‍🔥\",\n        [\"heart_decoration\"] = \"💟\",\n        [\"peace\"] = \"☮️\",\n        [\"peace_symbol\"] = \"☮️\",\n        [\"cross\"] = \"✝️\",\n        [\"latin_cross\"] = \"✝️\",\n        [\"star_and_crescent\"] = \"☪️\",\n        [\"om_symbol\"] = \"🕉️\",\n        [\"wheel_of_dharma\"] = \"☸️\",\n        [\"star_of_david\"] = \"✡️\",\n        [\"six_pointed_star\"] = \"🔯\",\n        [\"menorah\"] = \"🕎\",\n        [\"yin_yang\"] = \"☯️\",\n        [\"orthodox_cross\"] = \"☦️\",\n        [\"place_of_worship\"] = \"🛐\",\n        [\"worship_symbol\"] = \"🛐\",\n        [\"ophiuchus\"] = \"⛎\",\n        [\"aries\"] = \"♈\",\n        [\"taurus\"] = \"♉\",\n        [\"gemini\"] = \"♊\",\n        [\"cancer\"] = \"♋\",\n        [\"leo\"] = \"♌\",\n        [\"virgo\"] = \"♍\",\n        [\"libra\"] = \"♎\",\n        [\"scorpius\"] = \"♏\",\n        [\"sagittarius\"] = \"♐\",\n        [\"capricorn\"] = \"♑\",\n        [\"aquarius\"] = \"♒\",\n        [\"pisces\"] = \"♓\",\n        [\"id\"] = \"🆔\",\n        [\"atom\"] = \"⚛️\",\n        [\"atom_symbol\"] = \"⚛️\",\n        [\"accept\"] = \"🉑\",\n        [\"radioactive\"] = \"☢️\",\n        [\"radioactive_sign\"] = \"☢️\",\n        [\"biohazard\"] = \"☣️\",\n        [\"biohazard_sign\"] = \"☣️\",\n        [\"mobile_phone_off\"] = \"📴\",\n        [\"vibration_mode\"] = \"📳\",\n        [\"u6709\"] = \"🈶\",\n        [\"u7121\"] = \"🈚\",\n        [\"u7533\"] = \"🈸\",\n        [\"u55b6\"] = \"🈺\",\n        [\"u6708\"] = \"🈷️\",\n        [\"eight_pointed_black_star\"] = \"✴️\",\n        [\"vs\"] = \"🆚\",\n        [\"white_flower\"] = \"💮\",\n        [\"ideograph_advantage\"] = \"🉐\",\n        [\"secret\"] = \"㊙️\",\n        [\"congratulations\"] = \"㊗️\",\n        [\"u5408\"] = \"🈴\",\n        [\"u6e80\"] = \"🈵\",\n        [\"u5272\"] = \"🈹\",\n        [\"u7981\"] = \"🈲\",\n        [\"a\"] = \"🅰️\",\n        [\"b\"] = \"🅱️\",\n        [\"ab\"] = \"🆎\",\n        [\"cl\"] = \"🆑\",\n        [\"o2\"] = \"🅾️\",\n        [\"sos\"] = \"🆘\",\n        [\"x\"] = \"❌\",\n        [\"o\"] = \"⭕\",\n        [\"octagonal_sign\"] = \"🛑\",\n        [\"stop_sign\"] = \"🛑\",\n        [\"no_entry\"] = \"⛔\",\n        [\"name_badge\"] = \"📛\",\n        [\"no_entry_sign\"] = \"🚫\",\n        [\"100\"] = \"💯\",\n        [\"anger\"] = \"💢\",\n        [\"hotsprings\"] = \"♨️\",\n        [\"no_pedestrians\"] = \"🚷\",\n        [\"do_not_litter\"] = \"🚯\",\n        [\"no_bicycles\"] = \"🚳\",\n        [\"non_potable_water\"] = \"🚱\",\n        [\"underage\"] = \"🔞\",\n        [\"no_mobile_phones\"] = \"📵\",\n        [\"no_smoking\"] = \"🚭\",\n        [\"exclamation\"] = \"❗\",\n        [\"grey_exclamation\"] = \"❕\",\n        [\"question\"] = \"❓\",\n        [\"grey_question\"] = \"❔\",\n        [\"bangbang\"] = \"‼️\",\n        [\"interrobang\"] = \"⁉️\",\n        [\"low_brightness\"] = \"🔅\",\n        [\"high_brightness\"] = \"🔆\",\n        [\"part_alternation_mark\"] = \"〽️\",\n        [\"warning\"] = \"⚠️\",\n        [\"children_crossing\"] = \"🚸\",\n        [\"trident\"] = \"🔱\",\n        [\"fleur_de_lis\"] = \"⚜️\",\n        [\"beginner\"] = \"🔰\",\n        [\"recycle\"] = \"♻️\",\n        [\"white_check_mark\"] = \"✅\",\n        [\"u6307\"] = \"🈯\",\n        [\"chart\"] = \"💹\",\n        [\"sparkle\"] = \"❇️\",\n        [\"eight_spoked_asterisk\"] = \"✳️\",\n        [\"negative_squared_cross_mark\"] = \"❎\",\n        [\"globe_with_meridians\"] = \"🌐\",\n        [\"diamond_shape_with_a_dot_inside\"] = \"💠\",\n        [\"m\"] = \"Ⓜ️\",\n        [\"cyclone\"] = \"🌀\",\n        [\"zzz\"] = \"💤\",\n        [\"atm\"] = \"🏧\",\n        [\"wc\"] = \"🚾\",\n        [\"wheelchair\"] = \"♿\",\n        [\"parking\"] = \"🅿️\",\n        [\"u7a7a\"] = \"🈳\",\n        [\"sa\"] = \"🈂️\",\n        [\"passport_control\"] = \"🛂\",\n        [\"customs\"] = \"🛃\",\n        [\"baggage_claim\"] = \"🛄\",\n        [\"left_luggage\"] = \"🛅\",\n        [\"elevator\"] = \"🛗\",\n        [\"mens\"] = \"🚹\",\n        [\"womens\"] = \"🚺\",\n        [\"baby_symbol\"] = \"🚼\",\n        [\"restroom\"] = \"🚻\",\n        [\"put_litter_in_its_place\"] = \"🚮\",\n        [\"cinema\"] = \"🎦\",\n        [\"signal_strength\"] = \"📶\",\n        [\"koko\"] = \"🈁\",\n        [\"symbols\"] = \"🔣\",\n        [\"information_source\"] = \"ℹ️\",\n        [\"abc\"] = \"🔤\",\n        [\"abcd\"] = \"🔡\",\n        [\"capital_abcd\"] = \"🔠\",\n        [\"ng\"] = \"🆖\",\n        [\"ok\"] = \"🆗\",\n        [\"up\"] = \"🆙\",\n        [\"cool\"] = \"🆒\",\n        [\"new\"] = \"🆕\",\n        [\"free\"] = \"🆓\",\n        [\"zero\"] = \"0️⃣\",\n        [\"one\"] = \"1️⃣\",\n        [\"two\"] = \"2️⃣\",\n        [\"three\"] = \"3️⃣\",\n        [\"four\"] = \"4️⃣\",\n        [\"five\"] = \"5️⃣\",\n        [\"six\"] = \"6️⃣\",\n        [\"seven\"] = \"7️⃣\",\n        [\"eight\"] = \"8️⃣\",\n        [\"nine\"] = \"9️⃣\",\n        [\"keycap_ten\"] = \"🔟\",\n        [\"1234\"] = \"🔢\",\n        [\"hash\"] = \"#️⃣\",\n        [\"asterisk\"] = \"*️⃣\",\n        [\"keycap_asterisk\"] = \"*️⃣\",\n        [\"eject\"] = \"⏏️\",\n        [\"eject_symbol\"] = \"⏏️\",\n        [\"arrow_forward\"] = \"▶️\",\n        [\"pause_button\"] = \"⏸️\",\n        [\"double_vertical_bar\"] = \"⏸️\",\n        [\"play_pause\"] = \"⏯️\",\n        [\"stop_button\"] = \"⏹️\",\n        [\"record_button\"] = \"⏺️\",\n        [\"track_next\"] = \"⏭️\",\n        [\"next_track\"] = \"⏭️\",\n        [\"track_previous\"] = \"⏮️\",\n        [\"previous_track\"] = \"⏮️\",\n        [\"fast_forward\"] = \"⏩\",\n        [\"rewind\"] = \"⏪\",\n        [\"arrow_double_up\"] = \"⏫\",\n        [\"arrow_double_down\"] = \"⏬\",\n        [\"arrow_backward\"] = \"◀️\",\n        [\"arrow_up_small\"] = \"🔼\",\n        [\"arrow_down_small\"] = \"🔽\",\n        [\"arrow_right\"] = \"➡️\",\n        [\"arrow_left\"] = \"⬅️\",\n        [\"arrow_up\"] = \"⬆️\",\n        [\"arrow_down\"] = \"⬇️\",\n        [\"arrow_upper_right\"] = \"↗️\",\n        [\"arrow_lower_right\"] = \"↘️\",\n        [\"arrow_lower_left\"] = \"↙️\",\n        [\"arrow_upper_left\"] = \"↖️\",\n        [\"arrow_up_down\"] = \"↕️\",\n        [\"left_right_arrow\"] = \"↔️\",\n        [\"arrow_right_hook\"] = \"↪️\",\n        [\"leftwards_arrow_with_hook\"] = \"↩️\",\n        [\"arrow_heading_up\"] = \"⤴️\",\n        [\"arrow_heading_down\"] = \"⤵️\",\n        [\"twisted_rightwards_arrows\"] = \"🔀\",\n        [\"repeat\"] = \"🔁\",\n        [\"repeat_one\"] = \"🔂\",\n        [\"arrows_counterclockwise\"] = \"🔄\",\n        [\"arrows_clockwise\"] = \"🔃\",\n        [\"musical_note\"] = \"🎵\",\n        [\"notes\"] = \"🎶\",\n        [\"heavy_plus_sign\"] = \"➕\",\n        [\"heavy_minus_sign\"] = \"➖\",\n        [\"heavy_division_sign\"] = \"➗\",\n        [\"heavy_multiplication_x\"] = \"✖️\",\n        [\"infinity\"] = \"♾️\",\n        [\"heavy_dollar_sign\"] = \"💲\",\n        [\"currency_exchange\"] = \"💱\",\n        [\"tm\"] = \"™️\",\n        [\"copyright\"] = \"©️\",\n        [\"registered\"] = \"®️\",\n        [\"wavy_dash\"] = \"〰️\",\n        [\"curly_loop\"] = \"➰\",\n        [\"loop\"] = \"➿\",\n        [\"end\"] = \"🔚\",\n        [\"back\"] = \"🔙\",\n        [\"on\"] = \"🔛\",\n        [\"top\"] = \"🔝\",\n        [\"soon\"] = \"🔜\",\n        [\"heavy_check_mark\"] = \"✔️\",\n        [\"ballot_box_with_check\"] = \"☑️\",\n        [\"radio_button\"] = \"🔘\",\n        [\"white_circle\"] = \"⚪\",\n        [\"black_circle\"] = \"⚫\",\n        [\"red_circle\"] = \"🔴\",\n        [\"blue_circle\"] = \"🔵\",\n        [\"brown_circle\"] = \"🟤\",\n        [\"purple_circle\"] = \"🟣\",\n        [\"green_circle\"] = \"🟢\",\n        [\"yellow_circle\"] = \"🟡\",\n        [\"orange_circle\"] = \"🟠\",\n        [\"small_red_triangle\"] = \"🔺\",\n        [\"small_red_triangle_down\"] = \"🔻\",\n        [\"small_orange_diamond\"] = \"🔸\",\n        [\"small_blue_diamond\"] = \"🔹\",\n        [\"large_orange_diamond\"] = \"🔶\",\n        [\"large_blue_diamond\"] = \"🔷\",\n        [\"white_square_button\"] = \"🔳\",\n        [\"black_square_button\"] = \"🔲\",\n        [\"black_small_square\"] = \"▪️\",\n        [\"white_small_square\"] = \"▫️\",\n        [\"black_medium_small_square\"] = \"◾\",\n        [\"white_medium_small_square\"] = \"◽\",\n        [\"black_medium_square\"] = \"◼️\",\n        [\"white_medium_square\"] = \"◻️\",\n        [\"black_large_square\"] = \"⬛\",\n        [\"white_large_square\"] = \"⬜\",\n        [\"orange_square\"] = \"🟧\",\n        [\"blue_square\"] = \"🟦\",\n        [\"red_square\"] = \"🟥\",\n        [\"brown_square\"] = \"🟫\",\n        [\"purple_square\"] = \"🟪\",\n        [\"green_square\"] = \"🟩\",\n        [\"yellow_square\"] = \"🟨\",\n        [\"speaker\"] = \"🔈\",\n        [\"mute\"] = \"🔇\",\n        [\"sound\"] = \"🔉\",\n        [\"loud_sound\"] = \"🔊\",\n        [\"bell\"] = \"🔔\",\n        [\"no_bell\"] = \"🔕\",\n        [\"mega\"] = \"📣\",\n        [\"loudspeaker\"] = \"📢\",\n        [\"speech_left\"] = \"🗨️\",\n        [\"left_speech_bubble\"] = \"🗨️\",\n        [\"eye_in_speech_bubble\"] = \"👁‍🗨\",\n        [\"speech_balloon\"] = \"💬\",\n        [\"thought_balloon\"] = \"💭\",\n        [\"anger_right\"] = \"🗯️\",\n        [\"right_anger_bubble\"] = \"🗯️\",\n        [\"spades\"] = \"♠️\",\n        [\"clubs\"] = \"♣️\",\n        [\"hearts\"] = \"♥️\",\n        [\"diamonds\"] = \"♦️\",\n        [\"black_joker\"] = \"🃏\",\n        [\"flower_playing_cards\"] = \"🎴\",\n        [\"mahjong\"] = \"🀄\",\n        [\"clock1\"] = \"🕐\",\n        [\"clock2\"] = \"🕑\",\n        [\"clock3\"] = \"🕒\",\n        [\"clock4\"] = \"🕓\",\n        [\"clock5\"] = \"🕔\",\n        [\"clock6\"] = \"🕕\",\n        [\"clock7\"] = \"🕖\",\n        [\"clock8\"] = \"🕗\",\n        [\"clock9\"] = \"🕘\",\n        [\"clock10\"] = \"🕙\",\n        [\"clock11\"] = \"🕚\",\n        [\"clock12\"] = \"🕛\",\n        [\"clock130\"] = \"🕜\",\n        [\"clock230\"] = \"🕝\",\n        [\"clock330\"] = \"🕞\",\n        [\"clock430\"] = \"🕟\",\n        [\"clock530\"] = \"🕠\",\n        [\"clock630\"] = \"🕡\",\n        [\"clock730\"] = \"🕢\",\n        [\"clock830\"] = \"🕣\",\n        [\"clock930\"] = \"🕤\",\n        [\"clock1030\"] = \"🕥\",\n        [\"clock1130\"] = \"🕦\",\n        [\"clock1230\"] = \"🕧\",\n        [\"female_sign\"] = \"♀️\",\n        [\"male_sign\"] = \"♂️\",\n        [\"transgender_symbol\"] = \"⚧\",\n        [\"medical_symbol\"] = \"⚕️\",\n        [\"regional_indicator_z\"] = \"🇿\",\n        [\"regional_indicator_y\"] = \"🇾\",\n        [\"regional_indicator_x\"] = \"🇽\",\n        [\"regional_indicator_w\"] = \"🇼\",\n        [\"regional_indicator_v\"] = \"🇻\",\n        [\"regional_indicator_u\"] = \"🇺\",\n        [\"regional_indicator_t\"] = \"🇹\",\n        [\"regional_indicator_s\"] = \"🇸\",\n        [\"regional_indicator_r\"] = \"🇷\",\n        [\"regional_indicator_q\"] = \"🇶\",\n        [\"regional_indicator_p\"] = \"🇵\",\n        [\"regional_indicator_o\"] = \"🇴\",\n        [\"regional_indicator_n\"] = \"🇳\",\n        [\"regional_indicator_m\"] = \"🇲\",\n        [\"regional_indicator_l\"] = \"🇱\",\n        [\"regional_indicator_k\"] = \"🇰\",\n        [\"regional_indicator_j\"] = \"🇯\",\n        [\"regional_indicator_i\"] = \"🇮\",\n        [\"regional_indicator_h\"] = \"🇭\",\n        [\"regional_indicator_g\"] = \"🇬\",\n        [\"regional_indicator_f\"] = \"🇫\",\n        [\"regional_indicator_e\"] = \"🇪\",\n        [\"regional_indicator_d\"] = \"🇩\",\n        [\"regional_indicator_c\"] = \"🇨\",\n        [\"regional_indicator_b\"] = \"🇧\",\n        [\"regional_indicator_a\"] = \"🇦\",\n        [\"flag_white\"] = \"🏳️\",\n        [\"flag_black\"] = \"🏴\",\n        [\"checkered_flag\"] = \"🏁\",\n        [\"triangular_flag_on_post\"] = \"🚩\",\n        [\"rainbow_flag\"] = \"🏳️‍🌈\",\n        [\"gay_pride_flag\"] = \"🏳️‍🌈\",\n        [\"transgender_flag\"] = \"🏳️‍⚧️\",\n        [\"pirate_flag\"] = \"🏴‍☠️\",\n        [\"flag_af\"] = \"🇦🇫\",\n        [\"flag_ax\"] = \"🇦🇽\",\n        [\"flag_al\"] = \"🇦🇱\",\n        [\"flag_dz\"] = \"🇩🇿\",\n        [\"flag_as\"] = \"🇦🇸\",\n        [\"flag_ad\"] = \"🇦🇩\",\n        [\"flag_ao\"] = \"🇦🇴\",\n        [\"flag_ai\"] = \"🇦🇮\",\n        [\"flag_aq\"] = \"🇦🇶\",\n        [\"flag_ag\"] = \"🇦🇬\",\n        [\"flag_ar\"] = \"🇦🇷\",\n        [\"flag_am\"] = \"🇦🇲\",\n        [\"flag_aw\"] = \"🇦🇼\",\n        [\"flag_au\"] = \"🇦🇺\",\n        [\"flag_at\"] = \"🇦🇹\",\n        [\"flag_az\"] = \"🇦🇿\",\n        [\"flag_bs\"] = \"🇧🇸\",\n        [\"flag_bh\"] = \"🇧🇭\",\n        [\"flag_bd\"] = \"🇧🇩\",\n        [\"flag_bb\"] = \"🇧🇧\",\n        [\"flag_by\"] = \"🇧🇾\",\n        [\"flag_be\"] = \"🇧🇪\",\n        [\"flag_bz\"] = \"🇧🇿\",\n        [\"flag_bj\"] = \"🇧🇯\",\n        [\"flag_bm\"] = \"🇧🇲\",\n        [\"flag_bt\"] = \"🇧🇹\",\n        [\"flag_bo\"] = \"🇧🇴\",\n        [\"flag_ba\"] = \"🇧🇦\",\n        [\"flag_bw\"] = \"🇧🇼\",\n        [\"flag_br\"] = \"🇧🇷\",\n        [\"flag_io\"] = \"🇮🇴\",\n        [\"flag_vg\"] = \"🇻🇬\",\n        [\"flag_bn\"] = \"🇧🇳\",\n        [\"flag_bg\"] = \"🇧🇬\",\n        [\"flag_bf\"] = \"🇧🇫\",\n        [\"flag_bi\"] = \"🇧🇮\",\n        [\"flag_kh\"] = \"🇰🇭\",\n        [\"flag_cm\"] = \"🇨🇲\",\n        [\"flag_ca\"] = \"🇨🇦\",\n        [\"flag_ic\"] = \"🇮🇨\",\n        [\"flag_cv\"] = \"🇨🇻\",\n        [\"flag_bq\"] = \"🇧🇶\",\n        [\"flag_ky\"] = \"🇰🇾\",\n        [\"flag_cf\"] = \"🇨🇫\",\n        [\"flag_td\"] = \"🇹🇩\",\n        [\"flag_cl\"] = \"🇨🇱\",\n        [\"flag_cn\"] = \"🇨🇳\",\n        [\"flag_cx\"] = \"🇨🇽\",\n        [\"flag_cc\"] = \"🇨🇨\",\n        [\"flag_co\"] = \"🇨🇴\",\n        [\"flag_km\"] = \"🇰🇲\",\n        [\"flag_cg\"] = \"🇨🇬\",\n        [\"flag_cd\"] = \"🇨🇩\",\n        [\"flag_ck\"] = \"🇨🇰\",\n        [\"flag_cr\"] = \"🇨🇷\",\n        [\"flag_ci\"] = \"🇨🇮\",\n        [\"flag_hr\"] = \"🇭🇷\",\n        [\"flag_cu\"] = \"🇨🇺\",\n        [\"flag_cw\"] = \"🇨🇼\",\n        [\"flag_cy\"] = \"🇨🇾\",\n        [\"flag_cz\"] = \"🇨🇿\",\n        [\"flag_dk\"] = \"🇩🇰\",\n        [\"flag_dj\"] = \"🇩🇯\",\n        [\"flag_dm\"] = \"🇩🇲\",\n        [\"flag_do\"] = \"🇩🇴\",\n        [\"flag_ec\"] = \"🇪🇨\",\n        [\"flag_eg\"] = \"🇪🇬\",\n        [\"flag_sv\"] = \"🇸🇻\",\n        [\"flag_gq\"] = \"🇬🇶\",\n        [\"flag_er\"] = \"🇪🇷\",\n        [\"flag_ee\"] = \"🇪🇪\",\n        [\"flag_et\"] = \"🇪🇹\",\n        [\"flag_eu\"] = \"🇪🇺\",\n        [\"flag_fk\"] = \"🇫🇰\",\n        [\"flag_fo\"] = \"🇫🇴\",\n        [\"flag_fj\"] = \"🇫🇯\",\n        [\"flag_fi\"] = \"🇫🇮\",\n        [\"flag_fr\"] = \"🇫🇷\",\n        [\"flag_gf\"] = \"🇬🇫\",\n        [\"flag_pf\"] = \"🇵🇫\",\n        [\"flag_tf\"] = \"🇹🇫\",\n        [\"flag_ga\"] = \"🇬🇦\",\n        [\"flag_gm\"] = \"🇬🇲\",\n        [\"flag_ge\"] = \"🇬🇪\",\n        [\"flag_de\"] = \"🇩🇪\",\n        [\"flag_gh\"] = \"🇬🇭\",\n        [\"flag_gi\"] = \"🇬🇮\",\n        [\"flag_gr\"] = \"🇬🇷\",\n        [\"flag_gl\"] = \"🇬🇱\",\n        [\"flag_gd\"] = \"🇬🇩\",\n        [\"flag_gp\"] = \"🇬🇵\",\n        [\"flag_gu\"] = \"🇬🇺\",\n        [\"flag_gt\"] = \"🇬🇹\",\n        [\"flag_gg\"] = \"🇬🇬\",\n        [\"flag_gn\"] = \"🇬🇳\",\n        [\"flag_gw\"] = \"🇬🇼\",\n        [\"flag_gy\"] = \"🇬🇾\",\n        [\"flag_ht\"] = \"🇭🇹\",\n        [\"flag_hn\"] = \"🇭🇳\",\n        [\"flag_hk\"] = \"🇭🇰\",\n        [\"flag_hu\"] = \"🇭🇺\",\n        [\"flag_is\"] = \"🇮🇸\",\n        [\"flag_in\"] = \"🇮🇳\",\n        [\"flag_id\"] = \"🇮🇩\",\n        [\"flag_ir\"] = \"🇮🇷\",\n        [\"flag_iq\"] = \"🇮🇶\",\n        [\"flag_ie\"] = \"🇮🇪\",\n        [\"flag_im\"] = \"🇮🇲\",\n        [\"flag_il\"] = \"🇮🇱\",\n        [\"flag_it\"] = \"🇮🇹\",\n        [\"flag_jm\"] = \"🇯🇲\",\n        [\"flag_jp\"] = \"🇯🇵\",\n        [\"crossed_flags\"] = \"🎌\",\n        [\"flag_je\"] = \"🇯🇪\",\n        [\"flag_jo\"] = \"🇯🇴\",\n        [\"flag_kz\"] = \"🇰🇿\",\n        [\"flag_ke\"] = \"🇰🇪\",\n        [\"flag_ki\"] = \"🇰🇮\",\n        [\"flag_xk\"] = \"🇽🇰\",\n        [\"flag_kw\"] = \"🇰🇼\",\n        [\"flag_kg\"] = \"🇰🇬\",\n        [\"flag_la\"] = \"🇱🇦\",\n        [\"flag_lv\"] = \"🇱🇻\",\n        [\"flag_lb\"] = \"🇱🇧\",\n        [\"flag_ls\"] = \"🇱🇸\",\n        [\"flag_lr\"] = \"🇱🇷\",\n        [\"flag_ly\"] = \"🇱🇾\",\n        [\"flag_li\"] = \"🇱🇮\",\n        [\"flag_lt\"] = \"🇱🇹\",\n        [\"flag_lu\"] = \"🇱🇺\",\n        [\"flag_mo\"] = \"🇲🇴\",\n        [\"flag_mk\"] = \"🇲🇰\",\n        [\"flag_mg\"] = \"🇲🇬\",\n        [\"flag_mw\"] = \"🇲🇼\",\n        [\"flag_my\"] = \"🇲🇾\",\n        [\"flag_mv\"] = \"🇲🇻\",\n        [\"flag_ml\"] = \"🇲🇱\",\n        [\"flag_mt\"] = \"🇲🇹\",\n        [\"flag_mh\"] = \"🇲🇭\",\n        [\"flag_mq\"] = \"🇲🇶\",\n        [\"flag_mr\"] = \"🇲🇷\",\n        [\"flag_mu\"] = \"🇲🇺\",\n        [\"flag_yt\"] = \"🇾🇹\",\n        [\"flag_mx\"] = \"🇲🇽\",\n        [\"flag_fm\"] = \"🇫🇲\",\n        [\"flag_md\"] = \"🇲🇩\",\n        [\"flag_mc\"] = \"🇲🇨\",\n        [\"flag_mn\"] = \"🇲🇳\",\n        [\"flag_me\"] = \"🇲🇪\",\n        [\"flag_ms\"] = \"🇲🇸\",\n        [\"flag_ma\"] = \"🇲🇦\",\n        [\"flag_mz\"] = \"🇲🇿\",\n        [\"flag_mm\"] = \"🇲🇲\",\n        [\"flag_na\"] = \"🇳🇦\",\n        [\"flag_nr\"] = \"🇳🇷\",\n        [\"flag_np\"] = \"🇳🇵\",\n        [\"flag_nl\"] = \"🇳🇱\",\n        [\"flag_nc\"] = \"🇳🇨\",\n        [\"flag_nz\"] = \"🇳🇿\",\n        [\"flag_ni\"] = \"🇳🇮\",\n        [\"flag_ne\"] = \"🇳🇪\",\n        [\"flag_ng\"] = \"🇳🇬\",\n        [\"flag_nu\"] = \"🇳🇺\",\n        [\"flag_nf\"] = \"🇳🇫\",\n        [\"flag_kp\"] = \"🇰🇵\",\n        [\"flag_mp\"] = \"🇲🇵\",\n        [\"flag_no\"] = \"🇳🇴\",\n        [\"flag_om\"] = \"🇴🇲\",\n        [\"flag_pk\"] = \"🇵🇰\",\n        [\"flag_pw\"] = \"🇵🇼\",\n        [\"flag_ps\"] = \"🇵🇸\",\n        [\"flag_pa\"] = \"🇵🇦\",\n        [\"flag_pg\"] = \"🇵🇬\",\n        [\"flag_py\"] = \"🇵🇾\",\n        [\"flag_pe\"] = \"🇵🇪\",\n        [\"flag_ph\"] = \"🇵🇭\",\n        [\"flag_pn\"] = \"🇵🇳\",\n        [\"flag_pl\"] = \"🇵🇱\",\n        [\"flag_pt\"] = \"🇵🇹\",\n        [\"flag_pr\"] = \"🇵🇷\",\n        [\"flag_qa\"] = \"🇶🇦\",\n        [\"flag_re\"] = \"🇷🇪\",\n        [\"flag_ro\"] = \"🇷🇴\",\n        [\"flag_ru\"] = \"🇷🇺\",\n        [\"flag_rw\"] = \"🇷🇼\",\n        [\"flag_ws\"] = \"🇼🇸\",\n        [\"flag_sm\"] = \"🇸🇲\",\n        [\"flag_st\"] = \"🇸🇹\",\n        [\"flag_sa\"] = \"🇸🇦\",\n        [\"flag_sn\"] = \"🇸🇳\",\n        [\"flag_rs\"] = \"🇷🇸\",\n        [\"flag_sc\"] = \"🇸🇨\",\n        [\"flag_sl\"] = \"🇸🇱\",\n        [\"flag_sg\"] = \"🇸🇬\",\n        [\"flag_sx\"] = \"🇸🇽\",\n        [\"flag_sk\"] = \"🇸🇰\",\n        [\"flag_si\"] = \"🇸🇮\",\n        [\"flag_gs\"] = \"🇬🇸\",\n        [\"flag_sb\"] = \"🇸🇧\",\n        [\"flag_so\"] = \"🇸🇴\",\n        [\"flag_za\"] = \"🇿🇦\",\n        [\"flag_kr\"] = \"🇰🇷\",\n        [\"flag_ss\"] = \"🇸🇸\",\n        [\"flag_es\"] = \"🇪🇸\",\n        [\"flag_lk\"] = \"🇱🇰\",\n        [\"flag_bl\"] = \"🇧🇱\",\n        [\"flag_sh\"] = \"🇸🇭\",\n        [\"flag_kn\"] = \"🇰🇳\",\n        [\"flag_lc\"] = \"🇱🇨\",\n        [\"flag_pm\"] = \"🇵🇲\",\n        [\"flag_vc\"] = \"🇻🇨\",\n        [\"flag_sd\"] = \"🇸🇩\",\n        [\"flag_sr\"] = \"🇸🇷\",\n        [\"flag_sz\"] = \"🇸🇿\",\n        [\"flag_se\"] = \"🇸🇪\",\n        [\"flag_ch\"] = \"🇨🇭\",\n        [\"flag_sy\"] = \"🇸🇾\",\n        [\"flag_tw\"] = \"🇹🇼\",\n        [\"flag_tj\"] = \"🇹🇯\",\n        [\"flag_tz\"] = \"🇹🇿\",\n        [\"flag_th\"] = \"🇹🇭\",\n        [\"flag_tl\"] = \"🇹🇱\",\n        [\"flag_tg\"] = \"🇹🇬\",\n        [\"flag_tk\"] = \"🇹🇰\",\n        [\"flag_to\"] = \"🇹🇴\",\n        [\"flag_tt\"] = \"🇹🇹\",\n        [\"flag_tn\"] = \"🇹🇳\",\n        [\"flag_tr\"] = \"🇹🇷\",\n        [\"flag_tm\"] = \"🇹🇲\",\n        [\"flag_tc\"] = \"🇹🇨\",\n        [\"flag_vi\"] = \"🇻🇮\",\n        [\"flag_tv\"] = \"🇹🇻\",\n        [\"flag_ug\"] = \"🇺🇬\",\n        [\"flag_ua\"] = \"🇺🇦\",\n        [\"flag_ae\"] = \"🇦🇪\",\n        [\"flag_gb\"] = \"🇬🇧\",\n        [\"england\"] = \"🏴󠁧󠁢󠁥󠁮󠁧󠁿\",\n        [\"scotland\"] = \"🏴󠁧󠁢󠁳󠁣󠁴󠁿\",\n        [\"wales\"] = \"🏴󠁧󠁢󠁷󠁬󠁳󠁿\",\n        [\"flag_us\"] = \"🇺🇸\",\n        [\"flag_uy\"] = \"🇺🇾\",\n        [\"flag_uz\"] = \"🇺🇿\",\n        [\"flag_vu\"] = \"🇻🇺\",\n        [\"flag_va\"] = \"🇻🇦\",\n        [\"flag_ve\"] = \"🇻🇪\",\n        [\"flag_vn\"] = \"🇻🇳\",\n        [\"flag_wf\"] = \"🇼🇫\",\n        [\"flag_eh\"] = \"🇪🇭\",\n        [\"flag_ye\"] = \"🇾🇪\",\n        [\"flag_zm\"] = \"🇿🇲\",\n        [\"flag_zw\"] = \"🇿🇼\",\n        [\"flag_ac\"] = \"🇦🇨\",\n        [\"flag_bv\"] = \"🇧🇻\",\n        [\"flag_cp\"] = \"🇨🇵\",\n        [\"flag_ea\"] = \"🇪🇦\",\n        [\"flag_dg\"] = \"🇩🇬\",\n        [\"flag_hm\"] = \"🇭🇲\",\n        [\"flag_mf\"] = \"🇲🇫\",\n        [\"flag_sj\"] = \"🇸🇯\",\n        [\"flag_ta\"] = \"🇹🇦\",\n        [\"flag_um\"] = \"🇺🇲\",\n        [\"united_nations\"] = \"🇺🇳\",\n    };\n\n    public static IReadOnlyCollection<string> GetAllNames() => _toCodes.Keys;\n\n    public static string? TryGetCode(string name) => _toCodes.GetValueOrDefault(name);\n\n    public static string? TryGetName(string code) => _fromCodes.GetValueOrDefault(code);\n}\n"
  },
  {
    "path": "DiscordChatExporter.Core/Discord/Data/Guild.cs",
    "content": "﻿using System.Text.Json;\nusing DiscordChatExporter.Core.Discord.Data.Common;\nusing DiscordChatExporter.Core.Utils.Extensions;\nusing JsonExtensions.Reading;\n\nnamespace DiscordChatExporter.Core.Discord.Data;\n\n// https://discord.com/developers/docs/resources/guild#guild-object\npublic partial record Guild(Snowflake Id, string Name, string IconUrl) : IHasId\n{\n    public bool IsDirect { get; } = Id == Snowflake.Zero;\n}\n\npublic partial record Guild\n{\n    // Direct messages are encapsulated within a special pseudo-guild for consistency\n    public static Guild DirectMessages { get; } =\n        new(Snowflake.Zero, \"Direct Messages\", ImageCdn.GetFallbackUserAvatarUrl());\n\n    public static Guild Parse(JsonElement json)\n    {\n        var id = json.GetProperty(\"id\").GetNonWhiteSpaceString().Pipe(Snowflake.Parse);\n        var name = json.GetProperty(\"name\").GetNonNullString();\n\n        var iconUrl =\n            json.GetPropertyOrNull(\"icon\")\n                ?.GetNonWhiteSpaceStringOrNull()\n                ?.Pipe(h => ImageCdn.GetGuildIconUrl(id, h))\n            ?? ImageCdn.GetFallbackUserAvatarUrl();\n\n        return new Guild(id, name, iconUrl);\n    }\n}\n"
  },
  {
    "path": "DiscordChatExporter.Core/Discord/Data/Interaction.cs",
    "content": "﻿using System.Text.Json;\nusing DiscordChatExporter.Core.Utils.Extensions;\nusing JsonExtensions.Reading;\n\nnamespace DiscordChatExporter.Core.Discord.Data;\n\n// https://discord.com/developers/docs/interactions/receiving-and-responding#message-interaction-object\npublic record Interaction(Snowflake Id, string Name, User User)\n{\n    public static Interaction Parse(JsonElement json)\n    {\n        var id = json.GetProperty(\"id\").GetNonWhiteSpaceString().Pipe(Snowflake.Parse);\n        var name = json.GetProperty(\"name\").GetNonNullString(); // may be empty, but not null\n        var user = json.GetProperty(\"user\").Pipe(User.Parse);\n\n        return new Interaction(id, name, user);\n    }\n}\n"
  },
  {
    "path": "DiscordChatExporter.Core/Discord/Data/Invite.cs",
    "content": "﻿using System.Text.Json;\nusing System.Text.RegularExpressions;\nusing DiscordChatExporter.Core.Utils.Extensions;\nusing JsonExtensions.Reading;\n\nnamespace DiscordChatExporter.Core.Discord.Data;\n\n// https://discord.com/developers/docs/resources/invite#invite-object\npublic record Invite(string Code, Guild Guild, Channel? Channel)\n{\n    public static string? TryGetCodeFromUrl(string url) =>\n        Regex.Match(url, @\"^https?://discord\\.gg/(\\w+)/?$\").Groups[1].Value.NullIfWhiteSpace();\n\n    public static Invite Parse(JsonElement json)\n    {\n        var code = json.GetProperty(\"code\").GetNonWhiteSpaceString();\n        var guild = json.GetPropertyOrNull(\"guild\")?.Pipe(Guild.Parse) ?? Guild.DirectMessages;\n        var channel = json.GetPropertyOrNull(\"channel\")?.Pipe(c => Channel.Parse(c));\n\n        return new Invite(code, guild, channel);\n    }\n}\n"
  },
  {
    "path": "DiscordChatExporter.Core/Discord/Data/Member.cs",
    "content": "﻿using System.Collections.Generic;\nusing System.Linq;\nusing System.Text.Json;\nusing DiscordChatExporter.Core.Discord.Data.Common;\nusing DiscordChatExporter.Core.Utils.Extensions;\nusing JsonExtensions.Reading;\n\nnamespace DiscordChatExporter.Core.Discord.Data;\n\n// https://discord.com/developers/docs/resources/guild#guild-member-object\npublic partial record Member(\n    User User,\n    string? DisplayName,\n    string? AvatarUrl,\n    IReadOnlyList<Snowflake> RoleIds\n) : IHasId\n{\n    public Snowflake Id { get; } = User.Id;\n}\n\npublic partial record Member\n{\n    public static Member CreateFallback(User user) => new(user, null, null, []);\n\n    public static Member Parse(JsonElement json, Snowflake? guildId = null)\n    {\n        var user = json.GetProperty(\"user\").Pipe(User.Parse);\n        var displayName = json.GetPropertyOrNull(\"nick\")?.GetNonWhiteSpaceStringOrNull();\n\n        var roleIds =\n            json.GetPropertyOrNull(\"roles\")\n                ?.EnumerateArray()\n                .Select(j => j.GetNonWhiteSpaceString())\n                .Select(Snowflake.Parse)\n                .ToArray()\n            ?? [];\n\n        var avatarUrl = guildId is not null\n            ? json.GetPropertyOrNull(\"avatar\")\n                ?.GetNonWhiteSpaceStringOrNull()\n                ?.Pipe(h => ImageCdn.GetMemberAvatarUrl(guildId.Value, user.Id, h))\n            : null;\n\n        return new Member(user, displayName, avatarUrl, roleIds);\n    }\n}\n"
  },
  {
    "path": "DiscordChatExporter.Core/Discord/Data/Message.cs",
    "content": "﻿using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Text.Json;\nusing DiscordChatExporter.Core.Discord.Data.Common;\nusing DiscordChatExporter.Core.Discord.Data.Embeds;\nusing DiscordChatExporter.Core.Utils.Extensions;\nusing JsonExtensions.Reading;\n\nnamespace DiscordChatExporter.Core.Discord.Data;\n\n// https://discord.com/developers/docs/resources/channel#message-object\npublic partial record Message(\n    Snowflake Id,\n    MessageKind Kind,\n    MessageFlags Flags,\n    User Author,\n    DateTimeOffset Timestamp,\n    DateTimeOffset? EditedTimestamp,\n    DateTimeOffset? CallEndedTimestamp,\n    bool IsPinned,\n    string Content,\n    IReadOnlyList<Attachment> Attachments,\n    IReadOnlyList<Embed> Embeds,\n    IReadOnlyList<Sticker> Stickers,\n    IReadOnlyList<Reaction> Reactions,\n    IReadOnlyList<User> MentionedUsers,\n    MessageReference? Reference,\n    Message? ReferencedMessage,\n    MessageSnapshot? ForwardedMessage,\n    Interaction? Interaction\n) : IHasId\n{\n    public bool IsEmpty { get; } =\n        string.IsNullOrWhiteSpace(Content)\n        && !Attachments.Any()\n        && !Embeds.Any()\n        && !Stickers.Any();\n\n    public bool IsSystemNotification { get; } =\n        Kind is >= MessageKind.RecipientAdd and <= MessageKind.ThreadCreated;\n\n    public bool IsReply { get; } = Kind == MessageKind.Reply;\n\n    // App interactions are rendered as replies in the Discord client, but they are not actually replies\n    public bool IsReplyLike => IsReply || Interaction is not null;\n\n    public bool IsForwarded { get; } = Reference?.Kind == MessageReferenceKind.Forward;\n\n    public IEnumerable<User> GetReferencedUsers()\n    {\n        yield return Author;\n\n        foreach (var user in MentionedUsers)\n            yield return user;\n\n        if (ReferencedMessage is not null)\n            yield return ReferencedMessage.Author;\n\n        if (Interaction is not null)\n            yield return Interaction.User;\n    }\n}\n\npublic partial record Message\n{\n    private static IReadOnlyList<Embed> NormalizeEmbeds(IReadOnlyList<Embed> embeds)\n    {\n        if (embeds.Count <= 1)\n            return embeds;\n\n        // Discord API doesn't support embeds with multiple images, even though Discord client does.\n        // To work around this, it seems that the API returns multiple consecutive embeds with different images,\n        // which are then merged together on the client. We need to replicate the same behavior ourselves.\n        // Currently, only known case where this workaround is required is Twitter embeds.\n        // https://github.com/Tyrrrz/DiscordChatExporter/issues/695\n\n        var normalizedEmbeds = new List<Embed>();\n\n        for (var i = 0; i < embeds.Count; i++)\n        {\n            var embed = embeds[i];\n\n            if (embed.Url?.Contains(\"://twitter.com/\", StringComparison.OrdinalIgnoreCase) == true)\n            {\n                // Find embeds with the same URL that only contain a single image and nothing else\n                var trailingEmbeds = embeds\n                    .Skip(i + 1)\n                    .TakeWhile(e =>\n                        e.Url == embed.Url\n                        && e.Timestamp is null\n                        && e.Author is null\n                        && e.Color is null\n                        && string.IsNullOrWhiteSpace(e.Description)\n                        && !e.Fields.Any()\n                        && e.Images.Count == 1\n                        && e.Footer is null\n                    )\n                    .ToArray();\n\n                if (trailingEmbeds.Any())\n                {\n                    // Concatenate all images into one embed\n                    var images = embed\n                        .Images.Concat(trailingEmbeds.SelectMany(e => e.Images))\n                        .ToArray();\n\n                    normalizedEmbeds.Add(embed with { Images = images });\n\n                    i += trailingEmbeds.Length;\n                }\n                else\n                {\n                    normalizedEmbeds.Add(embed);\n                }\n            }\n            else\n            {\n                normalizedEmbeds.Add(embed);\n            }\n        }\n\n        return normalizedEmbeds;\n    }\n\n    public static Message Parse(JsonElement json)\n    {\n        var id = json.GetProperty(\"id\").GetNonWhiteSpaceString().Pipe(Snowflake.Parse);\n        var kind = json.GetProperty(\"type\").GetInt32().Pipe(t => (MessageKind)t);\n\n        var flags =\n            json.GetPropertyOrNull(\"flags\")?.GetInt32OrNull()?.Pipe(f => (MessageFlags)f)\n            ?? MessageFlags.None;\n\n        var author = json.GetProperty(\"author\").Pipe(User.Parse);\n        var timestamp = json.GetProperty(\"timestamp\").GetDateTimeOffset();\n        var editedTimestamp = json.GetPropertyOrNull(\"edited_timestamp\")?.GetDateTimeOffsetOrNull();\n        var callEndedTimestamp = json.GetPropertyOrNull(\"call\")\n            ?.GetPropertyOrNull(\"ended_timestamp\")\n            ?.GetDateTimeOffsetOrNull();\n\n        var isPinned = json.GetPropertyOrNull(\"pinned\")?.GetBooleanOrNull() ?? false;\n        var content = json.GetPropertyOrNull(\"content\")?.GetStringOrNull() ?? \"\";\n\n        var attachments =\n            json.GetPropertyOrNull(\"attachments\")\n                ?.EnumerateArrayOrNull()\n                ?.Select(Attachment.Parse)\n                .ToArray()\n            ?? [];\n\n        var embeds = NormalizeEmbeds(\n            json.GetPropertyOrNull(\"embeds\")?.EnumerateArrayOrNull()?.Select(Embed.Parse).ToArray()\n                ?? []\n        );\n\n        var stickers =\n            json.GetPropertyOrNull(\"sticker_items\")\n                ?.EnumerateArrayOrNull()\n                ?.Select(Sticker.Parse)\n                .ToArray()\n            ?? [];\n\n        var reactions =\n            json.GetPropertyOrNull(\"reactions\")\n                ?.EnumerateArrayOrNull()\n                ?.Select(Reaction.Parse)\n                .ToArray()\n            ?? [];\n\n        var mentionedUsers =\n            json.GetPropertyOrNull(\"mentions\")?.EnumerateArrayOrNull()?.Select(User.Parse).ToArray()\n            ?? [];\n\n        var messageReference = json.GetPropertyOrNull(\"message_reference\")\n            ?.Pipe(MessageReference.Parse);\n\n        var referencedMessage = json.GetPropertyOrNull(\"referenced_message\")?.Pipe(Parse);\n\n        // Currently Discord only supports 1 snapshot per forward\n        var forwardedMessage = json.GetPropertyOrNull(\"message_snapshots\")\n            ?.EnumerateArrayOrNull()\n            ?.Select(j => j.GetPropertyOrNull(\"message\"))\n            .WhereNotNull()\n            .Select(MessageSnapshot.Parse)\n            .FirstOrDefault();\n\n        var interaction = json.GetPropertyOrNull(\"interaction\")?.Pipe(Interaction.Parse);\n\n        return new Message(\n            id,\n            kind,\n            flags,\n            author,\n            timestamp,\n            editedTimestamp,\n            callEndedTimestamp,\n            isPinned,\n            content,\n            attachments,\n            embeds,\n            stickers,\n            reactions,\n            mentionedUsers,\n            messageReference,\n            referencedMessage,\n            forwardedMessage,\n            interaction\n        );\n    }\n}\n"
  },
  {
    "path": "DiscordChatExporter.Core/Discord/Data/MessageFlags.cs",
    "content": "﻿using System;\n\nnamespace DiscordChatExporter.Core.Discord.Data;\n\n// https://discord.com/developers/docs/resources/channel#message-object-message-flags\n[Flags]\npublic enum MessageFlags\n{\n    None = 0,\n    CrossPosted = 1,\n    CrossPost = 2,\n    SuppressEmbeds = 4,\n    SourceMessageDeleted = 8,\n    Urgent = 16,\n    HasThread = 32,\n    Ephemeral = 64,\n    Loading = 128,\n}\n"
  },
  {
    "path": "DiscordChatExporter.Core/Discord/Data/MessageKind.cs",
    "content": "﻿namespace DiscordChatExporter.Core.Discord.Data;\n\n// https://discord.com/developers/docs/resources/channel#message-object-message-types\npublic enum MessageKind\n{\n    Default = 0,\n    RecipientAdd = 1,\n    RecipientRemove = 2,\n    Call = 3,\n    ChannelNameChange = 4,\n    ChannelIconChange = 5,\n    ChannelPinnedMessage = 6,\n    GuildMemberJoin = 7,\n    ThreadCreated = 18,\n    Reply = 19,\n}\n"
  },
  {
    "path": "DiscordChatExporter.Core/Discord/Data/MessageReference.cs",
    "content": "using System.Text.Json;\nusing DiscordChatExporter.Core.Utils.Extensions;\nusing JsonExtensions.Reading;\n\nnamespace DiscordChatExporter.Core.Discord.Data;\n\n// https://discord.com/developers/docs/resources/channel#message-object-message-reference-structure\npublic record MessageReference(\n    MessageReferenceKind Kind,\n    Snowflake? MessageId,\n    Snowflake? ChannelId,\n    Snowflake? GuildId\n)\n{\n    public static MessageReference Parse(JsonElement json)\n    {\n        var kind =\n            json.GetPropertyOrNull(\"type\")?.GetInt32OrNull()?.Pipe(t => (MessageReferenceKind)t)\n            ?? MessageReferenceKind.Default;\n\n        var messageId = json.GetPropertyOrNull(\"message_id\")\n            ?.GetNonWhiteSpaceStringOrNull()\n            ?.Pipe(Snowflake.Parse);\n\n        var channelId = json.GetPropertyOrNull(\"channel_id\")\n            ?.GetNonWhiteSpaceStringOrNull()\n            ?.Pipe(Snowflake.Parse);\n\n        var guildId = json.GetPropertyOrNull(\"guild_id\")\n            ?.GetNonWhiteSpaceStringOrNull()\n            ?.Pipe(Snowflake.Parse);\n\n        return new MessageReference(kind, messageId, channelId, guildId);\n    }\n}\n"
  },
  {
    "path": "DiscordChatExporter.Core/Discord/Data/MessageReferenceKind.cs",
    "content": "namespace DiscordChatExporter.Core.Discord.Data;\n\n// https://discord.com/developers/docs/resources/channel#message-reference-types\npublic enum MessageReferenceKind\n{\n    Default = 0,\n    Forward = 1,\n}\n"
  },
  {
    "path": "DiscordChatExporter.Core/Discord/Data/MessageSnapshot.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Text.Json;\nusing DiscordChatExporter.Core.Discord.Data.Embeds;\nusing JsonExtensions.Reading;\n\nnamespace DiscordChatExporter.Core.Discord.Data;\n\n// https://docs.discord.com/developers/resources/message#message-snapshot-object\npublic record MessageSnapshot(\n    DateTimeOffset Timestamp,\n    DateTimeOffset? EditedTimestamp,\n    string Content,\n    IReadOnlyList<Attachment> Attachments,\n    IReadOnlyList<Embed> Embeds,\n    IReadOnlyList<Sticker> Stickers\n)\n{\n    public static MessageSnapshot Parse(JsonElement json)\n    {\n        var timestamp =\n            json.GetPropertyOrNull(\"timestamp\")?.GetDateTimeOffsetOrNull()\n            ?? DateTimeOffset.MinValue;\n\n        var editedTimestamp = json.GetPropertyOrNull(\"edited_timestamp\")?.GetDateTimeOffsetOrNull();\n\n        var content = json.GetPropertyOrNull(\"content\")?.GetStringOrNull() ?? \"\";\n\n        var attachments =\n            json.GetPropertyOrNull(\"attachments\")\n                ?.EnumerateArrayOrNull()\n                ?.Select(Attachment.Parse)\n                .ToArray()\n            ?? [];\n\n        var embeds =\n            json.GetPropertyOrNull(\"embeds\")?.EnumerateArrayOrNull()?.Select(Embed.Parse).ToArray()\n            ?? [];\n\n        var stickers =\n            json.GetPropertyOrNull(\"sticker_items\")\n                ?.EnumerateArrayOrNull()\n                ?.Select(Sticker.Parse)\n                .ToArray()\n            ?? [];\n\n        return new MessageSnapshot(\n            timestamp,\n            editedTimestamp,\n            content,\n            attachments,\n            embeds,\n            stickers\n        );\n    }\n}\n"
  },
  {
    "path": "DiscordChatExporter.Core/Discord/Data/Reaction.cs",
    "content": "﻿using System.Text.Json;\nusing DiscordChatExporter.Core.Utils.Extensions;\n\nnamespace DiscordChatExporter.Core.Discord.Data;\n\n// https://discord.com/developers/docs/resources/channel#reaction-object\npublic record Reaction(Emoji Emoji, int Count)\n{\n    public static Reaction Parse(JsonElement json)\n    {\n        var emoji = json.GetProperty(\"emoji\").Pipe(Emoji.Parse);\n        var count = json.GetProperty(\"count\").GetInt32();\n\n        return new Reaction(emoji, count);\n    }\n}\n"
  },
  {
    "path": "DiscordChatExporter.Core/Discord/Data/Role.cs",
    "content": "﻿using System.Drawing;\nusing System.Text.Json;\nusing DiscordChatExporter.Core.Discord.Data.Common;\nusing DiscordChatExporter.Core.Utils.Extensions;\nusing JsonExtensions.Reading;\n\nnamespace DiscordChatExporter.Core.Discord.Data;\n\n// https://discord.com/developers/docs/topics/permissions#role-object\npublic record Role(Snowflake Id, string Name, int Position, Color? Color) : IHasId\n{\n    public static Role Parse(JsonElement json)\n    {\n        var id = json.GetProperty(\"id\").GetNonWhiteSpaceString().Pipe(Snowflake.Parse);\n        var name = json.GetProperty(\"name\").GetNonNullString();\n        var position = json.GetProperty(\"position\").GetInt32();\n\n        var color = json.GetPropertyOrNull(\"color\")\n            ?.GetInt32OrNull()\n            ?.Pipe(System.Drawing.Color.FromArgb)\n            .ResetAlpha()\n            .NullIf(c => c.ToRgb() <= 0);\n\n        return new Role(id, name, position, color);\n    }\n}\n"
  },
  {
    "path": "DiscordChatExporter.Core/Discord/Data/Sticker.cs",
    "content": "﻿using System;\nusing System.Text.Json;\nusing DiscordChatExporter.Core.Discord.Data.Common;\nusing DiscordChatExporter.Core.Utils.Extensions;\nusing JsonExtensions.Reading;\n\nnamespace DiscordChatExporter.Core.Discord.Data;\n\n// https://discord.com/developers/docs/resources/sticker#sticker-resource\npublic partial record Sticker(Snowflake Id, string Name, StickerFormat Format, string SourceUrl)\n{\n    public bool IsImage { get; } = Format != StickerFormat.Lottie;\n}\n\npublic partial record Sticker\n{\n    public static Sticker Parse(JsonElement json)\n    {\n        var id = json.GetProperty(\"id\").GetNonWhiteSpaceString().Pipe(Snowflake.Parse);\n        var name = json.GetProperty(\"name\").GetNonNullString();\n        var format = json.GetProperty(\"format_type\").GetInt32().Pipe(t => (StickerFormat)t);\n\n        var sourceUrl = ImageCdn.GetStickerUrl(\n            id,\n            format switch\n            {\n                StickerFormat.Png => \"png\",\n                StickerFormat.Apng => \"png\",\n                StickerFormat.Lottie => \"json\",\n                StickerFormat.Gif => \"gif\",\n                _ => throw new InvalidOperationException($\"Unknown sticker format '{format}'.\"),\n            }\n        );\n\n        return new Sticker(id, name, format, sourceUrl);\n    }\n}\n"
  },
  {
    "path": "DiscordChatExporter.Core/Discord/Data/StickerFormat.cs",
    "content": "﻿namespace DiscordChatExporter.Core.Discord.Data;\n\npublic enum StickerFormat\n{\n    Png = 1,\n    Apng = 2,\n    Lottie = 3,\n    Gif = 4,\n}\n"
  },
  {
    "path": "DiscordChatExporter.Core/Discord/Data/User.cs",
    "content": "﻿using System.Text.Json;\nusing DiscordChatExporter.Core.Discord.Data.Common;\nusing DiscordChatExporter.Core.Utils.Extensions;\nusing JsonExtensions.Reading;\n\nnamespace DiscordChatExporter.Core.Discord.Data;\n\n// https://discord.com/developers/docs/resources/user#user-object\npublic partial record User(\n    Snowflake Id,\n    bool IsBot,\n    // Remove after Discord migrates all accounts to the new system.\n    // With that, also remove the DiscriminatorFormatted and FullName properties.\n    // Replace existing calls to FullName with Name (not DisplayName).\n    int? Discriminator,\n    string Name,\n    string DisplayName,\n    string AvatarUrl\n) : IHasId\n{\n    public string DiscriminatorFormatted { get; } =\n        Discriminator is not null ? $\"{Discriminator:0000}\" : \"0000\";\n\n    // This effectively represents the user's true identity.\n    // In the old system, this is formed from the username and discriminator.\n    // In the new system, the username is already the user's unique identifier.\n    public string FullName => Discriminator is not null ? $\"{Name}#{DiscriminatorFormatted}\" : Name;\n}\n\npublic partial record User\n{\n    public static User Parse(JsonElement json)\n    {\n        var id = json.GetProperty(\"id\").GetNonWhiteSpaceString().Pipe(Snowflake.Parse);\n        var isBot = json.GetPropertyOrNull(\"bot\")?.GetBooleanOrNull() ?? false;\n\n        var discriminator = json.GetPropertyOrNull(\"discriminator\")\n            ?.GetNonWhiteSpaceStringOrNull()\n            ?.Pipe(int.Parse)\n            .NullIfDefault();\n\n        var name = json.GetProperty(\"username\").GetNonNullString();\n        var displayName =\n            json.GetPropertyOrNull(\"global_name\")?.GetNonWhiteSpaceStringOrNull() ?? name;\n\n        var avatarIndex = discriminator % 5 ?? (int)((id.Value >> 22) % 6);\n\n        var avatarUrl =\n            json.GetPropertyOrNull(\"avatar\")\n                ?.GetNonWhiteSpaceStringOrNull()\n                ?.Pipe(h => ImageCdn.GetUserAvatarUrl(id, h))\n            ?? ImageCdn.GetFallbackUserAvatarUrl(avatarIndex);\n\n        return new User(id, isBot, discriminator, name, displayName, avatarUrl);\n    }\n}\n"
  },
  {
    "path": "DiscordChatExporter.Core/Discord/DiscordClient.cs",
    "content": "﻿using System;\nusing System.Collections.Generic;\nusing System.Globalization;\nusing System.Linq;\nusing System.Net;\nusing System.Net.Http;\nusing System.Runtime.CompilerServices;\nusing System.Text.Json;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing DiscordChatExporter.Core.Discord.Data;\nusing DiscordChatExporter.Core.Exceptions;\nusing DiscordChatExporter.Core.Utils;\nusing DiscordChatExporter.Core.Utils.Extensions;\nusing Gress;\nusing JsonExtensions.Http;\nusing JsonExtensions.Reading;\n\nnamespace DiscordChatExporter.Core.Discord;\n\npublic class DiscordClient(\n    string token,\n    RateLimitPreference rateLimitPreference = RateLimitPreference.RespectAll\n)\n{\n    private readonly Uri _baseUri = new(\"https://discord.com/api/v10/\", UriKind.Absolute);\n    private TokenKind? _resolvedTokenKind;\n\n    private async ValueTask<HttpResponseMessage> GetResponseAsync(\n        string url,\n        TokenKind tokenKind,\n        CancellationToken cancellationToken = default\n    )\n    {\n        return await Http.ResponseResiliencePipeline.ExecuteAsync(\n            async innerCancellationToken =>\n            {\n                using var request = new HttpRequestMessage(HttpMethod.Get, new Uri(_baseUri, url));\n\n                // Don't validate because the token can have special characters\n                // https://github.com/Tyrrrz/DiscordChatExporter/issues/828\n                request.Headers.TryAddWithoutValidation(\n                    \"Authorization\",\n                    tokenKind == TokenKind.Bot ? $\"Bot {token}\" : token\n                );\n\n                var response = await Http.Client.SendAsync(\n                    request,\n                    HttpCompletionOption.ResponseHeadersRead,\n                    innerCancellationToken\n                );\n\n                // Discord has advisory rate limits (communicated via response headers), but they are typically\n                // way stricter than the actual rate limits enforced by the server.\n                // The user may choose to ignore the advisory rate limits and only retry on hard rate limits,\n                // if they want to prioritize speed over compliance (and safety of their account/bot).\n                // https://github.com/Tyrrrz/DiscordChatExporter/issues/1021\n                if (rateLimitPreference.IsRespectedFor(tokenKind))\n                {\n                    var remainingRequestCount = response\n                        .Headers.TryGetValue(\"X-RateLimit-Remaining\")\n                        ?.Pipe(s => int.Parse(s, CultureInfo.InvariantCulture));\n\n                    var resetAfterDelay = response\n                        .Headers.TryGetValue(\"X-RateLimit-Reset-After\")\n                        ?.Pipe(s => double.Parse(s, CultureInfo.InvariantCulture))\n                        .Pipe(TimeSpan.FromSeconds);\n\n                    // If this was the last request available before hitting the rate limit,\n                    // wait out the reset time so that future requests can succeed.\n                    // This may add an unnecessary delay in case the user doesn't intend to\n                    // make any more requests, but implementing a smarter solution would\n                    // require properly keeping track of Discord's global/per-route/per-resource\n                    // rate limits and that's just way too much effort.\n                    // https://discord.com/developers/docs/topics/rate-limits\n                    if (remainingRequestCount <= 0 && resetAfterDelay is not null)\n                    {\n                        var delay =\n                            // Adding a small buffer to the reset time reduces the chance of getting\n                            // rate limited again, because it allows for more requests to be released.\n                            (resetAfterDelay.Value + TimeSpan.FromSeconds(1))\n                            // Sometimes Discord returns an absurdly high value for the reset time, which\n                            // is not actually enforced by the server. So we cap it at a reasonable value.\n                            .Clamp(TimeSpan.Zero, TimeSpan.FromSeconds(60));\n\n                        await Task.Delay(delay, innerCancellationToken);\n                    }\n                }\n\n                return response;\n            },\n            cancellationToken\n        );\n    }\n\n    private async ValueTask<TokenKind> ResolveTokenKindAsync(\n        CancellationToken cancellationToken = default\n    )\n    {\n        if (_resolvedTokenKind is not null)\n            return _resolvedTokenKind.Value;\n\n        // Try authenticating as a user\n        using var userResponse = await GetResponseAsync(\n            \"users/@me\",\n            TokenKind.User,\n            cancellationToken\n        );\n\n        if (userResponse.StatusCode != HttpStatusCode.Unauthorized)\n            return (_resolvedTokenKind = TokenKind.User).Value;\n\n        // Try authenticating as a bot\n        using var botResponse = await GetResponseAsync(\n            \"users/@me\",\n            TokenKind.Bot,\n            cancellationToken\n        );\n\n        if (botResponse.StatusCode != HttpStatusCode.Unauthorized)\n            return (_resolvedTokenKind = TokenKind.Bot).Value;\n\n        throw new DiscordChatExporterException(\"Authentication token is invalid.\", true);\n    }\n\n    private async ValueTask<HttpResponseMessage> GetResponseAsync(\n        string url,\n        CancellationToken cancellationToken = default\n    ) =>\n        await GetResponseAsync(\n            url,\n            await ResolveTokenKindAsync(cancellationToken),\n            cancellationToken\n        );\n\n    private async ValueTask<JsonElement> GetJsonResponseAsync(\n        string url,\n        CancellationToken cancellationToken = default\n    )\n    {\n        using var response = await GetResponseAsync(url, cancellationToken);\n\n        if (!response.IsSuccessStatusCode)\n        {\n            throw response.StatusCode switch\n            {\n                HttpStatusCode.Unauthorized => throw new DiscordChatExporterException(\n                    \"Authentication token is invalid.\",\n                    true\n                ),\n\n                HttpStatusCode.Forbidden => throw new DiscordChatExporterException(\n                    $\"Request to '{url}' failed: forbidden.\"\n                ),\n\n                HttpStatusCode.NotFound => throw new DiscordChatExporterException(\n                    $\"Request to '{url}' failed: not found.\"\n                ),\n\n                _ => throw new DiscordChatExporterException(\n                    $\"\"\"\n                    Request to '{url}' failed: {response\n                        .StatusCode.ToString()\n                        .ToSpaceSeparatedWords()\n                        .ToLowerInvariant()}.\n                    Response content: {await response.Content.ReadAsStringAsync(\n                        cancellationToken\n                    )}\n                    \"\"\",\n                    true\n                ),\n            };\n        }\n\n        return await response.Content.ReadAsJsonAsync(cancellationToken);\n    }\n\n    private async ValueTask<JsonElement?> TryGetJsonResponseAsync(\n        string url,\n        CancellationToken cancellationToken = default\n    )\n    {\n        using var response = await GetResponseAsync(url, cancellationToken);\n        return response.IsSuccessStatusCode\n            ? await response.Content.ReadAsJsonAsync(cancellationToken)\n            : null;\n    }\n\n    public async ValueTask<Application> GetApplicationAsync(\n        CancellationToken cancellationToken = default\n    )\n    {\n        var response = await GetJsonResponseAsync(\"applications/@me\", cancellationToken);\n        return Application.Parse(response);\n    }\n\n    private async ValueTask EnsureMessageContentIntentAsync(\n        CancellationToken cancellationToken = default\n    )\n    {\n        if (await ResolveTokenKindAsync(cancellationToken) != TokenKind.Bot)\n            return;\n\n        var application = await GetApplicationAsync(cancellationToken);\n        if (application.IsMessageContentIntentEnabled)\n            return;\n\n        throw new DiscordChatExporterException(\n            \"Provided bot account is missing the MESSAGE_CONTENT privileged intent.\",\n            true\n        );\n    }\n\n    public async ValueTask<User?> TryGetUserAsync(\n        Snowflake userId,\n        CancellationToken cancellationToken = default\n    )\n    {\n        var response = await TryGetJsonResponseAsync($\"users/{userId}\", cancellationToken);\n        return response?.Pipe(User.Parse);\n    }\n\n    public async IAsyncEnumerable<Guild> GetUserGuildsAsync(\n        [EnumeratorCancellation] CancellationToken cancellationToken = default\n    )\n    {\n        yield return Guild.DirectMessages;\n\n        var currentAfter = Snowflake.Zero;\n        while (true)\n        {\n            var url = new UrlBuilder()\n                .SetPath(\"users/@me/guilds\")\n                .SetQueryParameter(\"limit\", \"100\")\n                .SetQueryParameter(\"after\", currentAfter.ToString())\n                .Build();\n\n            var response = await GetJsonResponseAsync(url, cancellationToken);\n\n            var count = 0;\n            foreach (var guildJson in response.EnumerateArray())\n            {\n                var guild = Guild.Parse(guildJson);\n                yield return guild;\n\n                currentAfter = guild.Id;\n                count++;\n            }\n\n            if (count <= 0)\n                yield break;\n        }\n    }\n\n    public async ValueTask<Guild> GetGuildAsync(\n        Snowflake guildId,\n        CancellationToken cancellationToken = default\n    )\n    {\n        if (guildId == Guild.DirectMessages.Id)\n            return Guild.DirectMessages;\n\n        var response = await GetJsonResponseAsync($\"guilds/{guildId}\", cancellationToken);\n        return Guild.Parse(response);\n    }\n\n    public async IAsyncEnumerable<Channel> GetGuildChannelsAsync(\n        Snowflake guildId,\n        [EnumeratorCancellation] CancellationToken cancellationToken = default\n    )\n    {\n        if (guildId == Guild.DirectMessages.Id)\n        {\n            var response = await GetJsonResponseAsync(\"users/@me/channels\", cancellationToken);\n            foreach (var channelJson in response.EnumerateArray())\n                yield return Channel.Parse(channelJson);\n        }\n        else\n        {\n            var response = await GetJsonResponseAsync(\n                $\"guilds/{guildId}/channels\",\n                cancellationToken\n            );\n\n            var channelsJson = response\n                .EnumerateArray()\n                .OrderBy(j => j.GetProperty(\"position\").GetInt32())\n                .ThenBy(j => j.GetProperty(\"id\").GetNonWhiteSpaceString().Pipe(Snowflake.Parse))\n                .ToArray();\n\n            var parentsById = channelsJson\n                .Where(j => j.GetProperty(\"type\").GetInt32() == (int)ChannelKind.GuildCategory)\n                .Select((j, i) => Channel.Parse(j, null, i + 1))\n                .ToDictionary(j => j.Id);\n\n            // Discord channel positions are relative, so we need to normalize them\n            // so that the user may refer to them more easily in file name templates.\n            var position = 0;\n\n            foreach (var channelJson in channelsJson)\n            {\n                var parent = channelJson\n                    .GetPropertyOrNull(\"parent_id\")\n                    ?.GetNonWhiteSpaceStringOrNull()\n                    ?.Pipe(Snowflake.Parse)\n                    .Pipe(parentsById.GetValueOrDefault);\n\n                yield return Channel.Parse(channelJson, parent, position);\n                position++;\n            }\n        }\n    }\n\n    public async IAsyncEnumerable<Channel> GetGuildThreadsAsync(\n        Snowflake guildId,\n        bool includeArchived = false,\n        Snowflake? before = null,\n        Snowflake? after = null,\n        [EnumeratorCancellation] CancellationToken cancellationToken = default\n    )\n    {\n        if (guildId == Guild.DirectMessages.Id)\n            yield break;\n\n        var channels = await GetGuildChannelsAsync(guildId, cancellationToken);\n\n        foreach (\n            var channel in await GetChannelThreadsAsync(\n                channels,\n                includeArchived,\n                before,\n                after,\n                cancellationToken\n            )\n        )\n        {\n            yield return channel;\n        }\n    }\n\n    public async IAsyncEnumerable<Role> GetGuildRolesAsync(\n        Snowflake guildId,\n        [EnumeratorCancellation] CancellationToken cancellationToken = default\n    )\n    {\n        if (guildId == Guild.DirectMessages.Id)\n            yield break;\n\n        var response = await GetJsonResponseAsync($\"guilds/{guildId}/roles\", cancellationToken);\n        foreach (var roleJson in response.EnumerateArray())\n            yield return Role.Parse(roleJson);\n    }\n\n    public async ValueTask<Member?> TryGetGuildMemberAsync(\n        Snowflake guildId,\n        Snowflake memberId,\n        CancellationToken cancellationToken = default\n    )\n    {\n        if (guildId == Guild.DirectMessages.Id)\n            return null;\n\n        var response = await TryGetJsonResponseAsync(\n            $\"guilds/{guildId}/members/{memberId}\",\n            cancellationToken\n        );\n        return response?.Pipe(j => Member.Parse(j, guildId));\n    }\n\n    public async ValueTask<Invite?> TryGetInviteAsync(\n        string code,\n        CancellationToken cancellationToken = default\n    )\n    {\n        var response = await TryGetJsonResponseAsync($\"invites/{code}\", cancellationToken);\n        return response?.Pipe(Invite.Parse);\n    }\n\n    public async ValueTask<Channel> GetChannelAsync(\n        Snowflake channelId,\n        CancellationToken cancellationToken = default\n    )\n    {\n        var response = await GetJsonResponseAsync($\"channels/{channelId}\", cancellationToken);\n\n        var parentId = response\n            .GetPropertyOrNull(\"parent_id\")\n            ?.GetNonWhiteSpaceStringOrNull()\n            ?.Pipe(Snowflake.Parse);\n\n        // It's possible for the parent channel to be inaccessible, despite the\n        // child channel being accessible.\n        // https://github.com/Tyrrrz/DiscordChatExporter/issues/1108\n        var parent = parentId is not null\n            ? await TryGetChannelAsync(parentId.Value, cancellationToken)\n            : null;\n\n        return Channel.Parse(response, parent);\n    }\n\n    public async ValueTask<Channel?> TryGetChannelAsync(\n        Snowflake channelId,\n        CancellationToken cancellationToken = default\n    )\n    {\n        var response = await TryGetJsonResponseAsync($\"channels/{channelId}\", cancellationToken);\n        if (response is null)\n            return null;\n\n        var parentId = response\n            .Value.GetPropertyOrNull(\"parent_id\")\n            ?.GetNonWhiteSpaceStringOrNull()\n            ?.Pipe(Snowflake.Parse);\n\n        Channel? parent = null;\n        if (parentId is not null)\n        {\n            // It's possible for the parent channel to be inaccessible, despite the\n            // child channel being accessible.\n            // https://github.com/Tyrrrz/DiscordChatExporter/issues/1108\n            parent = await TryGetChannelAsync(parentId.Value, cancellationToken);\n        }\n\n        return Channel.Parse(response.Value, parent);\n    }\n\n    public async IAsyncEnumerable<Channel> GetChannelThreadsAsync(\n        IReadOnlyList<Channel> channels,\n        bool includeArchived = false,\n        Snowflake? before = null,\n        Snowflake? after = null,\n        [EnumeratorCancellation] CancellationToken cancellationToken = default\n    )\n    {\n        var filteredChannels = channels\n            // Categories cannot have threads\n            .Where(c => !c.IsCategory)\n            // Voice channels cannot have threads\n            .Where(c => !c.IsVoice)\n            // Empty channels cannot have threads\n            .Where(c => !c.IsEmpty)\n            // If the 'before' boundary is specified, skip channels that don't have messages\n            // for that range, because thread-start event should always be accompanied by a message.\n            // Note that we don't perform a similar check for the 'after' boundary, because\n            // threads may have messages in range, even if the parent channel doesn't.\n            .Where(c => before is null || c.MayHaveMessagesBefore(before.Value))\n            .ToArray();\n\n        // Track yielded thread IDs to avoid duplicates that can occur when a thread transitions\n        // from active to archived between the two separate API calls used to fetch threads.\n        // https://github.com/Tyrrrz/DiscordChatExporter/issues/1433\n        var seenThreadIds = new HashSet<Snowflake>();\n\n        // User accounts can only fetch threads using the search endpoint\n        if (await ResolveTokenKindAsync(cancellationToken) == TokenKind.User)\n        {\n            foreach (var channel in filteredChannels)\n            {\n                // Either include both active and archived threads, or only active threads\n                foreach (\n                    var isArchived in includeArchived ? new[] { false, true } : new[] { false }\n                )\n                {\n                    // Offset is just the index of the last thread in the previous batch\n                    var currentOffset = 0;\n                    while (true)\n                    {\n                        var url = new UrlBuilder()\n                            .SetPath($\"channels/{channel.Id}/threads/search\")\n                            .SetQueryParameter(\"sort_by\", \"last_message_time\")\n                            .SetQueryParameter(\"sort_order\", \"desc\")\n                            .SetQueryParameter(\"archived\", isArchived.ToString().ToLowerInvariant())\n                            .SetQueryParameter(\"offset\", currentOffset.ToString())\n                            .Build();\n\n                        // Can be null on channels that the user cannot access or channels without threads\n                        var response = await TryGetJsonResponseAsync(url, cancellationToken);\n                        if (response is null)\n                            break;\n\n                        var breakOuter = false;\n\n                        foreach (\n                            var threadJson in response.Value.GetProperty(\"threads\").EnumerateArray()\n                        )\n                        {\n                            var thread = Channel.Parse(threadJson, channel);\n\n                            // If the 'after' boundary is specified, we can break early,\n                            // because threads are sorted by last message timestamp.\n                            if (after is not null && !thread.MayHaveMessagesAfter(after.Value))\n                            {\n                                breakOuter = true;\n                                break;\n                            }\n\n                            if (seenThreadIds.Add(thread.Id))\n                                yield return thread;\n\n                            currentOffset++;\n                        }\n\n                        if (breakOuter)\n                            break;\n\n                        if (!response.Value.GetProperty(\"has_more\").GetBoolean())\n                            break;\n                    }\n                }\n            }\n        }\n        // Bot accounts can only fetch threads using the threads endpoint\n        else\n        {\n            var guilds = new HashSet<Snowflake>();\n            foreach (var channel in filteredChannels)\n                guilds.Add(channel.GuildId);\n\n            // Active threads\n            foreach (var guildId in guilds)\n            {\n                var parentsById = filteredChannels.ToDictionary(c => c.Id);\n\n                var response = await GetJsonResponseAsync(\n                    $\"guilds/{guildId}/threads/active\",\n                    cancellationToken\n                );\n\n                foreach (var threadJson in response.GetProperty(\"threads\").EnumerateArray())\n                {\n                    var parent = threadJson\n                        .GetPropertyOrNull(\"parent_id\")\n                        ?.GetNonWhiteSpaceStringOrNull()\n                        ?.Pipe(Snowflake.Parse)\n                        .Pipe(parentsById.GetValueOrDefault);\n\n                    if (filteredChannels.Contains(parent))\n                    {\n                        var thread = Channel.Parse(threadJson, parent);\n\n                        if (seenThreadIds.Add(thread.Id))\n                            yield return thread;\n                    }\n                }\n            }\n\n            // Archived threads\n            if (includeArchived)\n            {\n                foreach (var channel in filteredChannels)\n                {\n                    foreach (var archiveType in new[] { \"public\", \"private\" })\n                    {\n                        // This endpoint parameter expects an ISO8601 timestamp, not a snowflake\n                        var currentBefore = before\n                            ?.ToDate()\n                            .ToString(\"O\", CultureInfo.InvariantCulture);\n\n                        while (true)\n                        {\n                            // Threads are sorted by archive timestamp, not by last message timestamp\n                            var url = new UrlBuilder()\n                                .SetPath($\"channels/{channel.Id}/threads/archived/{archiveType}\")\n                                .SetQueryParameter(\"before\", currentBefore)\n                                .Build();\n\n                            // Can be null on certain channels\n                            var response = await TryGetJsonResponseAsync(url, cancellationToken);\n                            if (response is null)\n                                break;\n\n                            foreach (\n                                var threadJson in response\n                                    .Value.GetProperty(\"threads\")\n                                    .EnumerateArray()\n                            )\n                            {\n                                var thread = Channel.Parse(threadJson, channel);\n\n                                currentBefore = threadJson\n                                    .GetProperty(\"thread_metadata\")\n                                    .GetProperty(\"archive_timestamp\")\n                                    .GetString();\n\n                                if (seenThreadIds.Add(thread.Id))\n                                    yield return thread;\n                            }\n\n                            if (!response.Value.GetProperty(\"has_more\").GetBoolean())\n                                break;\n                        }\n                    }\n                }\n            }\n        }\n    }\n\n    private async ValueTask<Message?> TryGetFirstMessageAsync(\n        Snowflake channelId,\n        Snowflake? after = null,\n        CancellationToken cancellationToken = default\n    )\n    {\n        var url = new UrlBuilder()\n            .SetPath($\"channels/{channelId}/messages\")\n            .SetQueryParameter(\"limit\", \"1\")\n            .SetQueryParameter(\"after\", (after ?? Snowflake.Zero).ToString())\n            .Build();\n\n        var response = await GetJsonResponseAsync(url, cancellationToken);\n        var message = response.EnumerateArray().Select(Message.Parse).FirstOrDefault();\n\n        return message;\n    }\n\n    private async ValueTask<Message?> TryGetLastMessageAsync(\n        Snowflake channelId,\n        Snowflake? before = null,\n        CancellationToken cancellationToken = default\n    )\n    {\n        var url = new UrlBuilder()\n            .SetPath($\"channels/{channelId}/messages\")\n            .SetQueryParameter(\"limit\", \"1\")\n            .SetQueryParameter(\"before\", before?.ToString())\n            .Build();\n\n        var response = await GetJsonResponseAsync(url, cancellationToken);\n        return response.EnumerateArray().Select(Message.Parse).LastOrDefault();\n    }\n\n    public async IAsyncEnumerable<Message> GetMessagesAsync(\n        Snowflake channelId,\n        Snowflake? after = null,\n        Snowflake? before = null,\n        IProgress<Percentage>? progress = null,\n        [EnumeratorCancellation] CancellationToken cancellationToken = default\n    )\n    {\n        // Get the last message in the specified range, so we can later calculate the\n        // progress based on the difference between message timestamps.\n        // This also snapshots the boundaries, which means that messages posted after\n        // the export started will not appear in the output.\n        var lastMessage = await TryGetLastMessageAsync(channelId, before, cancellationToken);\n        if (lastMessage is null || lastMessage.Timestamp < after?.ToDate())\n            yield break;\n\n        // Keep track of the first message in range in order to calculate the progress\n        var firstMessage = default(Message);\n\n        var currentAfter = after ?? Snowflake.Zero;\n        while (true)\n        {\n            var url = new UrlBuilder()\n                .SetPath($\"channels/{channelId}/messages\")\n                .SetQueryParameter(\"limit\", \"100\")\n                .SetQueryParameter(\"after\", currentAfter.ToString())\n                .Build();\n\n            var response = await GetJsonResponseAsync(url, cancellationToken);\n\n            var messages = response\n                .EnumerateArray()\n                .Select(Message.Parse)\n                // Messages are returned from newest to oldest, so we need to reverse them\n                .Reverse()\n                .ToArray();\n\n            // Break if there are no messages (can happen if messages are deleted during execution)\n            if (!messages.Any())\n                yield break;\n\n            // If all messages are empty, make sure that it's not because the bot account doesn't\n            // have the MESSAGE_CONTENT intent enabled.\n            // https://github.com/Tyrrrz/DiscordChatExporter/issues/1106#issuecomment-1741548959\n            if (messages.All(m => m.IsEmpty))\n                await EnsureMessageContentIntentAsync(cancellationToken);\n\n            foreach (var message in messages)\n            {\n                firstMessage ??= message;\n\n                // Ensure that the messages are in range\n                if (message.Timestamp > lastMessage.Timestamp)\n                    yield break;\n\n                // Report progress based on timestamps\n                if (progress is not null)\n                {\n                    var exportedDuration = (message.Timestamp - firstMessage.Timestamp).Duration();\n                    var totalDuration = (lastMessage.Timestamp - firstMessage.Timestamp).Duration();\n\n                    progress.Report(\n                        Percentage.FromFraction(\n                            // Avoid division by zero if all messages have the exact same timestamp\n                            // (which happens when there's only one message in the channel)\n                            totalDuration > TimeSpan.Zero\n                                ? exportedDuration / totalDuration\n                                : 1\n                        )\n                    );\n                }\n\n                yield return message;\n                currentAfter = message.Id;\n            }\n        }\n    }\n\n    public async IAsyncEnumerable<Message> GetMessagesInReverseAsync(\n        Snowflake channelId,\n        Snowflake? after = null,\n        Snowflake? before = null,\n        IProgress<Percentage>? progress = null,\n        [EnumeratorCancellation] CancellationToken cancellationToken = default\n    )\n    {\n        // Get the first message in the specified range, so we can later calculate the\n        // progress based on the difference between message timestamps.\n        // Snapshotting is not necessary here because new messages can't appear in the past.\n        var firstMessage = await TryGetFirstMessageAsync(channelId, after, cancellationToken);\n        if (firstMessage is null || firstMessage.Timestamp > before?.ToDate())\n            yield break;\n\n        // Keep track of the last message in range in order to calculate the progress\n        var lastMessage = default(Message);\n\n        var currentBefore = before;\n        while (true)\n        {\n            var url = new UrlBuilder()\n                .SetPath($\"channels/{channelId}/messages\")\n                .SetQueryParameter(\"limit\", \"100\")\n                .SetQueryParameter(\"before\", currentBefore?.ToString())\n                .Build();\n\n            var response = await GetJsonResponseAsync(url, cancellationToken);\n\n            var messages = response.EnumerateArray().Select(Message.Parse).ToArray();\n\n            // Break if there are no messages (can happen if messages are deleted during execution)\n            if (!messages.Any())\n                yield break;\n\n            // If all messages are empty, make sure that it's not because the bot account doesn't\n            // have the MESSAGE_CONTENT intent enabled.\n            // https://github.com/Tyrrrz/DiscordChatExporter/issues/1106#issuecomment-1741548959\n            if (messages.All(m => m.IsEmpty))\n                await EnsureMessageContentIntentAsync(cancellationToken);\n\n            foreach (var message in messages)\n            {\n                lastMessage ??= message;\n\n                // Report progress based on timestamps\n                if (progress is not null)\n                {\n                    var exportedDuration = (lastMessage.Timestamp - message.Timestamp).Duration();\n                    var totalDuration = (lastMessage.Timestamp - firstMessage.Timestamp).Duration();\n\n                    progress.Report(\n                        Percentage.FromFraction(\n                            // Avoid division by zero if all messages have the exact same timestamp\n                            // (which happens when there's only one message in the channel)\n                            totalDuration > TimeSpan.Zero\n                                ? exportedDuration / totalDuration\n                                : 1\n                        )\n                    );\n                }\n\n                yield return message;\n            }\n\n            currentBefore = messages.Last().Id;\n        }\n    }\n\n    public async IAsyncEnumerable<User> GetMessageReactionsAsync(\n        Snowflake channelId,\n        Snowflake messageId,\n        Emoji emoji,\n        [EnumeratorCancellation] CancellationToken cancellationToken = default\n    )\n    {\n        var reactionName = emoji.Id is not null\n            // Custom emoji\n            ? emoji.Name + ':' + emoji.Id\n            // Standard emoji\n            : emoji.Name;\n\n        var currentAfter = Snowflake.Zero;\n        while (true)\n        {\n            var url = new UrlBuilder()\n                .SetPath(\n                    $\"channels/{channelId}/messages/{messageId}/reactions/{Uri.EscapeDataString(reactionName)}\"\n                )\n                .SetQueryParameter(\"limit\", \"100\")\n                .SetQueryParameter(\"after\", currentAfter.ToString())\n                .Build();\n\n            // Can be null on reactions with an emoji that has been deleted (?)\n            // https://github.com/Tyrrrz/DiscordChatExporter/issues/1226\n            var response = await TryGetJsonResponseAsync(url, cancellationToken);\n            if (response is null)\n                yield break;\n\n            var count = 0;\n            foreach (var userJson in response.Value.EnumerateArray())\n            {\n                var user = User.Parse(userJson);\n                yield return user;\n\n                currentAfter = user.Id;\n                count++;\n            }\n\n            if (count <= 0)\n                yield break;\n        }\n    }\n}\n"
  },
  {
    "path": "DiscordChatExporter.Core/Discord/Dump/DataDump.cs",
    "content": "﻿using System;\nusing System.Collections.Generic;\nusing System.IO.Compression;\nusing System.Linq;\nusing System.Text.Json;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing JsonExtensions.Reading;\n\nnamespace DiscordChatExporter.Core.Discord.Dump;\n\npublic partial class DataDump(IReadOnlyList<DataDumpChannel> channels)\n{\n    public IReadOnlyList<DataDumpChannel> Channels { get; } = channels;\n}\n\npublic partial class DataDump\n{\n    public static DataDump Parse(JsonElement json)\n    {\n        var channels = new List<DataDumpChannel>();\n\n        foreach (var property in json.EnumerateObjectOrEmpty())\n        {\n            var channelId = Snowflake.Parse(property.Name);\n            var channelName = property.Value.GetString();\n\n            // Null items refer to deleted channels\n            if (channelName is null)\n                continue;\n\n            var channel = new DataDumpChannel(channelId, channelName);\n            channels.Add(channel);\n        }\n\n        return new DataDump(channels);\n    }\n\n    public static async ValueTask<DataDump> LoadAsync(\n        string zipFilePath,\n        CancellationToken cancellationToken = default\n    )\n    {\n        await using var archive = await ZipFile.OpenReadAsync(zipFilePath, cancellationToken);\n\n        // Use case-insensitive search to accommodate for different data dump versions\n        // https://github.com/Tyrrrz/DiscordChatExporter/issues/1459\n        var entry =\n            archive.Entries.FirstOrDefault(e =>\n                e.FullName.Equals(\"messages/index.json\", StringComparison.OrdinalIgnoreCase)\n            )\n            ?? throw new InvalidOperationException(\n                \"Failed to locate the channel index inside the data package.\"\n            );\n\n        await using var stream = await entry.OpenAsync(cancellationToken);\n        using var document = await JsonDocument.ParseAsync(stream, default, cancellationToken);\n\n        return Parse(document.RootElement);\n    }\n}\n"
  },
  {
    "path": "DiscordChatExporter.Core/Discord/Dump/DataDumpChannel.cs",
    "content": "﻿namespace DiscordChatExporter.Core.Discord.Dump;\n\npublic record DataDumpChannel(Snowflake Id, string Name);\n"
  },
  {
    "path": "DiscordChatExporter.Core/Discord/RateLimitPreference.cs",
    "content": "﻿using System;\n\nnamespace DiscordChatExporter.Core.Discord;\n\n[Flags]\npublic enum RateLimitPreference\n{\n    IgnoreAll = 0,\n    RespectForUserTokens = 0b1,\n    RespectForBotTokens = 0b10,\n    RespectAll = RespectForUserTokens | RespectForBotTokens,\n}\n\npublic static class RateLimitPreferenceExtensions\n{\n    extension(RateLimitPreference rateLimitPreference)\n    {\n        internal bool IsRespectedFor(TokenKind tokenKind) =>\n            tokenKind switch\n            {\n                TokenKind.User => (rateLimitPreference & RateLimitPreference.RespectForUserTokens)\n                    != 0,\n                TokenKind.Bot => (rateLimitPreference & RateLimitPreference.RespectForBotTokens)\n                    != 0,\n                _ => throw new ArgumentOutOfRangeException(nameof(tokenKind)),\n            };\n\n        public string GetDisplayName() =>\n            rateLimitPreference switch\n            {\n                RateLimitPreference.IgnoreAll => \"Always ignore\",\n                RateLimitPreference.RespectForUserTokens => \"Respect for user tokens\",\n                RateLimitPreference.RespectForBotTokens => \"Respect for bot tokens\",\n                RateLimitPreference.RespectAll => \"Always respect\",\n                _ => throw new ArgumentOutOfRangeException(nameof(rateLimitPreference)),\n            };\n    }\n}\n"
  },
  {
    "path": "DiscordChatExporter.Core/Discord/Snowflake.cs",
    "content": "﻿using System;\nusing System.Diagnostics.CodeAnalysis;\nusing System.Globalization;\n\nnamespace DiscordChatExporter.Core.Discord;\n\npublic readonly partial record struct Snowflake(ulong Value)\n{\n    public DateTimeOffset ToDate() =>\n        DateTimeOffset\n            .FromUnixTimeMilliseconds((long)((Value >> 22) + 1420070400000UL))\n            .ToLocalTime();\n\n    [ExcludeFromCodeCoverage]\n    public override string ToString() => Value.ToString(CultureInfo.InvariantCulture);\n}\n\npublic partial record struct Snowflake\n{\n    public static Snowflake Zero { get; } = new(0);\n\n    public static Snowflake FromDate(DateTimeOffset instant) =>\n        new(((ulong)instant.ToUnixTimeMilliseconds() - 1420070400000UL) << 22);\n\n    public static Snowflake? TryParse(string? value, IFormatProvider? formatProvider = null)\n    {\n        if (string.IsNullOrWhiteSpace(value))\n            return null;\n\n        // As number\n        if (ulong.TryParse(value, NumberStyles.None, formatProvider, out var number))\n            return new Snowflake(number);\n\n        // As date\n        if (DateTimeOffset.TryParse(value, formatProvider, DateTimeStyles.None, out var instant))\n            return FromDate(instant);\n\n        return null;\n    }\n\n    public static Snowflake Parse(string value, IFormatProvider? formatProvider) =>\n        TryParse(value, formatProvider)\n        ?? throw new FormatException($\"Invalid snowflake '{value}'.\");\n\n    public static Snowflake Parse(string value) => Parse(value, null);\n}\n\npublic partial record struct Snowflake : IComparable<Snowflake>, IComparable\n{\n    public int CompareTo(Snowflake other) => Value.CompareTo(other.Value);\n\n    public int CompareTo(object? obj)\n    {\n        if (obj is not Snowflake other)\n            throw new ArgumentException($\"Object must be of type {nameof(Snowflake)}.\");\n\n        return Value.CompareTo(other.Value);\n    }\n\n    public static bool operator >(Snowflake left, Snowflake right) => left.CompareTo(right) > 0;\n\n    public static bool operator <(Snowflake left, Snowflake right) => left.CompareTo(right) < 0;\n}\n"
  },
  {
    "path": "DiscordChatExporter.Core/Discord/TokenKind.cs",
    "content": "﻿namespace DiscordChatExporter.Core.Discord;\n\npublic enum TokenKind\n{\n    User,\n    Bot,\n}\n"
  },
  {
    "path": "DiscordChatExporter.Core/DiscordChatExporter.Core.csproj",
    "content": "<Project Sdk=\"Microsoft.NET.Sdk\">\n  <ItemGroup>\n    <PackageReference Include=\"AngleSharp\" />\n    <PackageReference Include=\"AsyncKeyedLock\" />\n    <PackageReference Include=\"CSharpier.MsBuild\" PrivateAssets=\"all\" />\n    <PackageReference Include=\"Gress\" />\n    <PackageReference Include=\"JsonExtensions\" />\n    <PackageReference Include=\"Polly\" />\n    <PackageReference Include=\"RazorBlade\" />\n    <PackageReference Include=\"Superpower\" />\n    <PackageReference Include=\"WebMarkupMin.Core\" />\n    <PackageReference Include=\"YoutubeExplode\" />\n  </ItemGroup>\n</Project>\n"
  },
  {
    "path": "DiscordChatExporter.Core/Exceptions/ChannelEmptyException.cs",
    "content": "namespace DiscordChatExporter.Core.Exceptions;\n\npublic class ChannelEmptyException(string message) : DiscordChatExporterException(message);\n"
  },
  {
    "path": "DiscordChatExporter.Core/Exceptions/DiscordChatExporterException.cs",
    "content": "﻿using System;\n\nnamespace DiscordChatExporter.Core.Exceptions;\n\npublic class DiscordChatExporterException(\n    string message,\n    bool isFatal = false,\n    Exception? innerException = null\n) : Exception(message, innerException)\n{\n    public bool IsFatal { get; } = isFatal;\n}\n"
  },
  {
    "path": "DiscordChatExporter.Core/Exporting/ChannelExporter.cs",
    "content": "﻿using System;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing DiscordChatExporter.Core.Discord;\nusing DiscordChatExporter.Core.Discord.Data;\nusing DiscordChatExporter.Core.Exceptions;\nusing Gress;\n\nnamespace DiscordChatExporter.Core.Exporting;\n\npublic class ChannelExporter(DiscordClient discord)\n{\n    public async ValueTask ExportChannelAsync(\n        ExportRequest request,\n        IProgress<Percentage>? progress = null,\n        CancellationToken cancellationToken = default\n    )\n    {\n        // Forum channels don't have messages, they are just a list of threads\n        if (request.Channel.Kind == ChannelKind.GuildForum)\n        {\n            throw new DiscordChatExporterException(\n                $\"Channel '{request.Channel.Name}' \"\n                    + $\"of guild '{request.Guild.Name}' \"\n                    + $\"is a forum and cannot be exported directly. \"\n                    + \"You need to pull its threads and export them individually.\"\n            );\n        }\n\n        // Build context\n        var context = new ExportContext(discord, request);\n        await context.PopulateChannelsAndRolesAsync(cancellationToken);\n\n        // Initialize the exporter before further checks to ensure the file is created even if\n        // an exception is thrown after this point.\n        await using var messageExporter = new MessageExporter(context);\n\n        // Check if the channel is empty\n        if (request.Channel.IsEmpty)\n        {\n            throw new ChannelEmptyException(\n                $\"Channel '{request.Channel.Name}' \"\n                    + $\"of guild '{request.Guild.Name}' \"\n                    + $\"does not contain any messages; an empty file will be created.\"\n            );\n        }\n\n        // Check if the 'before' and 'after' boundaries are valid\n        if (\n            (\n                request.Before is not null\n                && !request.Channel.MayHaveMessagesBefore(request.Before.Value)\n            )\n            || (\n                request.After is not null\n                && !request.Channel.MayHaveMessagesAfter(request.After.Value)\n            )\n        )\n        {\n            throw new ChannelEmptyException(\n                $\"Channel '{request.Channel.Name}' \"\n                    + $\"of guild '{request.Guild.Name}' \"\n                    + $\"does not contain any messages within the specified period; an empty file will be created.\"\n            );\n        }\n\n        var messages = !request.IsReverseMessageOrder\n            ? discord.GetMessagesAsync(\n                request.Channel.Id,\n                request.After,\n                request.Before,\n                progress,\n                cancellationToken\n            )\n            : discord.GetMessagesInReverseAsync(\n                request.Channel.Id,\n                request.After,\n                request.Before,\n                progress,\n                cancellationToken\n            );\n\n        await foreach (var message in messages)\n        {\n            try\n            {\n                // Resolve members for referenced users\n                foreach (var user in message.GetReferencedUsers())\n                    await context.PopulateMemberAsync(user, cancellationToken);\n\n                // Export the message\n                if (request.MessageFilter.IsMatch(message))\n                    await messageExporter.ExportMessageAsync(message, cancellationToken);\n            }\n            catch (Exception ex)\n            {\n                // Provide more context to the exception, to simplify debugging based on error messages\n                throw new DiscordChatExporterException(\n                    $\"Failed to export message #{message.Id} \"\n                        + $\"in channel '{request.Channel.Name}' (#{request.Channel.Id}) \"\n                        + $\"of guild '{request.Guild.Name} (#{request.Guild.Id})'.\",\n                    ex is not DiscordChatExporterException dex || dex.IsFatal,\n                    ex\n                );\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "DiscordChatExporter.Core/Exporting/CsvMessageWriter.cs",
    "content": "﻿using System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Text;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing DiscordChatExporter.Core.Discord.Data;\nusing DiscordChatExporter.Core.Utils.Extensions;\n\nnamespace DiscordChatExporter.Core.Exporting;\n\ninternal partial class CsvMessageWriter(Stream stream, ExportContext context)\n    : MessageWriter(stream, context)\n{\n    private readonly TextWriter _writer = new StreamWriter(stream);\n\n    private async ValueTask<string> FormatMarkdownAsync(\n        string markdown,\n        CancellationToken cancellationToken = default\n    ) =>\n        Context.Request.ShouldFormatMarkdown\n            ? await PlainTextMarkdownVisitor.FormatAsync(Context, markdown, cancellationToken)\n            : markdown;\n\n    public override async ValueTask WritePreambleAsync(\n        CancellationToken cancellationToken = default\n    ) => await _writer.WriteLineAsync(\"AuthorID,Author,Date,Content,Attachments,Reactions\");\n\n    private async ValueTask WriteAttachmentsAsync(\n        IReadOnlyList<Attachment> attachments,\n        CancellationToken cancellationToken = default\n    )\n    {\n        var buffer = new StringBuilder();\n\n        foreach (var attachment in attachments)\n        {\n            cancellationToken.ThrowIfCancellationRequested();\n\n            buffer\n                .AppendIfNotEmpty(',')\n                .Append(await Context.ResolveAssetUrlAsync(attachment.Url, cancellationToken));\n        }\n\n        await _writer.WriteAsync(CsvEncode(buffer.ToString()));\n    }\n\n    private async ValueTask WriteReactionsAsync(\n        IReadOnlyList<Reaction> reactions,\n        CancellationToken cancellationToken = default\n    )\n    {\n        var buffer = new StringBuilder();\n\n        foreach (var reaction in reactions)\n        {\n            cancellationToken.ThrowIfCancellationRequested();\n\n            buffer\n                .AppendIfNotEmpty(',')\n                .Append(reaction.Emoji.Name)\n                .Append(' ')\n                .Append('(')\n                .Append(reaction.Count)\n                .Append(')');\n        }\n\n        await _writer.WriteAsync(CsvEncode(buffer.ToString()));\n    }\n\n    public override async ValueTask WriteMessageAsync(\n        Message message,\n        CancellationToken cancellationToken = default\n    )\n    {\n        await base.WriteMessageAsync(message, cancellationToken);\n\n        // Author ID\n        await _writer.WriteAsync(CsvEncode(message.Author.Id.ToString()));\n        await _writer.WriteAsync(',');\n\n        // Author name\n        await _writer.WriteAsync(CsvEncode(message.Author.FullName));\n        await _writer.WriteAsync(',');\n\n        // Message timestamp\n        await _writer.WriteAsync(CsvEncode(Context.FormatDate(message.Timestamp, \"o\")));\n        await _writer.WriteAsync(',');\n\n        // Message content\n        if (message.IsSystemNotification)\n        {\n            await _writer.WriteAsync(CsvEncode(message.GetFallbackContent()));\n        }\n        else\n        {\n            await _writer.WriteAsync(\n                CsvEncode(await FormatMarkdownAsync(message.Content, cancellationToken))\n            );\n        }\n\n        await _writer.WriteAsync(',');\n\n        // Attachments\n        await WriteAttachmentsAsync(message.Attachments, cancellationToken);\n        await _writer.WriteAsync(',');\n\n        // Reactions\n        await WriteReactionsAsync(message.Reactions, cancellationToken);\n\n        // Finish row\n        await _writer.WriteLineAsync();\n    }\n\n    public override async ValueTask DisposeAsync()\n    {\n        await _writer.DisposeAsync();\n        await base.DisposeAsync();\n    }\n}\n\ninternal partial class CsvMessageWriter\n{\n    private static string CsvEncode(string value)\n    {\n        value = value.Replace(\"\\\"\", \"\\\"\\\"\", StringComparison.Ordinal);\n        return $\"\\\"{value}\\\"\";\n    }\n}\n"
  },
  {
    "path": "DiscordChatExporter.Core/Exporting/ExportAssetDownloader.cs",
    "content": "﻿using System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Security.Cryptography;\nusing System.Text;\nusing System.Text.RegularExpressions;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing System.Web;\nusing AsyncKeyedLock;\nusing DiscordChatExporter.Core.Utils;\nusing DiscordChatExporter.Core.Utils.Extensions;\n\nnamespace DiscordChatExporter.Core.Exporting;\n\ninternal partial class ExportAssetDownloader(string workingDirPath, bool reuse)\n{\n    private static readonly AsyncKeyedLocker<string> Locker = new();\n\n    // File paths of the previously downloaded assets\n    private readonly Dictionary<string, string> _previousPathsByUrl = new(StringComparer.Ordinal);\n\n    public async ValueTask<string> DownloadAsync(\n        string url,\n        CancellationToken cancellationToken = default\n    )\n    {\n        var fileName = GetFileNameFromUrl(url);\n        var filePath = Path.Combine(workingDirPath, fileName);\n\n        using var _ = await Locker.LockAsync(filePath, cancellationToken);\n\n        if (_previousPathsByUrl.TryGetValue(url, out var cachedFilePath))\n            return cachedFilePath;\n\n        // Reuse existing files if we're allowed to\n        if (reuse && File.Exists(filePath))\n            return _previousPathsByUrl[url] = filePath;\n\n        // Check for a file cached by the legacy naming scheme (5-char hash) and rename it\n        // to the new naming scheme to preserve backwards compatibility with existing exports\n        if (reuse)\n        {\n            var legacyFilePath = Path.Combine(workingDirPath, GetLegacyFileNameFromUrl(url));\n            if (File.Exists(legacyFilePath))\n            {\n                // Overwrite in case the destination file was created concurrently between our\n                // earlier existence check and this move operation\n                try\n                {\n                    File.Move(legacyFilePath, filePath, overwrite: true);\n                    return _previousPathsByUrl[url] = filePath;\n                }\n                catch (IOException)\n                {\n                    // The legacy file was moved or deleted concurrently or something else happened.\n                    // Upgrading old files is not crucial, so we can just move on.\n                }\n            }\n        }\n\n        Directory.CreateDirectory(workingDirPath);\n\n        await Http.ResiliencePipeline.ExecuteAsync(\n            async innerCancellationToken =>\n            {\n                // Download the file\n                using var response = await Http.Client.GetAsync(url, innerCancellationToken);\n                await using var output = File.Create(filePath);\n                await response.Content.CopyToAsync(output, innerCancellationToken);\n            },\n            cancellationToken\n        );\n\n        return _previousPathsByUrl[url] = filePath;\n    }\n}\n\ninternal partial class ExportAssetDownloader\n{\n    private static string NormalizeUrl(string url)\n    {\n        // Remove signature parameters from Discord CDN URLs to normalize them\n        var uri = new Uri(url);\n        if (!string.Equals(uri.Host, \"cdn.discordapp.com\", StringComparison.OrdinalIgnoreCase))\n            return url;\n\n        var query = HttpUtility.ParseQueryString(uri.Query);\n        query.Remove(\"ex\");\n        query.Remove(\"is\");\n        query.Remove(\"hm\");\n\n        return uri.GetLeftPart(UriPartial.Path) + query;\n    }\n\n    private static string GetFileNameFromUrl(string url, string urlHash)\n    {\n        // Try to extract the file name from URL\n        var fileName = Regex.Match(url, @\".+/([^?]*)\").Groups[1].Value;\n\n        // If it's not there, just use the URL hash as the file name\n        if (string.IsNullOrWhiteSpace(fileName))\n            return urlHash;\n\n        // Otherwise, use the original file name but inject the hash in the middle\n        var fileNameWithoutExtension = Path.GetFileNameWithoutExtension(fileName);\n        var fileExtension = Path.GetExtension(fileName);\n\n        // Probably not a file extension, just a dot in a long file name\n        // https://github.com/Tyrrrz/DiscordChatExporter/pull/812\n        if (fileExtension.Length > 41)\n        {\n            fileNameWithoutExtension = fileName;\n            fileExtension = \"\";\n        }\n\n        return Path.EscapeFileName(\n            fileNameWithoutExtension.Truncate(42) + '-' + urlHash + fileExtension\n        );\n    }\n\n    private static string GetFileNameFromUrl(string url) =>\n        GetFileNameFromUrl(\n            url,\n            // 16 chars = 64 bits, reaches 1% collision probability at ~609 million files\n            SHA256\n                .HashData(Encoding.UTF8.GetBytes(NormalizeUrl(url)))\n                .Pipe(Convert.ToHexStringLower)\n                .Truncate(16)\n        );\n\n    // Legacy naming used a 5-char hash, kept for backwards compatibility with existing exports\n    private static string GetLegacyFileNameFromUrl(string url) =>\n        GetFileNameFromUrl(\n            url,\n            SHA256\n                .HashData(Encoding.UTF8.GetBytes(NormalizeUrl(url)))\n                .Pipe(Convert.ToHexStringLower)\n                // 5 chars = 20 bits, reaches 1% collision probability at ~145 files\n                .Truncate(5)\n        );\n}\n"
  },
  {
    "path": "DiscordChatExporter.Core/Exporting/ExportContext.cs",
    "content": "﻿using System;\nusing System.Collections.Generic;\nusing System.Drawing;\nusing System.IO;\nusing System.Linq;\nusing System.Net.Http;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing DiscordChatExporter.Core.Discord;\nusing DiscordChatExporter.Core.Discord.Data;\nusing DiscordChatExporter.Core.Utils;\nusing DiscordChatExporter.Core.Utils.Extensions;\n\nnamespace DiscordChatExporter.Core.Exporting;\n\ninternal class ExportContext(DiscordClient discord, ExportRequest request)\n{\n    private readonly Dictionary<Snowflake, Member?> _membersById = new();\n    private readonly Dictionary<Snowflake, Channel?> _channelsById = new();\n    private readonly Dictionary<Snowflake, Role> _rolesById = new();\n\n    private readonly ExportAssetDownloader _assetDownloader = new(\n        request.AssetsDirPath,\n        request.ShouldReuseAssets\n    );\n\n    public DiscordClient Discord { get; } = discord;\n\n    public ExportRequest Request { get; } = request;\n\n    public DateTimeOffset NormalizeDate(DateTimeOffset instant) =>\n        Request.IsUtcNormalizationEnabled ? instant.ToUniversalTime() : instant.ToLocalTime();\n\n    public string FormatDate(DateTimeOffset instant, string format = \"g\") =>\n        NormalizeDate(instant).ToString(format, Request.CultureInfo);\n\n    public async ValueTask PopulateChannelsAndRolesAsync(\n        CancellationToken cancellationToken = default\n    )\n    {\n        await foreach (\n            var channel in Discord.GetGuildChannelsAsync(Request.Guild.Id, cancellationToken)\n        )\n        {\n            _channelsById[channel.Id] = channel;\n        }\n\n        await foreach (var role in Discord.GetGuildRolesAsync(Request.Guild.Id, cancellationToken))\n        {\n            _rolesById[role.Id] = role;\n        }\n    }\n\n    // Threads are not preloaded, so we resolve them on demand\n    public async ValueTask PopulateChannelAsync(\n        Snowflake id,\n        CancellationToken cancellationToken = default\n    )\n    {\n        if (_channelsById.ContainsKey(id))\n            return;\n\n        var channel = await Discord.TryGetChannelAsync(id, cancellationToken);\n\n        // Store the result even if it's null, to avoid re-fetching non-existing channels\n        _channelsById[id] = channel;\n    }\n\n    // Because members cannot be pulled in bulk, we need to populate them on demand\n    private async ValueTask PopulateMemberAsync(\n        Snowflake id,\n        User? fallbackUser,\n        CancellationToken cancellationToken = default\n    )\n    {\n        if (_membersById.ContainsKey(id))\n            return;\n\n        var member = await Discord.TryGetGuildMemberAsync(Request.Guild.Id, id, cancellationToken);\n\n        // User may have left the guild since they were mentioned.\n        // Create a dummy member object based on the user info.\n        if (member is null)\n        {\n            var user = fallbackUser ?? await Discord.TryGetUserAsync(id, cancellationToken);\n\n            // User may have been deleted since they were mentioned\n            if (user is not null)\n                member = Member.CreateFallback(user);\n        }\n\n        // Store the result even if it's null, to avoid re-fetching non-existing members\n        _membersById[id] = member;\n    }\n\n    public async ValueTask PopulateMemberAsync(\n        Snowflake id,\n        CancellationToken cancellationToken = default\n    ) => await PopulateMemberAsync(id, null, cancellationToken);\n\n    public async ValueTask PopulateMemberAsync(\n        User user,\n        CancellationToken cancellationToken = default\n    ) => await PopulateMemberAsync(user.Id, user, cancellationToken);\n\n    public Member? TryGetMember(Snowflake id) => _membersById.GetValueOrDefault(id);\n\n    public Channel? TryGetChannel(Snowflake id) => _channelsById.GetValueOrDefault(id);\n\n    public Role? TryGetRole(Snowflake id) => _rolesById.GetValueOrDefault(id);\n\n    public IReadOnlyList<Role> GetUserRoles(Snowflake id) =>\n        TryGetMember(id)\n            ?.RoleIds.Select(TryGetRole)\n            .WhereNotNull()\n            .OrderByDescending(r => r.Position)\n            .ToArray()\n        ?? [];\n\n    public Color? TryGetUserColor(Snowflake id) =>\n        GetUserRoles(id).Where(r => r.Color is not null).Select(r => r.Color).FirstOrDefault();\n\n    public async ValueTask<string> ResolveAssetUrlAsync(\n        string url,\n        CancellationToken cancellationToken = default\n    )\n    {\n        if (!Request.ShouldDownloadAssets)\n            return url;\n\n        try\n        {\n            var filePath = await _assetDownloader.DownloadAsync(url, cancellationToken);\n            var relativeFilePath = Path.GetRelativePath(Request.OutputDirPath, filePath);\n\n            // Prefer the relative path so that the export package can be copied around without breaking references.\n            // However, if the assets directory lies outside the export directory, use the absolute path instead.\n            var shouldUseAbsoluteFilePath =\n                relativeFilePath.StartsWith(\n                    \"..\" + Path.DirectorySeparatorChar,\n                    StringComparison.Ordinal\n                )\n                || relativeFilePath.StartsWith(\n                    \"..\" + Path.AltDirectorySeparatorChar,\n                    StringComparison.Ordinal\n                );\n\n            var optimalFilePath = shouldUseAbsoluteFilePath ? filePath : relativeFilePath;\n\n            // For HTML, the path needs to be properly formatted\n            if (Request.Format is ExportFormat.HtmlDark or ExportFormat.HtmlLight)\n                return Url.EncodeFilePath(optimalFilePath);\n\n            return optimalFilePath;\n        }\n        // Try to catch only exceptions related to failed HTTP requests\n        // https://github.com/Tyrrrz/DiscordChatExporter/issues/332\n        // https://github.com/Tyrrrz/DiscordChatExporter/issues/372\n        catch (Exception ex) when (ex is HttpRequestException or OperationCanceledException)\n        {\n            // We don't want this to crash the exporting process in case of failure.\n            // TODO: add logging so we can be more liberal with catching exceptions.\n            return url;\n        }\n    }\n}\n"
  },
  {
    "path": "DiscordChatExporter.Core/Exporting/ExportFormat.cs",
    "content": "﻿using System;\n\nnamespace DiscordChatExporter.Core.Exporting;\n\npublic enum ExportFormat\n{\n    PlainText,\n    HtmlDark,\n    HtmlLight,\n    Csv,\n    Json,\n}\n\npublic static class ExportFormatExtensions\n{\n    extension(ExportFormat format)\n    {\n        public string GetFileExtension() =>\n            format switch\n            {\n                ExportFormat.PlainText => \"txt\",\n                ExportFormat.HtmlDark => \"html\",\n                ExportFormat.HtmlLight => \"html\",\n                ExportFormat.Csv => \"csv\",\n                ExportFormat.Json => \"json\",\n                _ => throw new ArgumentOutOfRangeException(nameof(format)),\n            };\n\n        public string GetDisplayName() =>\n            format switch\n            {\n                ExportFormat.PlainText => \"TXT\",\n                ExportFormat.HtmlDark => \"HTML (Dark)\",\n                ExportFormat.HtmlLight => \"HTML (Light)\",\n                ExportFormat.Csv => \"CSV\",\n                ExportFormat.Json => \"JSON\",\n                _ => throw new ArgumentOutOfRangeException(nameof(format)),\n            };\n    }\n}\n"
  },
  {
    "path": "DiscordChatExporter.Core/Exporting/ExportRequest.cs",
    "content": "﻿using System;\nusing System.Globalization;\nusing System.IO;\nusing System.Text;\nusing System.Text.RegularExpressions;\nusing DiscordChatExporter.Core.Discord;\nusing DiscordChatExporter.Core.Discord.Data;\nusing DiscordChatExporter.Core.Exporting.Filtering;\nusing DiscordChatExporter.Core.Exporting.Partitioning;\nusing DiscordChatExporter.Core.Utils.Extensions;\n\nnamespace DiscordChatExporter.Core.Exporting;\n\npublic partial class ExportRequest\n{\n    public Guild Guild { get; }\n\n    public Channel Channel { get; }\n\n    public string OutputFilePath { get; }\n\n    public string OutputDirPath { get; }\n\n    public string AssetsDirPath { get; }\n\n    public ExportFormat Format { get; }\n\n    public Snowflake? After { get; }\n\n    public Snowflake? Before { get; }\n\n    public PartitionLimit PartitionLimit { get; }\n\n    public MessageFilter MessageFilter { get; }\n\n    public bool IsReverseMessageOrder { get; }\n\n    public bool ShouldFormatMarkdown { get; }\n\n    public bool ShouldDownloadAssets { get; }\n\n    public bool ShouldReuseAssets { get; }\n\n    public string? Locale { get; }\n\n    public CultureInfo? CultureInfo { get; }\n\n    public bool IsUtcNormalizationEnabled { get; }\n\n    public ExportRequest(\n        Guild guild,\n        Channel channel,\n        string outputPath,\n        string? assetsDirPath,\n        ExportFormat format,\n        Snowflake? after,\n        Snowflake? before,\n        PartitionLimit partitionLimit,\n        MessageFilter messageFilter,\n        bool isReverseMessageOrder,\n        bool shouldFormatMarkdown,\n        bool shouldDownloadAssets,\n        bool shouldReuseAssets,\n        string? locale,\n        bool isUtcNormalizationEnabled\n    )\n    {\n        Guild = guild;\n        Channel = channel;\n        Format = format;\n        After = after;\n        Before = before;\n        PartitionLimit = partitionLimit;\n        MessageFilter = messageFilter;\n        IsReverseMessageOrder = isReverseMessageOrder;\n        ShouldFormatMarkdown = shouldFormatMarkdown;\n        ShouldDownloadAssets = shouldDownloadAssets;\n        ShouldReuseAssets = shouldReuseAssets;\n        Locale = locale;\n        IsUtcNormalizationEnabled = isUtcNormalizationEnabled;\n\n        OutputFilePath = GetOutputBaseFilePath(Guild, Channel, outputPath, Format, After, Before);\n\n        OutputDirPath = Path.GetDirectoryName(OutputFilePath)!;\n\n        AssetsDirPath = !string.IsNullOrWhiteSpace(assetsDirPath)\n            ? FormatPath(assetsDirPath, Guild, Channel, After, Before)\n            : $\"{OutputFilePath}_Files{Path.DirectorySeparatorChar}\";\n\n        CultureInfo = Locale?.Pipe(CultureInfo.GetCultureInfo);\n    }\n}\n\npublic partial class ExportRequest\n{\n    public static string GetDefaultOutputFileName(\n        Guild guild,\n        Channel channel,\n        ExportFormat format,\n        Snowflake? after = null,\n        Snowflake? before = null\n    )\n    {\n        var buffer = new StringBuilder();\n\n        // Guild name\n        buffer.Append(guild.Name);\n\n        // Parent name\n        if (channel.Parent is not null)\n            buffer.Append(\" - \").Append(channel.Parent.Name);\n\n        // Channel name and ID\n        buffer\n            .Append(\" - \")\n            .Append(channel.Name)\n            .Append(' ')\n            .Append('[')\n            .Append(channel.Id)\n            .Append(']');\n\n        // Date range\n        if (after is not null || before is not null)\n        {\n            buffer.Append(' ').Append('(');\n\n            // Both 'after' and 'before' are set\n            if (after is not null && before is not null)\n            {\n                buffer.Append(\n                    $\"{after.Value.ToDate():yyyy-MM-dd} to {before.Value.ToDate():yyyy-MM-dd}\"\n                );\n            }\n            // Only 'after' is set\n            else if (after is not null)\n            {\n                buffer.Append($\"after {after.Value.ToDate():yyyy-MM-dd}\");\n            }\n            // Only 'before' is set\n            else if (before is not null)\n            {\n                buffer.Append($\"before {before.Value.ToDate():yyyy-MM-dd}\");\n            }\n\n            buffer.Append(')');\n        }\n\n        // File extension\n        buffer.Append('.').Append(format.GetFileExtension());\n\n        return Path.EscapeFileName(buffer.ToString());\n    }\n\n    private static string FormatPath(\n        string path,\n        Guild guild,\n        Channel channel,\n        Snowflake? after,\n        Snowflake? before\n    ) =>\n        Regex.Replace(\n            path,\n            \"%.\",\n            m =>\n                Path.EscapeFileName(\n                    m.Value switch\n                    {\n                        \"%g\" => guild.Id.ToString(),\n                        \"%G\" => guild.Name,\n\n                        \"%t\" => channel.Parent?.Id.ToString() ?? \"\",\n                        \"%T\" => channel.Parent?.Name ?? \"\",\n\n                        \"%c\" => channel.Id.ToString(),\n                        \"%C\" => channel.Name,\n\n                        \"%p\" => channel.Position?.ToString(CultureInfo.InvariantCulture) ?? \"0\",\n                        \"%P\" => channel.Parent?.Position?.ToString(CultureInfo.InvariantCulture)\n                            ?? \"0\",\n\n                        \"%a\" => after?.ToDate().ToString(\"yyyy-MM-dd\", CultureInfo.InvariantCulture)\n                            ?? \"\",\n                        \"%b\" => before\n                            ?.ToDate()\n                            .ToString(\"yyyy-MM-dd\", CultureInfo.InvariantCulture)\n                            ?? \"\",\n                        \"%d\" => DateTimeOffset.Now.ToString(\n                            \"yyyy-MM-dd\",\n                            CultureInfo.InvariantCulture\n                        ),\n\n                        \"%%\" => \"%\",\n                        _ => m.Value,\n                    }\n                )\n        );\n\n    private static string GetOutputBaseFilePath(\n        Guild guild,\n        Channel channel,\n        string outputPath,\n        ExportFormat format,\n        Snowflake? after = null,\n        Snowflake? before = null\n    )\n    {\n        var actualOutputPath = FormatPath(outputPath, guild, channel, after, before);\n\n        // Output is a directory\n        if (\n            Directory.Exists(actualOutputPath)\n            || string.IsNullOrWhiteSpace(Path.GetExtension(actualOutputPath))\n        )\n        {\n            var fileName = GetDefaultOutputFileName(guild, channel, format, after, before);\n            return Path.Combine(actualOutputPath, fileName);\n        }\n\n        // Output is a file\n        return actualOutputPath;\n    }\n}\n"
  },
  {
    "path": "DiscordChatExporter.Core/Exporting/Filtering/BinaryExpressionKind.cs",
    "content": "﻿namespace DiscordChatExporter.Core.Exporting.Filtering;\n\ninternal enum BinaryExpressionKind\n{\n    Or,\n    And,\n}\n"
  },
  {
    "path": "DiscordChatExporter.Core/Exporting/Filtering/BinaryExpressionMessageFilter.cs",
    "content": "﻿using System;\nusing DiscordChatExporter.Core.Discord.Data;\n\nnamespace DiscordChatExporter.Core.Exporting.Filtering;\n\ninternal class BinaryExpressionMessageFilter(\n    MessageFilter first,\n    MessageFilter second,\n    BinaryExpressionKind kind\n) : MessageFilter\n{\n    public override bool IsMatch(Message message) =>\n        kind switch\n        {\n            BinaryExpressionKind.Or => first.IsMatch(message) || second.IsMatch(message),\n            BinaryExpressionKind.And => first.IsMatch(message) && second.IsMatch(message),\n            _ => throw new InvalidOperationException($\"Unknown binary expression kind '{kind}'.\"),\n        };\n}\n"
  },
  {
    "path": "DiscordChatExporter.Core/Exporting/Filtering/ContainsMessageFilter.cs",
    "content": "﻿using System.Linq;\nusing System.Text.RegularExpressions;\nusing DiscordChatExporter.Core.Discord.Data;\n\nnamespace DiscordChatExporter.Core.Exporting.Filtering;\n\ninternal class ContainsMessageFilter(string text) : MessageFilter\n{\n    // Match content within word boundaries, between spaces, or as the whole input.\n    // For example, \"max\" shouldn't match on content \"our maximum effort\",\n    // but should match on content \"our max effort\".\n    // Also, \"(max)\" should match on content \"our (max) effort\", even though\n    // parentheses are not considered word characters.\n    // https://github.com/Tyrrrz/DiscordChatExporter/issues/909\n    private bool IsMatch(string? content) =>\n        !string.IsNullOrWhiteSpace(content)\n        && Regex.IsMatch(\n            content,\n            @\"(?:\\b|\\s|^)\" + Regex.Escape(text) + @\"(?:\\b|\\s|$)\",\n            RegexOptions.IgnoreCase | RegexOptions.CultureInvariant\n        );\n\n    public override bool IsMatch(Message message) =>\n        IsMatch(message.Content)\n        || message.Embeds.Any(e =>\n            IsMatch(e.Title)\n            || IsMatch(e.Author?.Name)\n            || IsMatch(e.Description)\n            || IsMatch(e.Footer?.Text)\n            || e.Fields.Any(f => IsMatch(f.Name) || IsMatch(f.Value))\n        );\n}\n"
  },
  {
    "path": "DiscordChatExporter.Core/Exporting/Filtering/FromMessageFilter.cs",
    "content": "﻿using System;\nusing DiscordChatExporter.Core.Discord.Data;\n\nnamespace DiscordChatExporter.Core.Exporting.Filtering;\n\ninternal class FromMessageFilter(string value) : MessageFilter\n{\n    public override bool IsMatch(Message message) =>\n        string.Equals(value, message.Author.Name, StringComparison.OrdinalIgnoreCase)\n        || string.Equals(value, message.Author.DisplayName, StringComparison.OrdinalIgnoreCase)\n        || string.Equals(value, message.Author.FullName, StringComparison.OrdinalIgnoreCase)\n        || string.Equals(value, message.Author.Id.ToString(), StringComparison.OrdinalIgnoreCase);\n}\n"
  },
  {
    "path": "DiscordChatExporter.Core/Exporting/Filtering/HasMessageFilter.cs",
    "content": "﻿using System;\nusing System.Linq;\nusing DiscordChatExporter.Core.Discord.Data;\nusing DiscordChatExporter.Core.Markdown.Parsing;\n\nnamespace DiscordChatExporter.Core.Exporting.Filtering;\n\ninternal class HasMessageFilter(MessageContentMatchKind kind) : MessageFilter\n{\n    public override bool IsMatch(Message message) =>\n        kind switch\n        {\n            MessageContentMatchKind.Link => MarkdownParser.ExtractLinks(message.Content).Any(),\n            MessageContentMatchKind.Embed => message.Embeds.Any(),\n            MessageContentMatchKind.File => message.Attachments.Any(),\n            MessageContentMatchKind.Video => message.Attachments.Any(file => file.IsVideo),\n            MessageContentMatchKind.Image => message.Attachments.Any(file => file.IsImage),\n            MessageContentMatchKind.Sound => message.Attachments.Any(file => file.IsAudio),\n            MessageContentMatchKind.Pin => message.IsPinned,\n            MessageContentMatchKind.Invite => MarkdownParser\n                .ExtractLinks(message.Content)\n                .Select(l => l.Url)\n                .Select(Invite.TryGetCodeFromUrl)\n                .Any(c => !string.IsNullOrWhiteSpace(c)),\n            _ => throw new InvalidOperationException(\n                $\"Unknown message content match kind '{kind}'.\"\n            ),\n        };\n}\n"
  },
  {
    "path": "DiscordChatExporter.Core/Exporting/Filtering/MentionsMessageFilter.cs",
    "content": "﻿using System;\nusing System.Linq;\nusing DiscordChatExporter.Core.Discord.Data;\n\nnamespace DiscordChatExporter.Core.Exporting.Filtering;\n\ninternal class MentionsMessageFilter(string value) : MessageFilter\n{\n    public override bool IsMatch(Message message) =>\n        message.MentionedUsers.Any(user =>\n            string.Equals(value, user.Name, StringComparison.OrdinalIgnoreCase)\n            || string.Equals(value, user.DisplayName, StringComparison.OrdinalIgnoreCase)\n            || string.Equals(value, user.FullName, StringComparison.OrdinalIgnoreCase)\n            || string.Equals(value, user.Id.ToString(), StringComparison.OrdinalIgnoreCase)\n        );\n}\n"
  },
  {
    "path": "DiscordChatExporter.Core/Exporting/Filtering/MessageContentMatchKind.cs",
    "content": "﻿namespace DiscordChatExporter.Core.Exporting.Filtering;\n\ninternal enum MessageContentMatchKind\n{\n    Link,\n    Embed,\n    File,\n    Video,\n    Image,\n    Sound,\n    Pin,\n    Invite,\n}\n"
  },
  {
    "path": "DiscordChatExporter.Core/Exporting/Filtering/MessageFilter.cs",
    "content": "﻿using DiscordChatExporter.Core.Discord.Data;\nusing DiscordChatExporter.Core.Exporting.Filtering.Parsing;\nusing Superpower;\n\nnamespace DiscordChatExporter.Core.Exporting.Filtering;\n\npublic abstract partial class MessageFilter\n{\n    public abstract bool IsMatch(Message message);\n}\n\npublic partial class MessageFilter\n{\n    public static MessageFilter Null { get; } = new NullMessageFilter();\n\n    public static MessageFilter Parse(string value) => FilterGrammar.Filter.Parse(value);\n}\n"
  },
  {
    "path": "DiscordChatExporter.Core/Exporting/Filtering/NegatedMessageFilter.cs",
    "content": "﻿using DiscordChatExporter.Core.Discord.Data;\n\nnamespace DiscordChatExporter.Core.Exporting.Filtering;\n\ninternal class NegatedMessageFilter(MessageFilter filter) : MessageFilter\n{\n    public override bool IsMatch(Message message) => !filter.IsMatch(message);\n}\n"
  },
  {
    "path": "DiscordChatExporter.Core/Exporting/Filtering/NullMessageFilter.cs",
    "content": "﻿using DiscordChatExporter.Core.Discord.Data;\n\nnamespace DiscordChatExporter.Core.Exporting.Filtering;\n\ninternal class NullMessageFilter : MessageFilter\n{\n    public override bool IsMatch(Message message) => true;\n}\n"
  },
  {
    "path": "DiscordChatExporter.Core/Exporting/Filtering/Parsing/FilterGrammar.cs",
    "content": "﻿using DiscordChatExporter.Core.Utils.Extensions;\nusing Superpower;\nusing Superpower.Parsers;\n\nnamespace DiscordChatExporter.Core.Exporting.Filtering.Parsing;\n\ninternal static class FilterGrammar\n{\n    private static readonly TextParser<char> EscapedCharacter = Character\n        .EqualTo('\\\\')\n        .IgnoreThen(Character.AnyChar);\n\n    private static readonly TextParser<string> QuotedString =\n        from open in Character.In('\"', '\\'')\n        from value in Parse.OneOf(EscapedCharacter, Character.Except(open)).Many().Text()\n        from close in Character.EqualTo(open)\n        select value;\n\n    private static readonly TextParser<string> UnquotedString = Parse\n        .OneOf(\n            EscapedCharacter,\n            // Avoid whitespace as it's treated as an implicit 'and' operator.\n            // Also avoid all special tokens used by other parsers.\n            Character.ExceptIn(' ', '(', ')', '\"', '\\'', '-', '~', '|', '&')\n        )\n        .AtLeastOnce()\n        .Text();\n\n    private static readonly TextParser<string> String = Parse\n        .OneOf(QuotedString, UnquotedString)\n        .Named(\"text string\");\n\n    private static readonly TextParser<MessageFilter> ContainsFilter = String.Select(v =>\n        (MessageFilter)new ContainsMessageFilter(v)\n    );\n\n    private static readonly TextParser<MessageFilter> FromFilter = Span.EqualToIgnoreCase(\"from:\")\n        .Try()\n        .IgnoreThen(String)\n        .Select(v => (MessageFilter)new FromMessageFilter(v))\n        .Named(\"from:<value>\");\n\n    private static readonly TextParser<MessageFilter> MentionsFilter = Span.EqualToIgnoreCase(\n            \"mentions:\"\n        )\n        .Try()\n        .IgnoreThen(String)\n        .Select(v => (MessageFilter)new MentionsMessageFilter(v))\n        .Named(\"mentions:<value>\");\n\n    private static readonly TextParser<MessageFilter> ReactionFilter = Span.EqualToIgnoreCase(\n            \"reaction:\"\n        )\n        .Try()\n        .IgnoreThen(String)\n        .Select(v => (MessageFilter)new ReactionMessageFilter(v))\n        .Named(\"reaction:<value>\");\n\n    private static readonly TextParser<MessageFilter> HasFilter = Span.EqualToIgnoreCase(\"has:\")\n        .Try()\n        .IgnoreThen(\n            Parse.OneOf(\n                Span.EqualToIgnoreCase(\"link\")\n                    .IgnoreThen(Parse.Return(MessageContentMatchKind.Link))\n                    .Try(),\n                Span.EqualToIgnoreCase(\"embed\")\n                    .IgnoreThen(Parse.Return(MessageContentMatchKind.Embed))\n                    .Try(),\n                Span.EqualToIgnoreCase(\"file\")\n                    .IgnoreThen(Parse.Return(MessageContentMatchKind.File))\n                    .Try(),\n                Span.EqualToIgnoreCase(\"video\")\n                    .IgnoreThen(Parse.Return(MessageContentMatchKind.Video))\n                    .Try(),\n                Span.EqualToIgnoreCase(\"image\")\n                    .IgnoreThen(Parse.Return(MessageContentMatchKind.Image))\n                    .Try(),\n                Span.EqualToIgnoreCase(\"sound\")\n                    .IgnoreThen(Parse.Return(MessageContentMatchKind.Sound)),\n                Span.EqualToIgnoreCase(\"pin\")\n                    .IgnoreThen(Parse.Return(MessageContentMatchKind.Pin))\n                    .Try(),\n                Span.EqualToIgnoreCase(\"invite\")\n                    .IgnoreThen(Parse.Return(MessageContentMatchKind.Invite))\n                    .Try()\n            )\n        )\n        .Select(k => (MessageFilter)new HasMessageFilter(k))\n        .Named(\"has:<value>\");\n\n    // Make sure that property-based filters like 'has:link' don't prevent text like 'hello' from being parsed.\n    // https://github.com/Tyrrrz/DiscordChatExporter/issues/909#issuecomment-1227575455\n    private static readonly TextParser<MessageFilter> PrimitiveFilter = Parse.OneOf(\n        FromFilter,\n        MentionsFilter,\n        ReactionFilter,\n        HasFilter,\n        ContainsFilter\n    );\n\n    private static readonly TextParser<MessageFilter> GroupedFilter =\n        from open in Character.EqualTo('(')\n        from content in Parse.Ref(() => ChainedFilter!).Token()\n        from close in Character.EqualTo(')')\n        select content;\n\n    private static readonly TextParser<MessageFilter> NegatedFilter = Character\n        // Dash is annoying to use from CLI due to conflicts with options, so we provide tilde as an alias\n        .In('-', '~')\n        .IgnoreThen(Parse.OneOf(GroupedFilter, PrimitiveFilter))\n        .Select(f => (MessageFilter)new NegatedMessageFilter(f));\n\n    private static readonly TextParser<MessageFilter> ChainedFilter = Parse.Chain(\n        // Operator\n        Parse.OneOf(\n            // Explicit operator\n            Character.In('|', '&').Token().Try(),\n            // Implicit operator (resolves to 'and')\n            Character.EqualTo(' ').AtLeastOnce().IgnoreThen(Parse.Return(' '))\n        ),\n        // Operand\n        Parse.OneOf(NegatedFilter, GroupedFilter, PrimitiveFilter),\n        // Reducer\n        (op, left, right) =>\n            op switch\n            {\n                '|' => new BinaryExpressionMessageFilter(left, right, BinaryExpressionKind.Or),\n                _ => new BinaryExpressionMessageFilter(left, right, BinaryExpressionKind.And),\n            }\n    );\n\n    public static readonly TextParser<MessageFilter> Filter = ChainedFilter.Token().AtEnd();\n}\n"
  },
  {
    "path": "DiscordChatExporter.Core/Exporting/Filtering/ReactionMessageFilter.cs",
    "content": "﻿using System;\nusing System.Linq;\nusing DiscordChatExporter.Core.Discord.Data;\n\nnamespace DiscordChatExporter.Core.Exporting.Filtering;\n\ninternal class ReactionMessageFilter(string value) : MessageFilter\n{\n    public override bool IsMatch(Message message) =>\n        message.Reactions.Any(r =>\n            string.Equals(value, r.Emoji.Id?.ToString(), StringComparison.OrdinalIgnoreCase)\n            || string.Equals(value, r.Emoji.Name, StringComparison.OrdinalIgnoreCase)\n            || string.Equals(value, r.Emoji.Code, StringComparison.OrdinalIgnoreCase)\n        );\n}\n"
  },
  {
    "path": "DiscordChatExporter.Core/Exporting/HtmlMarkdownVisitor.cs",
    "content": "﻿using System;\nusing System.Linq;\nusing System.Net;\nusing System.Text;\nusing System.Text.RegularExpressions;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing DiscordChatExporter.Core.Markdown;\nusing DiscordChatExporter.Core.Markdown.Parsing;\nusing DiscordChatExporter.Core.Utils.Extensions;\n\nnamespace DiscordChatExporter.Core.Exporting;\n\ninternal partial class HtmlMarkdownVisitor(\n    ExportContext context,\n    StringBuilder buffer,\n    bool isJumbo\n) : MarkdownVisitor\n{\n    protected override ValueTask VisitTextAsync(\n        TextNode text,\n        CancellationToken cancellationToken = default\n    )\n    {\n        buffer.Append(HtmlEncode(text.Text));\n        return default;\n    }\n\n    protected override async ValueTask VisitFormattingAsync(\n        FormattingNode formatting,\n        CancellationToken cancellationToken = default\n    )\n    {\n        var (openingTag, closingTag) = formatting.Kind switch\n        {\n            FormattingKind.Bold => (\n                // lang=html\n                \"<strong>\",\n                // lang=html\n                \"</strong>\"\n            ),\n\n            FormattingKind.Italic => (\n                // lang=html\n                \"<em>\",\n                // lang=html\n                \"</em>\"\n            ),\n\n            FormattingKind.Underline => (\n                // lang=html\n                \"<u>\",\n                // lang=html\n                \"</u>\"\n            ),\n\n            FormattingKind.Strikethrough => (\n                // lang=html\n                \"<s>\",\n                // lang=html\n                \"</s>\"\n            ),\n\n            FormattingKind.Spoiler => (\n                // lang=html\n                \"\"\"<span class=\"chatlog__markdown-spoiler chatlog__markdown-spoiler--hidden\" onclick=\"showSpoiler(event, this)\">\"\"\",\n                // lang=html\n                \"\"\"</span>\"\"\"\n            ),\n\n            FormattingKind.Quote => (\n                // lang=html\n                \"\"\"<div class=\"chatlog__markdown-quote\"><div class=\"chatlog__markdown-quote-border\"></div><div class=\"chatlog__markdown-quote-content\">\"\"\",\n                // lang=html\n                \"\"\"</div></div>\"\"\"\n            ),\n\n            _ => throw new InvalidOperationException(\n                $\"Unknown formatting kind '{formatting.Kind}'.\"\n            ),\n        };\n\n        buffer.Append(openingTag);\n        await VisitAsync(formatting.Children, cancellationToken);\n        buffer.Append(closingTag);\n    }\n\n    protected override async ValueTask VisitHeadingAsync(\n        HeadingNode heading,\n        CancellationToken cancellationToken = default\n    )\n    {\n        buffer.Append(\n            // lang=html\n            $\"<h{heading.Level}>\"\n        );\n\n        await VisitAsync(heading.Children, cancellationToken);\n\n        buffer.Append(\n            // lang=html\n            $\"</h{heading.Level}>\"\n        );\n    }\n\n    protected override async ValueTask VisitListAsync(\n        ListNode list,\n        CancellationToken cancellationToken = default\n    )\n    {\n        buffer.Append(\n            // lang=html\n            \"<ul>\"\n        );\n\n        await VisitAsync(list.Items, cancellationToken);\n\n        buffer.Append(\n            // lang=html\n            \"</ul>\"\n        );\n    }\n\n    protected override async ValueTask VisitListItemAsync(\n        ListItemNode listItem,\n        CancellationToken cancellationToken = default\n    )\n    {\n        buffer.Append(\n            // lang=html\n            \"<li>\"\n        );\n\n        await VisitAsync(listItem.Children, cancellationToken);\n\n        buffer.Append(\n            // lang=html\n            \"</li>\"\n        );\n    }\n\n    protected override ValueTask VisitInlineCodeBlockAsync(\n        InlineCodeBlockNode inlineCodeBlock,\n        CancellationToken cancellationToken = default\n    )\n    {\n        buffer.Append(\n            // lang=html\n            $\"\"\"\n            <code class=\"chatlog__markdown-pre chatlog__markdown-pre--inline\">{HtmlEncode(\n                inlineCodeBlock.Code\n            )}</code>\n            \"\"\"\n        );\n\n        return default;\n    }\n\n    protected override ValueTask VisitMultiLineCodeBlockAsync(\n        MultiLineCodeBlockNode multiLineCodeBlock,\n        CancellationToken cancellationToken = default\n    )\n    {\n        var highlightClass = !string.IsNullOrWhiteSpace(multiLineCodeBlock.Language)\n            ? $\"language-{multiLineCodeBlock.Language}\"\n            : \"nohighlight\";\n\n        buffer.Append(\n            // lang=html\n            $\"\"\"\n            <code class=\"chatlog__markdown-pre chatlog__markdown-pre--multiline {highlightClass}\">{HtmlEncode(\n                multiLineCodeBlock.Code\n            )}</code>\n            \"\"\"\n        );\n\n        return default;\n    }\n\n    protected override async ValueTask VisitLinkAsync(\n        LinkNode link,\n        CancellationToken cancellationToken = default\n    )\n    {\n        // Try to extract the message ID if the link points to a Discord message\n        var linkedMessageId = Regex\n            .Match(link.Url, @\"^https?://(?:discord|discordapp)\\.com/channels/.*?/(\\d+)/?$\")\n            .Groups[1]\n            .Value;\n\n        buffer.Append(\n            !string.IsNullOrWhiteSpace(linkedMessageId)\n                // lang=html\n                ? $\"\"\"<a href=\"{HtmlEncode(link.Url)}\" onclick=\"scrollToMessage(event, '{linkedMessageId}')\">\"\"\"\n                // lang=html\n                : $\"\"\"<a href=\"{HtmlEncode(link.Url)}\">\"\"\"\n        );\n\n        await VisitAsync(link.Children, cancellationToken);\n\n        buffer.Append(\n            // lang=html\n            \"</a>\"\n        );\n    }\n\n    protected override async ValueTask VisitEmojiAsync(\n        EmojiNode emoji,\n        CancellationToken cancellationToken = default\n    )\n    {\n        var jumboClass = isJumbo ? \"chatlog__emoji--large\" : \"\";\n\n        buffer.Append(\n            // lang=html\n            $\"\"\"\n            <img\n                loading=\"lazy\"\n                class=\"chatlog__emoji {jumboClass}\"\n                alt=\"{emoji.Name}\"\n                title=\"{emoji.Code}\"\n                src=\"{await context.ResolveAssetUrlAsync(emoji.ImageUrl, cancellationToken)}\">\n            \"\"\"\n        );\n    }\n\n    protected override async ValueTask VisitMentionAsync(\n        MentionNode mention,\n        CancellationToken cancellationToken = default\n    )\n    {\n        if (mention.Kind == MentionKind.Everyone)\n        {\n            buffer.Append(\n                // lang=html\n                \"\"\"\n                <span class=\"chatlog__markdown-mention\">@everyone</span>\n                \"\"\"\n            );\n        }\n        else if (mention.Kind == MentionKind.Here)\n        {\n            buffer.Append(\n                // lang=html\n                \"\"\"\n                <span class=\"chatlog__markdown-mention\">@here</span>\n                \"\"\"\n            );\n        }\n        else if (mention.Kind == MentionKind.User)\n        {\n            // User mentions are not always included in the message object,\n            // which means they need to be populated on demand.\n            // https://github.com/Tyrrrz/DiscordChatExporter/issues/304\n            if (mention.TargetId is not null)\n                await context.PopulateMemberAsync(mention.TargetId.Value, cancellationToken);\n\n            var member = mention.TargetId?.Pipe(context.TryGetMember);\n            var fullName = member?.User.FullName ?? \"Unknown\";\n            var displayName = member?.DisplayName ?? member?.User.DisplayName ?? \"Unknown\";\n\n            buffer.Append(\n                // lang=html\n                $\"\"\"\n                <span class=\"chatlog__markdown-mention\" title=\"{HtmlEncode(fullName)}\">@{HtmlEncode(\n                    displayName\n                )}</span>\n                \"\"\"\n            );\n        }\n        else if (mention.Kind == MentionKind.Channel)\n        {\n            // Channel/thread mentions may reference threads that are not preloaded,\n            // so we resolve them on demand.\n            // https://github.com/Tyrrrz/DiscordChatExporter/issues/1261\n            if (mention.TargetId is not null)\n                await context.PopulateChannelAsync(mention.TargetId.Value, cancellationToken);\n\n            var channel = mention.TargetId?.Pipe(context.TryGetChannel);\n            var symbol = channel?.IsVoice == true ? \"🔊\" : \"#\";\n            var name = channel?.Name ?? \"deleted-channel\";\n\n            buffer.Append(\n                // lang=html\n                $\"\"\"\n                <span class=\"chatlog__markdown-mention\">{symbol}{HtmlEncode(name)}</span>\n                \"\"\"\n            );\n        }\n        else if (mention.Kind == MentionKind.Role)\n        {\n            var role = mention.TargetId?.Pipe(context.TryGetRole);\n            var name = role?.Name ?? \"deleted-role\";\n            var color = role?.Color;\n\n            var style = color is not null\n                ? $\"\"\"\n                    color: rgb({color.Value.R}, {color.Value.G}, {color\n                        .Value\n                        .B}); background-color: rgba({color.Value.R}, {color.Value.G}, {color\n                        .Value\n                        .B}, 0.1);\n                    \"\"\"\n                : null;\n\n            buffer.Append(\n                // lang=html\n                $\"\"\"\n                <span class=\"chatlog__markdown-mention\" style=\"{style}\">@{HtmlEncode(name)}</span>\n                \"\"\"\n            );\n        }\n    }\n\n    protected override ValueTask VisitTimestampAsync(\n        TimestampNode timestamp,\n        CancellationToken cancellationToken = default\n    )\n    {\n        var formatted = timestamp.Instant is not null\n            ? context.FormatDate(timestamp.Instant.Value, timestamp.Format ?? \"g\")\n            : \"Invalid date\";\n\n        var formattedLong = timestamp.Instant is not null\n            ? context.FormatDate(timestamp.Instant.Value, \"f\")\n            : \"\";\n\n        buffer.Append(\n            // lang=html\n            $\"\"\"\n            <span class=\"chatlog__markdown-timestamp\" title=\"{HtmlEncode(\n                formattedLong\n            )}\">{HtmlEncode(formatted)}</span>\n            \"\"\"\n        );\n\n        return default;\n    }\n}\n\ninternal partial class HtmlMarkdownVisitor\n{\n    private static string HtmlEncode(string text) => WebUtility.HtmlEncode(text);\n\n    public static async ValueTask<string> FormatAsync(\n        ExportContext context,\n        string markdown,\n        bool isJumboAllowed = true,\n        CancellationToken cancellationToken = default\n    )\n    {\n        var nodes = MarkdownParser.Parse(markdown);\n\n        var isJumbo =\n            isJumboAllowed\n            && nodes.All(n =>\n                n is EmojiNode || n is TextNode textNode && string.IsNullOrWhiteSpace(textNode.Text)\n            );\n\n        var buffer = new StringBuilder();\n        await new HtmlMarkdownVisitor(context, buffer, isJumbo).VisitAsync(\n            nodes,\n            cancellationToken\n        );\n\n        return buffer.ToString();\n    }\n}\n"
  },
  {
    "path": "DiscordChatExporter.Core/Exporting/HtmlMessageExtensions.cs",
    "content": "using System;\nusing DiscordChatExporter.Core.Discord.Data;\nusing DiscordChatExporter.Core.Discord.Data.Embeds;\n\nnamespace DiscordChatExporter.Core.Exporting;\n\ninternal static class HtmlMessageExtensions\n{\n    // Message content is hidden if it's a link to an embedded media\n    // https://github.com/Tyrrrz/DiscordChatExporter/issues/682\n    extension(Message message)\n    {\n        public bool IsContentHidden()\n        {\n            if (message.Embeds.Count != 1)\n                return false;\n\n            var embed = message.Embeds[0];\n\n            return string.Equals(\n                    message.Content.Trim(),\n                    embed.Url,\n                    StringComparison.OrdinalIgnoreCase\n                ) && embed.Kind is EmbedKind.Image or EmbedKind.Gifv;\n        }\n    }\n}\n"
  },
  {
    "path": "DiscordChatExporter.Core/Exporting/HtmlMessageWriter.cs",
    "content": "﻿using System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Linq;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing DiscordChatExporter.Core.Discord.Data;\nusing WebMarkupMin.Core;\n\nnamespace DiscordChatExporter.Core.Exporting;\n\ninternal class HtmlMessageWriter(Stream stream, ExportContext context, string themeName)\n    : MessageWriter(stream, context)\n{\n    private readonly TextWriter _writer = new StreamWriter(stream);\n\n    private readonly HtmlMinifier _minifier = new();\n    private readonly List<Message> _messageGroup = [];\n\n    // Note: in reverse order, last message appears earlier than the first message\n    private bool CanJoinGroup(Message message)\n    {\n        // If the group is empty, any message can join it\n        if (_messageGroup.LastOrDefault() is not { } lastMessage)\n            return true;\n\n        // Reply-like messages cannot join existing groups because they need to appear first\n        if (message.IsReplyLike)\n            return false;\n\n        // Grouping for system notifications\n        if (message.IsSystemNotification)\n        {\n            // Can only be grouped with other system notifications\n            if (!lastMessage.IsSystemNotification)\n                return false;\n        }\n        // Grouping for normal messages\n        else\n        {\n            // Can only be grouped with other normal messages\n            if (lastMessage.IsSystemNotification)\n                return false;\n\n            // Messages must be within 7 minutes of each other\n            if ((message.Timestamp - lastMessage.Timestamp).Duration().TotalMinutes > 7)\n                return false;\n\n            // Messages must be sent by the same author\n            if (message.Author.Id != lastMessage.Author.Id)\n                return false;\n\n            // If the author changed their name after the last message, their new messages\n            // cannot join the existing group.\n            if (\n                !string.Equals(\n                    message.Author.FullName,\n                    lastMessage.Author.FullName,\n                    StringComparison.Ordinal\n                )\n            )\n                return false;\n        }\n\n        return true;\n    }\n\n    // Use <!--wmm:ignore--> to preserve blocks of code inside the templates\n    private string Minify(string html) => _minifier.Minify(html, false).MinifiedContent;\n\n    public override async ValueTask WritePreambleAsync(\n        CancellationToken cancellationToken = default\n    )\n    {\n        await _writer.WriteLineAsync(\n            Minify(\n                await new PreambleTemplate { Context = Context, ThemeName = themeName }.RenderAsync(\n                    cancellationToken\n                )\n            )\n        );\n    }\n\n    private async ValueTask WriteMessageGroupAsync(\n        IReadOnlyList<Message> messages,\n        CancellationToken cancellationToken = default\n    )\n    {\n        await _writer.WriteLineAsync(\n            Minify(\n                await new MessageGroupTemplate\n                {\n                    Context = Context,\n                    Messages = messages,\n                }.RenderAsync(cancellationToken)\n            )\n        );\n    }\n\n    public override async ValueTask WriteMessageAsync(\n        Message message,\n        CancellationToken cancellationToken = default\n    )\n    {\n        await base.WriteMessageAsync(message, cancellationToken);\n\n        // If the message can be grouped, buffer it for now\n        if (CanJoinGroup(message))\n        {\n            _messageGroup.Add(message);\n        }\n        // Otherwise, flush the group and render messages\n        else\n        {\n            await WriteMessageGroupAsync(_messageGroup, cancellationToken);\n\n            _messageGroup.Clear();\n            _messageGroup.Add(message);\n        }\n    }\n\n    public override async ValueTask WritePostambleAsync(\n        CancellationToken cancellationToken = default\n    )\n    {\n        // Flush current message group\n        if (_messageGroup.Any())\n            await WriteMessageGroupAsync(_messageGroup, cancellationToken);\n\n        await _writer.WriteLineAsync(\n            Minify(\n                await new PostambleTemplate\n                {\n                    Context = Context,\n                    MessagesWritten = MessagesWritten,\n                }.RenderAsync(cancellationToken)\n            )\n        );\n    }\n\n    public override async ValueTask DisposeAsync()\n    {\n        await _writer.DisposeAsync();\n        await base.DisposeAsync();\n    }\n}\n"
  },
  {
    "path": "DiscordChatExporter.Core/Exporting/JsonMessageWriter.cs",
    "content": "﻿using System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Linq;\nusing System.Text.Encodings.Web;\nusing System.Text.Json;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing DiscordChatExporter.Core.Discord.Data;\nusing DiscordChatExporter.Core.Discord.Data.Embeds;\nusing DiscordChatExporter.Core.Markdown.Parsing;\nusing DiscordChatExporter.Core.Utils.Extensions;\nusing JsonExtensions.Writing;\n\nnamespace DiscordChatExporter.Core.Exporting;\n\ninternal class JsonMessageWriter(Stream stream, ExportContext context)\n    : MessageWriter(stream, context)\n{\n    private readonly Utf8JsonWriter _writer = new(\n        stream,\n        new JsonWriterOptions\n        {\n            // https://github.com/Tyrrrz/DiscordChatExporter/issues/450\n            Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,\n            Indented = true,\n            // Validation errors may mask actual failures\n            // https://github.com/Tyrrrz/DiscordChatExporter/issues/413\n            SkipValidation = true,\n        }\n    );\n\n    private async ValueTask<string> FormatMarkdownAsync(\n        string markdown,\n        CancellationToken cancellationToken = default\n    ) =>\n        Context.Request.ShouldFormatMarkdown\n            ? await PlainTextMarkdownVisitor.FormatAsync(Context, markdown, cancellationToken)\n            : markdown;\n\n    private async ValueTask WriteUserAsync(\n        User user,\n        bool includeRoles = true,\n        CancellationToken cancellationToken = default\n    )\n    {\n        _writer.WriteStartObject();\n\n        _writer.WriteString(\"id\", user.Id.ToString());\n        _writer.WriteString(\"name\", user.Name);\n        _writer.WriteString(\"discriminator\", user.DiscriminatorFormatted);\n\n        _writer.WriteString(\n            \"nickname\",\n            Context.TryGetMember(user.Id)?.DisplayName ?? user.DisplayName\n        );\n\n        _writer.WriteString(\"color\", Context.TryGetUserColor(user.Id)?.ToHex());\n        _writer.WriteBoolean(\"isBot\", user.IsBot);\n\n        if (includeRoles)\n        {\n            _writer.WritePropertyName(\"roles\");\n            await WriteRolesAsync(Context.GetUserRoles(user.Id), cancellationToken);\n        }\n\n        _writer.WriteString(\n            \"avatarUrl\",\n            await Context.ResolveAssetUrlAsync(\n                Context.TryGetMember(user.Id)?.AvatarUrl ?? user.AvatarUrl,\n                cancellationToken\n            )\n        );\n\n        _writer.WriteEndObject();\n        await _writer.FlushAsync(cancellationToken);\n    }\n\n    private async ValueTask WriteEmojiAsync(\n        Emoji emoji,\n        CancellationToken cancellationToken = default\n    )\n    {\n        _writer.WriteStartObject();\n\n        _writer.WriteString(\"id\", emoji.Id.ToString());\n        _writer.WriteString(\"name\", emoji.Name);\n        _writer.WriteString(\"code\", emoji.Code);\n        _writer.WriteBoolean(\"isAnimated\", emoji.IsAnimated);\n        _writer.WriteString(\n            \"imageUrl\",\n            await Context.ResolveAssetUrlAsync(emoji.ImageUrl, cancellationToken)\n        );\n\n        _writer.WriteEndObject();\n        await _writer.FlushAsync(cancellationToken);\n    }\n\n    private async ValueTask WriteRolesAsync(\n        IReadOnlyList<Role> roles,\n        CancellationToken cancellationToken = default\n    )\n    {\n        _writer.WriteStartArray();\n\n        foreach (var role in roles)\n        {\n            _writer.WriteStartObject();\n\n            _writer.WriteString(\"id\", role.Id.ToString());\n            _writer.WriteString(\"name\", role.Name);\n            _writer.WriteString(\"color\", role.Color?.ToHex());\n            _writer.WriteNumber(\"position\", role.Position);\n\n            _writer.WriteEndObject();\n        }\n\n        _writer.WriteEndArray();\n        await _writer.FlushAsync(cancellationToken);\n    }\n\n    private async ValueTask WriteAttachmentAsync(\n        Attachment attachment,\n        CancellationToken cancellationToken = default\n    )\n    {\n        _writer.WriteStartObject();\n\n        _writer.WriteString(\"id\", attachment.Id.ToString());\n        _writer.WriteString(\n            \"url\",\n            await Context.ResolveAssetUrlAsync(attachment.Url, cancellationToken)\n        );\n        _writer.WriteString(\"fileName\", attachment.FileName);\n        _writer.WriteNumber(\"fileSizeBytes\", attachment.FileSize.TotalBytes);\n\n        _writer.WriteEndObject();\n    }\n\n    private async ValueTask WriteEmbedAuthorAsync(\n        EmbedAuthor embedAuthor,\n        CancellationToken cancellationToken = default\n    )\n    {\n        _writer.WriteStartObject();\n\n        _writer.WriteString(\"name\", embedAuthor.Name);\n        _writer.WriteString(\"url\", embedAuthor.Url);\n\n        if (!string.IsNullOrWhiteSpace(embedAuthor.IconUrl))\n        {\n            _writer.WriteString(\n                \"iconUrl\",\n                await Context.ResolveAssetUrlAsync(\n                    embedAuthor.IconProxyUrl ?? embedAuthor.IconUrl,\n                    cancellationToken\n                )\n            );\n\n            _writer.WriteString(\"iconCanonicalUrl\", embedAuthor.IconUrl);\n        }\n\n        _writer.WriteEndObject();\n        await _writer.FlushAsync(cancellationToken);\n    }\n\n    private async ValueTask WriteEmbedImageAsync(\n        EmbedImage embedImage,\n        CancellationToken cancellationToken = default\n    )\n    {\n        _writer.WriteStartObject();\n\n        if (!string.IsNullOrWhiteSpace(embedImage.Url))\n        {\n            _writer.WriteString(\n                \"url\",\n                await Context.ResolveAssetUrlAsync(\n                    embedImage.ProxyUrl ?? embedImage.Url,\n                    cancellationToken\n                )\n            );\n\n            _writer.WriteString(\"canonicalUrl\", embedImage.Url);\n        }\n\n        _writer.WriteNumber(\"width\", embedImage.Width);\n        _writer.WriteNumber(\"height\", embedImage.Height);\n\n        _writer.WriteEndObject();\n        await _writer.FlushAsync(cancellationToken);\n    }\n\n    private async ValueTask WriteEmbedVideoAsync(\n        EmbedVideo embedVideo,\n        CancellationToken cancellationToken = default\n    )\n    {\n        _writer.WriteStartObject();\n\n        if (!string.IsNullOrWhiteSpace(embedVideo.Url))\n        {\n            _writer.WriteString(\n                \"url\",\n                await Context.ResolveAssetUrlAsync(\n                    embedVideo.ProxyUrl ?? embedVideo.Url,\n                    cancellationToken\n                )\n            );\n\n            _writer.WriteString(\"canonicalUrl\", embedVideo.Url);\n        }\n\n        _writer.WriteNumber(\"width\", embedVideo.Width);\n        _writer.WriteNumber(\"height\", embedVideo.Height);\n\n        _writer.WriteEndObject();\n        await _writer.FlushAsync(cancellationToken);\n    }\n\n    private async ValueTask WriteEmbedFooterAsync(\n        EmbedFooter embedFooter,\n        CancellationToken cancellationToken = default\n    )\n    {\n        _writer.WriteStartObject();\n\n        _writer.WriteString(\"text\", embedFooter.Text);\n\n        if (!string.IsNullOrWhiteSpace(embedFooter.IconUrl))\n        {\n            _writer.WriteString(\n                \"iconUrl\",\n                await Context.ResolveAssetUrlAsync(\n                    embedFooter.IconProxyUrl ?? embedFooter.IconUrl,\n                    cancellationToken\n                )\n            );\n\n            _writer.WriteString(\"iconCanonicalUrl\", embedFooter.IconUrl);\n        }\n\n        _writer.WriteEndObject();\n        await _writer.FlushAsync(cancellationToken);\n    }\n\n    private async ValueTask WriteEmbedFieldAsync(\n        EmbedField embedField,\n        CancellationToken cancellationToken = default\n    )\n    {\n        _writer.WriteStartObject();\n\n        _writer.WriteString(\"name\", await FormatMarkdownAsync(embedField.Name, cancellationToken));\n        _writer.WriteString(\n            \"value\",\n            await FormatMarkdownAsync(embedField.Value, cancellationToken)\n        );\n        _writer.WriteBoolean(\"isInline\", embedField.IsInline);\n\n        _writer.WriteEndObject();\n        await _writer.FlushAsync(cancellationToken);\n    }\n\n    private async ValueTask WriteEmbedAsync(\n        Embed embed,\n        CancellationToken cancellationToken = default\n    )\n    {\n        _writer.WriteStartObject();\n\n        _writer.WriteString(\n            \"title\",\n            await FormatMarkdownAsync(embed.Title ?? \"\", cancellationToken)\n        );\n        _writer.WriteString(\"url\", embed.Url);\n        _writer.WriteString(\"timestamp\", embed.Timestamp?.Pipe(Context.NormalizeDate));\n        _writer.WriteString(\n            \"description\",\n            await FormatMarkdownAsync(embed.Description ?? \"\", cancellationToken)\n        );\n\n        if (embed.Color is not null)\n            _writer.WriteString(\"color\", embed.Color.Value.ToHex());\n\n        if (embed.Author is not null)\n        {\n            _writer.WritePropertyName(\"author\");\n            await WriteEmbedAuthorAsync(embed.Author, cancellationToken);\n        }\n\n        if (embed.Thumbnail is not null)\n        {\n            _writer.WritePropertyName(\"thumbnail\");\n            await WriteEmbedImageAsync(embed.Thumbnail, cancellationToken);\n        }\n\n        if (embed.Image is not null)\n        {\n            _writer.WritePropertyName(\"image\");\n            await WriteEmbedImageAsync(embed.Image, cancellationToken);\n        }\n\n        if (embed.Video is not null)\n        {\n            _writer.WritePropertyName(\"video\");\n            await WriteEmbedVideoAsync(embed.Video, cancellationToken);\n        }\n\n        if (embed.Footer is not null)\n        {\n            _writer.WritePropertyName(\"footer\");\n            await WriteEmbedFooterAsync(embed.Footer, cancellationToken);\n        }\n\n        // Images\n        _writer.WriteStartArray(\"images\");\n\n        foreach (var image in embed.Images)\n            await WriteEmbedImageAsync(image, cancellationToken);\n\n        _writer.WriteEndArray();\n\n        // Fields\n        _writer.WriteStartArray(\"fields\");\n\n        foreach (var field in embed.Fields)\n            await WriteEmbedFieldAsync(field, cancellationToken);\n\n        _writer.WriteEndArray();\n\n        // Inline emoji\n        _writer.WriteStartArray(\"inlineEmojis\");\n\n        if (!string.IsNullOrWhiteSpace(embed.Description))\n        {\n            foreach (\n                var emoji in MarkdownParser\n                    .ExtractEmojis(embed.Description)\n                    .DistinctBy(e => e.Name, StringComparer.Ordinal)\n            )\n            {\n                await WriteEmojiAsync(\n                    new Emoji(emoji.Id, emoji.Name, emoji.IsAnimated),\n                    cancellationToken\n                );\n            }\n        }\n\n        _writer.WriteEndArray();\n\n        _writer.WriteEndObject();\n        await _writer.FlushAsync(cancellationToken);\n    }\n\n    private async ValueTask WriteStickerAsync(\n        Sticker sticker,\n        CancellationToken cancellationToken = default\n    )\n    {\n        _writer.WriteStartObject();\n\n        _writer.WriteString(\"id\", sticker.Id.ToString());\n        _writer.WriteString(\"name\", sticker.Name);\n        _writer.WriteString(\"format\", sticker.Format.ToString());\n        _writer.WriteString(\n            \"sourceUrl\",\n            await Context.ResolveAssetUrlAsync(sticker.SourceUrl, cancellationToken)\n        );\n\n        _writer.WriteEndObject();\n    }\n\n    public override async ValueTask WritePreambleAsync(\n        CancellationToken cancellationToken = default\n    )\n    {\n        // Root object (start)\n        _writer.WriteStartObject();\n\n        // Guild\n        _writer.WriteStartObject(\"guild\");\n        _writer.WriteString(\"id\", Context.Request.Guild.Id.ToString());\n        _writer.WriteString(\"name\", Context.Request.Guild.Name);\n\n        _writer.WriteString(\n            \"iconUrl\",\n            await Context.ResolveAssetUrlAsync(Context.Request.Guild.IconUrl, cancellationToken)\n        );\n\n        _writer.WriteEndObject();\n\n        // Channel\n        _writer.WriteStartObject(\"channel\");\n        _writer.WriteString(\"id\", Context.Request.Channel.Id.ToString());\n        _writer.WriteString(\"type\", Context.Request.Channel.Kind.ToString());\n\n        // Original schema did not account for threads, so 'category' actually refers to the parent channel\n        _writer.WriteString(\"categoryId\", Context.Request.Channel.Parent?.Id.ToString());\n        _writer.WriteString(\"category\", Context.Request.Channel.Parent?.Name);\n\n        _writer.WriteString(\"name\", Context.Request.Channel.Name);\n        _writer.WriteString(\"topic\", Context.Request.Channel.Topic);\n\n        if (!string.IsNullOrWhiteSpace(Context.Request.Channel.IconUrl))\n        {\n            _writer.WriteString(\n                \"iconUrl\",\n                await Context.ResolveAssetUrlAsync(\n                    Context.Request.Channel.IconUrl,\n                    cancellationToken\n                )\n            );\n        }\n\n        _writer.WriteEndObject();\n\n        // Date range\n        _writer.WriteStartObject(\"dateRange\");\n        _writer.WriteString(\"after\", Context.Request.After?.ToDate().Pipe(Context.NormalizeDate));\n        _writer.WriteString(\"before\", Context.Request.Before?.ToDate().Pipe(Context.NormalizeDate));\n        _writer.WriteEndObject();\n\n        // Timestamp\n        _writer.WriteString(\"exportedAt\", Context.NormalizeDate(DateTimeOffset.UtcNow));\n\n        // Message array (start)\n        _writer.WriteStartArray(\"messages\");\n        await _writer.FlushAsync(cancellationToken);\n    }\n\n    public override async ValueTask WriteMessageAsync(\n        Message message,\n        CancellationToken cancellationToken = default\n    )\n    {\n        await base.WriteMessageAsync(message, cancellationToken);\n\n        _writer.WriteStartObject();\n\n        // Metadata\n        _writer.WriteString(\"id\", message.Id.ToString());\n        _writer.WriteString(\"type\", message.Kind.ToString());\n        _writer.WriteString(\"timestamp\", Context.NormalizeDate(message.Timestamp));\n        _writer.WriteString(\n            \"timestampEdited\",\n            message.EditedTimestamp?.Pipe(Context.NormalizeDate)\n        );\n        _writer.WriteString(\n            \"callEndedTimestamp\",\n            message.CallEndedTimestamp?.Pipe(Context.NormalizeDate)\n        );\n        _writer.WriteBoolean(\"isPinned\", message.IsPinned);\n\n        // Content\n        if (message.IsSystemNotification)\n        {\n            _writer.WriteString(\"content\", message.GetFallbackContent());\n        }\n        else\n        {\n            _writer.WriteString(\n                \"content\",\n                await FormatMarkdownAsync(message.Content, cancellationToken)\n            );\n        }\n\n        // Author\n        _writer.WritePropertyName(\"author\");\n        await WriteUserAsync(message.Author, true, cancellationToken);\n\n        // Attachments\n        _writer.WriteStartArray(\"attachments\");\n\n        foreach (var attachment in message.Attachments)\n            await WriteAttachmentAsync(attachment, cancellationToken);\n\n        _writer.WriteEndArray();\n\n        // Embeds\n        _writer.WriteStartArray(\"embeds\");\n\n        foreach (var embed in message.Embeds)\n            await WriteEmbedAsync(embed, cancellationToken);\n\n        _writer.WriteEndArray();\n\n        // Stickers\n        _writer.WriteStartArray(\"stickers\");\n\n        foreach (var sticker in message.Stickers)\n            await WriteStickerAsync(sticker, cancellationToken);\n\n        _writer.WriteEndArray();\n\n        // Reactions\n        _writer.WriteStartArray(\"reactions\");\n\n        foreach (var reaction in message.Reactions)\n        {\n            _writer.WriteStartObject();\n\n            // Emoji\n            _writer.WritePropertyName(\"emoji\");\n            await WriteEmojiAsync(reaction.Emoji, cancellationToken);\n\n            _writer.WriteNumber(\"count\", reaction.Count);\n\n            // Reaction authors\n            _writer.WriteStartArray(\"users\");\n\n            await foreach (\n                var user in Context.Discord.GetMessageReactionsAsync(\n                    Context.Request.Channel.Id,\n                    message.Id,\n                    reaction.Emoji,\n                    cancellationToken\n                )\n            )\n            {\n                await WriteUserAsync(user, false, cancellationToken);\n            }\n\n            _writer.WriteEndArray();\n\n            _writer.WriteEndObject();\n        }\n\n        _writer.WriteEndArray();\n\n        // Mentions\n        _writer.WriteStartArray(\"mentions\");\n        foreach (var user in message.MentionedUsers)\n            await WriteUserAsync(user, true, cancellationToken);\n\n        _writer.WriteEndArray();\n\n        // Message reference\n        if (message.Reference is not null)\n        {\n            _writer.WriteStartObject(\"reference\");\n            _writer.WriteString(\"type\", message.Reference.Kind.ToString());\n            _writer.WriteString(\"messageId\", message.Reference.MessageId?.ToString());\n            _writer.WriteString(\"channelId\", message.Reference.ChannelId?.ToString());\n            _writer.WriteString(\"guildId\", message.Reference.GuildId?.ToString());\n            _writer.WriteEndObject();\n        }\n\n        // Forwarded message\n        if (message.ForwardedMessage is not null)\n        {\n            _writer.WriteStartObject(\"forwardedMessage\");\n\n            _writer.WriteString(\n                \"timestamp\",\n                Context.NormalizeDate(message.ForwardedMessage.Timestamp)\n            );\n\n            _writer.WriteString(\n                \"timestampEdited\",\n                message.ForwardedMessage.EditedTimestamp?.Pipe(Context.NormalizeDate)\n            );\n\n            _writer.WriteString(\n                \"content\",\n                await FormatMarkdownAsync(message.ForwardedMessage.Content, cancellationToken)\n            );\n\n            // Forwarded attachments\n            _writer.WriteStartArray(\"attachments\");\n\n            foreach (var attachment in message.ForwardedMessage.Attachments)\n                await WriteAttachmentAsync(attachment, cancellationToken);\n\n            _writer.WriteEndArray();\n\n            // Forwarded embeds\n            _writer.WriteStartArray(\"embeds\");\n            foreach (var embed in message.ForwardedMessage.Embeds)\n                await WriteEmbedAsync(embed, cancellationToken);\n            _writer.WriteEndArray();\n\n            // Forwarded stickers\n            _writer.WriteStartArray(\"stickers\");\n\n            foreach (var sticker in message.ForwardedMessage.Stickers)\n                await WriteStickerAsync(sticker, cancellationToken);\n\n            _writer.WriteEndArray();\n\n            _writer.WriteEndObject();\n        }\n\n        // Interaction\n        if (message.Interaction is not null)\n        {\n            _writer.WriteStartObject(\"interaction\");\n\n            _writer.WriteString(\"id\", message.Interaction.Id.ToString());\n            _writer.WriteString(\"name\", message.Interaction.Name);\n\n            _writer.WritePropertyName(\"user\");\n            await WriteUserAsync(message.Interaction.User, true, cancellationToken);\n\n            _writer.WriteEndObject();\n        }\n\n        // Inline emoji\n        _writer.WriteStartArray(\"inlineEmojis\");\n\n        foreach (\n            var emoji in MarkdownParser\n                .ExtractEmojis(message.Content)\n                .DistinctBy(e => e.Name, StringComparer.Ordinal)\n        )\n        {\n            await WriteEmojiAsync(\n                new Emoji(emoji.Id, emoji.Name, emoji.IsAnimated),\n                cancellationToken\n            );\n        }\n\n        _writer.WriteEndArray();\n\n        _writer.WriteEndObject();\n        await _writer.FlushAsync(cancellationToken);\n    }\n\n    public override async ValueTask WritePostambleAsync(\n        CancellationToken cancellationToken = default\n    )\n    {\n        // Message array (end)\n        _writer.WriteEndArray();\n\n        _writer.WriteNumber(\"messageCount\", MessagesWritten);\n\n        // Root object (end)\n        _writer.WriteEndObject();\n        await _writer.FlushAsync(cancellationToken);\n    }\n\n    public override async ValueTask DisposeAsync()\n    {\n        await _writer.DisposeAsync();\n        await base.DisposeAsync();\n    }\n}\n"
  },
  {
    "path": "DiscordChatExporter.Core/Exporting/MessageExporter.cs",
    "content": "﻿using System;\nusing System.IO;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing DiscordChatExporter.Core.Discord.Data;\n\nnamespace DiscordChatExporter.Core.Exporting;\n\ninternal partial class MessageExporter(ExportContext context) : IAsyncDisposable\n{\n    private int _partitionIndex;\n    private MessageWriter? _writer;\n\n    public long MessagesExported { get; private set; }\n\n    private async ValueTask<MessageWriter> InitializeWriterAsync(\n        CancellationToken cancellationToken = default\n    )\n    {\n        // Ensure that the partition limit has not been reached\n        if (\n            _writer is not null\n            && context.Request.PartitionLimit.IsReached(\n                _writer.MessagesWritten,\n                _writer.BytesWritten\n            )\n        )\n        {\n            await UninitializeWriterAsync(cancellationToken);\n            _partitionIndex++;\n        }\n\n        // Writer is still valid, return\n        if (_writer is not null)\n            return _writer;\n\n        Directory.CreateDirectory(context.Request.OutputDirPath);\n        var filePath = GetPartitionFilePath(context.Request.OutputFilePath, _partitionIndex);\n\n        var writer = CreateMessageWriter(filePath, context.Request.Format, context);\n        await writer.WritePreambleAsync(cancellationToken);\n\n        return _writer = writer;\n    }\n\n    private async ValueTask UninitializeWriterAsync(CancellationToken cancellationToken = default)\n    {\n        if (_writer is not null)\n        {\n            try\n            {\n                await _writer.WritePostambleAsync(cancellationToken);\n            }\n            // Writer must be disposed, even if it fails to write the postamble\n            finally\n            {\n                await _writer.DisposeAsync();\n                _writer = null;\n            }\n        }\n    }\n\n    public async ValueTask ExportMessageAsync(\n        Message message,\n        CancellationToken cancellationToken = default\n    )\n    {\n        var writer = await InitializeWriterAsync(cancellationToken);\n        await writer.WriteMessageAsync(message, cancellationToken);\n        MessagesExported++;\n    }\n\n    public async ValueTask DisposeAsync()\n    {\n        // If not messages were written, force the creation of an empty file\n        if (MessagesExported <= 0)\n            _ = await InitializeWriterAsync();\n\n        await UninitializeWriterAsync();\n    }\n}\n\ninternal partial class MessageExporter\n{\n    private static string GetPartitionFilePath(string baseFilePath, int partitionIndex)\n    {\n        // First partition, don't change the file name\n        if (partitionIndex <= 0)\n            return baseFilePath;\n\n        // Inject partition index into the file name\n        var fileNameWithoutExt = Path.GetFileNameWithoutExtension(baseFilePath);\n        var fileExt = Path.GetExtension(baseFilePath);\n        var fileName = $\"{fileNameWithoutExt} [part {partitionIndex + 1}]{fileExt}\";\n        var dirPath = Path.GetDirectoryName(baseFilePath);\n\n        return !string.IsNullOrWhiteSpace(dirPath) ? Path.Combine(dirPath, fileName) : fileName;\n    }\n\n    private static MessageWriter CreateMessageWriter(\n        string filePath,\n        ExportFormat format,\n        ExportContext context\n    ) =>\n        format switch\n        {\n            ExportFormat.PlainText => new PlainTextMessageWriter(File.Create(filePath), context),\n            ExportFormat.Csv => new CsvMessageWriter(File.Create(filePath), context),\n            ExportFormat.HtmlDark => new HtmlMessageWriter(File.Create(filePath), context, \"Dark\"),\n            ExportFormat.HtmlLight => new HtmlMessageWriter(\n                File.Create(filePath),\n                context,\n                \"Light\"\n            ),\n            ExportFormat.Json => new JsonMessageWriter(File.Create(filePath), context),\n            _ => throw new ArgumentOutOfRangeException(\n                nameof(format),\n                $\"Unknown export format '{format}'.\"\n            ),\n        };\n}\n"
  },
  {
    "path": "DiscordChatExporter.Core/Exporting/MessageGroupTemplate.cshtml",
    "content": "@using System\n@using System.Collections.Generic\n@using System.Linq\n@using System.Threading.Tasks\n@using DiscordChatExporter.Core.Discord.Data\n@using DiscordChatExporter.Core.Discord.Data.Embeds\n@using DiscordChatExporter.Core.Markdown.Parsing\n@using DiscordChatExporter.Core.Utils.Extensions\n\n@inherits RazorBlade.HtmlTemplate\n\n@functions {\n    public required ExportContext Context { get; init; }\n\n    public required IReadOnlyList<Message> Messages { get; init; }\n}\n\n@{\n    ValueTask<string> ResolveAssetUrlAsync(string url) =>\n        Context.ResolveAssetUrlAsync(url, CancellationToken);\n\n    string FormatDate(DateTimeOffset instant, string format = \"g\") =>\n        Context.FormatDate(instant, format);\n\n    async ValueTask<string> FormatMarkdownAsync(string markdown) =>\n        Context.Request.ShouldFormatMarkdown\n            ? await HtmlMarkdownVisitor.FormatAsync(Context, markdown, true, CancellationToken)\n            : markdown;\n\n    async ValueTask<string> FormatEmbedMarkdownAsync(string markdown) =>\n        Context.Request.ShouldFormatMarkdown\n            ? await HtmlMarkdownVisitor.FormatAsync(Context, markdown, false, CancellationToken)\n            : markdown;\n}\n\n<div class=\"chatlog__message-group\">\n@foreach (var (i, message) in Messages.Index())\n{\n    var isFirst = i == 0;\n\n    var authorMember = Context.TryGetMember(message.Author.Id);\n    var authorColor = Context.TryGetUserColor(message.Author.Id);\n    var authorDisplayName = message.Author.IsBot\n        ? message.Author.DisplayName\n        : authorMember?.DisplayName ?? message.Author.DisplayName;\n\n    <div id=\"chatlog__message-container-@message.Id\" class=\"chatlog__message-container @(message.IsPinned ? \"chatlog__message-container--pinned\" : null)\" data-message-id=\"@message.Id\">\n        <div class=\"chatlog__message\">\n            @* System notification *@\n            @if (message.IsSystemNotification)\n            {\n                <div class=\"chatlog__message-aside\">\n                    <svg class=\"chatlog__system-notification-icon\">\n                        @{\n                            var icon = message.Kind switch {\n                                MessageKind.RecipientAdd => \"join-icon\",\n                                MessageKind.RecipientRemove => \"leave-icon\",\n                                MessageKind.Call => \"call-icon\",\n                                MessageKind.ChannelNameChange => \"pencil-icon\",\n                                MessageKind.ChannelIconChange => \"pencil-icon\",\n                                MessageKind.ChannelPinnedMessage => \"pin-icon\",\n                                MessageKind.GuildMemberJoin => \"join-icon\",\n                                MessageKind.ThreadCreated => \"thread-icon\",\n                                _ => \"pencil-icon\"\n                            };\n                        }\n\n                        <use href=\"#@icon\"></use>\n                    </svg>\n                </div>\n\n                <div class=\"chatlog__message-primary\">\n                    @* Author name *@\n                    <span class=\"chatlog__system-notification-author\" style=\"@(authorColor is not null ? $\"color: rgb({authorColor.Value.R}, {authorColor.Value.G}, {authorColor.Value.B})\" : null)\" title=\"@message.Author.FullName\" data-user-id=\"@message.Author.Id\">@authorDisplayName</span>\n\n                    @* Space out the content *@\n                    <span> </span>\n\n                    @* System notification content *@\n                    <span class=\"chatlog__system-notification-content\">\n                        @if (message.Kind == MessageKind.RecipientAdd && message.MentionedUsers.Any())\n                        {\n                            <span>added </span>\n                            <a class=\"chatlog__system-notification-link\" title=\"@message.MentionedUsers.First().FullName\">@message.MentionedUsers.First().DisplayName</a>\n                            <span> to the group.</span>\n                        }\n                        else if (message.Kind == MessageKind.RecipientRemove && message.MentionedUsers.Any())\n                        {\n                            if (message.Author.Id == message.MentionedUsers.First().Id)\n                            {\n                                <span>left the group.</span>\n                            }\n                            else\n                            {\n                                <span>removed </span>\n                                <a class=\"chatlog__system-notification-link\" title=\"@message.MentionedUsers.First().FullName\">@message.MentionedUsers.First().DisplayName</a>\n                                <span> from the group.</span>\n                            }\n                        }\n                        else if (message.Kind == MessageKind.Call)\n                        {\n                            <span>started a call that lasted @(((message.CallEndedTimestamp ?? message.Timestamp) - message.Timestamp).TotalMinutes.ToString(\"n0\", Context.Request.CultureInfo)) minutes</span>\n                        }\n                        else if (message.Kind == MessageKind.ChannelNameChange)\n                        {\n                            <span>changed the channel name: </span>\n                            <span class=\"chatlog__system-notification-link\">@message.Content</span>\n                        }\n                        else if (message.Kind == MessageKind.ChannelIconChange)\n                        {\n                            <span>changed the channel icon.</span>\n                        }\n                        else if (message.Kind == MessageKind.ChannelPinnedMessage && message.Reference is not null)\n                        {\n                            <span>pinned </span>\n                            <a class=\"chatlog__system-notification-link\" href=\"#chatlog__message-container-@message.Reference.MessageId\">a message</a>\n                            <span> to this channel.</span>\n                        }\n                        else if (message.Kind == MessageKind.ThreadCreated)\n                        {\n                            <span>started a thread.</span>\n                        }\n                        else if (message.Kind == MessageKind.GuildMemberJoin)\n                        {\n                            <span>joined the server.</span>\n                        }\n                        else\n                        {\n                            <span>@message.Content.ToLowerInvariant()</span>\n                        }\n                    </span>\n\n                    @* Timestamp *@\n                    <span class=\"chatlog__system-notification-timestamp\" title=\"@FormatDate(message.Timestamp, \"f\")\">\n                        <a href=\"#chatlog__message-container-@message.Id\">@FormatDate(message.Timestamp)</a>\n                    </span>\n                </div>\n            }\n            // Regular message\n            else\n            {\n                <div class=\"chatlog__message-aside\">\n                    @if (isFirst)\n                    {\n                        // Reply symbol\n                        if (message.IsReplyLike)\n                        {\n                            <div class=\"chatlog__reply-symbol\"></div>\n                        }\n\n                        // Avatar\n                        <img class=\"chatlog__avatar\" src=\"@await ResolveAssetUrlAsync(authorMember?.AvatarUrl ?? message.Author.AvatarUrl)\" alt=\"Avatar\" loading=\"lazy\">\n                    }\n                    else\n                    {\n                        <div class=\"chatlog__short-timestamp\" title=\"@FormatDate(message.Timestamp, \"f\")\">@FormatDate(message.Timestamp, \"t\")</div>\n                    }\n                </div>\n\n                <div class=\"chatlog__message-primary\">\n                    @if (isFirst)\n                    {\n                        // Message referenced by the reply\n                        if (message.IsReplyLike)\n                        {\n                            <div class=\"chatlog__reply\">\n                                @if (message.ReferencedMessage is not null)\n                                {\n                                    var referencedUserMember = Context.TryGetMember(message.ReferencedMessage.Author.Id);\n                                    var referencedUserColor = Context.TryGetUserColor(message.ReferencedMessage.Author.Id);\n                                    var referencedUserDisplayName = message.ReferencedMessage.Author.IsBot\n                                        ? message.ReferencedMessage.Author.DisplayName\n                                        : referencedUserMember?.DisplayName ?? message.ReferencedMessage.Author.DisplayName;\n\n                                    <img class=\"chatlog__reply-avatar\" src=\"@await ResolveAssetUrlAsync(referencedUserMember?.AvatarUrl ?? message.ReferencedMessage.Author.AvatarUrl)\" alt=\"Avatar\" loading=\"lazy\">\n                                    <div class=\"chatlog__reply-author\" style=\"@(referencedUserColor is not null ? $\"color: rgb({referencedUserColor.Value.R}, {referencedUserColor.Value.G}, {referencedUserColor.Value.B})\" : null)\" title=\"@message.ReferencedMessage.Author.FullName\">@referencedUserDisplayName</div>\n                                    <div class=\"chatlog__reply-content\">\n                                        <span class=\"chatlog__reply-link\" onclick=\"scrollToMessage(event, '@message.ReferencedMessage.Id')\">\n                                            @if (!string.IsNullOrWhiteSpace(message.ReferencedMessage.Content) && !message.ReferencedMessage.IsContentHidden())\n                                            {\n                                                <!--wmm:ignore-->@Html.Raw(await FormatEmbedMarkdownAsync(message.ReferencedMessage.Content))<!--/wmm:ignore-->\n                                            }\n                                            else if (message.ReferencedMessage.Attachments.Any() || message.ReferencedMessage.Embeds.Any())\n                                            {\n                                                <em>Click to see attachment</em>\n                                                <span>🖼️</span>\n                                            }\n                                            else\n                                            {\n                                                <em>Click to see original message</em>\n                                            }\n                                        </span>\n\n                                        @if (message.ReferencedMessage.EditedTimestamp is not null)\n                                        {\n                                            <span class=\"chatlog__reply-edited-timestamp\" title=\"@FormatDate(message.ReferencedMessage.EditedTimestamp.Value, \"f\")\">(edited)</span>\n                                        }\n                                    </div>\n                                }\n                                else if (message.Interaction is not null)\n                                {\n                                    var interactionUserMember = Context.TryGetMember(message.Interaction.User.Id);\n                                    var interactionUserColor = Context.TryGetUserColor(message.Interaction.User.Id);\n                                    var interactionUserDisplayName = message.Interaction.User.IsBot\n                                        ? message.Interaction.User.DisplayName\n                                        : interactionUserMember?.DisplayName ?? message.Interaction.User.DisplayName;\n\n                                    <img class=\"chatlog__reply-avatar\" src=\"@await ResolveAssetUrlAsync(interactionUserMember?.AvatarUrl ?? message.Interaction.User.AvatarUrl)\" alt=\"Avatar\" loading=\"lazy\">\n                                    <div class=\"chatlog__reply-author\" style=\"@(interactionUserColor is not null ? $\"color: rgb({interactionUserColor.Value.R}, {interactionUserColor.Value.G}, {interactionUserColor.Value.B})\" : null)\" title=\"@message.Interaction.User.FullName\">@interactionUserDisplayName</div>\n                                    <div class=\"chatlog__reply-content\">\n                                        used /@message.Interaction.Name\n                                    </div>\n                                }\n                                else\n                                {\n                                    <div class=\"chatlog__reply-unknown\">\n                                        Original message was deleted or could not be loaded.\n                                    </div>\n                                }\n                            </div>\n                        }\n\n                        // Header\n                        <div class=\"chatlog__header\">\n                            @* Author name *@\n                            <span class=\"chatlog__author\" style=\"@(authorColor is not null ? $\"color: rgb({authorColor.Value.R}, {authorColor.Value.G}, {authorColor.Value.B})\" : null)\" title=\"@message.Author.FullName\" data-user-id=\"@message.Author.Id\">@authorDisplayName</span>\n\n                            @* Bot tag *@\n                            @if (message.Author.IsBot)\n                            {\n                                // For cross-posts, the BOT tag is replaced with the SERVER tag\n                                if (message.Flags.HasFlag(MessageFlags.CrossPost))\n                                {\n                                    <span class=\"chatlog__author-tag\">SERVER</span>\n                                }\n                                else\n                                {\n                                    <span class=\"chatlog__author-tag\">BOT</span>\n                                }\n                            }\n\n                            @* Timestamp *@\n                            <span class=\"chatlog__timestamp\" title=\"@FormatDate(message.Timestamp, \"f\")\"><a href=\"#chatlog__message-container-@message.Id\">@FormatDate(message.Timestamp)</a></span>\n                        </div>\n                    }\n\n                    @* Content *@\n                    @if ((!string.IsNullOrWhiteSpace(message.Content) && !message.IsContentHidden()) || message.EditedTimestamp is not null)\n                    {\n                        <div class=\"chatlog__content chatlog__markdown\">\n                            @* Text *@\n                            @if (!string.IsNullOrWhiteSpace(message.Content) && !message.IsContentHidden())\n                            {\n                                <span class=\"chatlog__markdown-preserve\"><!--wmm:ignore-->@Html.Raw(await FormatMarkdownAsync(message.Content))<!--/wmm:ignore--></span>\n                            }\n\n                            @* Edited timestamp *@\n                            @if (message.EditedTimestamp is not null)\n                            {\n                                <span class=\"chatlog__edited-timestamp\" title=\"@FormatDate(message.EditedTimestamp.Value, \"f\")\">(edited)</span>\n                            }\n                        </div>\n                    }\n\n                    @* Forwarded message *@\n                    @if (message is { IsForwarded: true, ForwardedMessage: not null })\n                    {\n                        <div class=\"chatlog__forwarded\">\n                            <div class=\"chatlog__forwarded-header\">\n                                <svg class=\"chatlog__forwarded-icon\">\n                                    <use href=\"#forward-icon\"></use>\n                                </svg>\n                                <em>Forwarded</em>\n                            </div>\n\n                            @* Forwarded content *@\n                            @if (!string.IsNullOrWhiteSpace(message.ForwardedMessage.Content))\n                            {\n                                <div class=\"chatlog__forwarded-content chatlog__markdown\">\n                                    <span class=\"chatlog__markdown-preserve\"><!--wmm:ignore-->@Html.Raw(await FormatMarkdownAsync(message.ForwardedMessage.Content))<!--/wmm:ignore--></span>\n                                </div>\n                            }\n\n                            @* Forwarded attachments *@\n                            @if (message.ForwardedMessage.Attachments.Any())\n                            {\n                                <div class=\"chatlog__forwarded-attachments\">\n                                    @foreach (var attachment in message.ForwardedMessage.Attachments)\n                                    {\n                                        @if (attachment.IsImage)\n                                        {\n                                            <a href=\"@await ResolveAssetUrlAsync(attachment.Url)\">\n                                                <img class=\"chatlog__forwarded-attachment\" src=\"@await ResolveAssetUrlAsync(attachment.Url)\" alt=\"@(attachment.Description ?? \"Image attachment\")\" title=\"Image: @attachment.FileName (@attachment.FileSize)\" loading=\"lazy\">\n                                            </a>\n                                        }\n                                        else if (attachment.IsVideo)\n                                        {\n                                            <video class=\"chatlog__forwarded-attachment\" controls>\n                                                <source src=\"@await ResolveAssetUrlAsync(attachment.Url)\" alt=\"@(attachment.Description ?? \"Video attachment\")\" title=\"Video: @attachment.FileName (@attachment.FileSize)\">\n                                            </video>\n                                        }\n                                        else if (attachment.IsAudio)\n                                        {\n                                            <audio class=\"chatlog__forwarded-attachment\" controls>\n                                                <source src=\"@await ResolveAssetUrlAsync(attachment.Url)\" alt=\"@(attachment.Description ?? \"Audio attachment\")\" title=\"Audio: @attachment.FileName (@attachment.FileSize)\">\n                                            </audio>\n                                        }\n                                        else\n                                        {\n                                            <div class=\"chatlog__attachment-generic\">\n                                                <svg class=\"chatlog__attachment-generic-icon\">\n                                                    <use href=\"#attachment-icon\"/>\n                                                </svg>\n                                                <div class=\"chatlog__attachment-generic-name\">\n                                                    <a href=\"@await ResolveAssetUrlAsync(attachment.Url)\">\n                                                        @attachment.FileName\n                                                    </a>\n                                                </div>\n                                                <div class=\"chatlog__attachment-generic-size\">\n                                                    @attachment.FileSize\n                                                </div>\n                                            </div>\n                                        }\n                                    }\n                                </div>\n                            }\n\n                            @* Forwarded stickers *@\n                            @foreach (var sticker in message.ForwardedMessage.Stickers)\n                            {\n                                <div class=\"chatlog__sticker\" title=\"@sticker.Name\">\n                                    @if (sticker.IsImage)\n                                    {\n                                        <img class=\"chatlog__sticker--media\" src=\"@await ResolveAssetUrlAsync(sticker.SourceUrl)\" alt=\"Sticker\">\n                                    }\n                                    else if (sticker.Format == StickerFormat.Lottie)\n                                    {\n                                        <div class=\"chatlog__sticker--media\" data-source=\"@await ResolveAssetUrlAsync(sticker.SourceUrl)\"></div>\n                                    }\n                                </div>\n                            }\n\n                            @* Forwarded timestamp *@\n                            <div class=\"chatlog__forwarded-timestamp\">\n                                <span title=\"@FormatDate(message.ForwardedMessage.Timestamp, \"f\")\">Originally sent: @FormatDate(message.ForwardedMessage.Timestamp)</span>\n                                @if (message.ForwardedMessage.EditedTimestamp is not null)\n                                {\n                                    <span title=\"@FormatDate(message.ForwardedMessage.EditedTimestamp.Value, \"f\")\"> (edited)</span>\n                                }\n                            </div>\n                        </div>\n                    }\n\n                    @* Attachments *@\n                    @foreach (var attachment in message.Attachments)\n                    {\n                        <div class=\"chatlog__attachment @(attachment.IsSpoiler ? \"chatlog__attachment--hidden\" : null)\" onclick=\"@(attachment.IsSpoiler ? \"showSpoiler(event, this)\" : null)\">\n                            @* Spoiler caption *@\n                            @if (attachment.IsSpoiler)\n                            {\n                                <div class=\"chatlog__attachment-spoiler-caption\">SPOILER</div>\n                            }\n\n                            @* Attachment preview *@\n                            @if (attachment.IsImage)\n                            {\n                                <a href=\"@await ResolveAssetUrlAsync(attachment.Url)\">\n                                    <img class=\"chatlog__attachment-media\" src=\"@await ResolveAssetUrlAsync(attachment.Url)\" alt=\"@(attachment.Description ?? \"Image attachment\")\" title=\"Image: @attachment.FileName (@attachment.FileSize)\" loading=\"lazy\">\n                                </a>\n                            }\n                            else if (attachment.IsVideo)\n                            {\n                                <video class=\"chatlog__attachment-media\" controls>\n                                    <source src=\"@await ResolveAssetUrlAsync(attachment.Url)\" alt=\"@(attachment.Description ?? \"Video attachment\")\" title=\"Video: @attachment.FileName (@attachment.FileSize)\">\n                                </video>\n                            }\n                            else if (attachment.IsAudio)\n                            {\n                                <audio class=\"chatlog__attachment-media\" controls>\n                                    <source src=\"@await ResolveAssetUrlAsync(attachment.Url)\" alt=\"@(attachment.Description ?? \"Audio attachment\")\" title=\"Audio: @attachment.FileName (@attachment.FileSize)\">\n                                </audio>\n                            }\n                            else\n                            {\n                                <div class=\"chatlog__attachment-generic\">\n                                    <svg class=\"chatlog__attachment-generic-icon\">\n                                        <use href=\"#attachment-icon\"/>\n                                    </svg>\n                                    <div class=\"chatlog__attachment-generic-name\">\n                                        <a href=\"@await ResolveAssetUrlAsync(attachment.Url)\">\n                                            @attachment.FileName\n                                        </a>\n                                    </div>\n                                    <div class=\"chatlog__attachment-generic-size\">\n                                        @attachment.FileSize\n                                    </div>\n                                </div>\n                            }\n                        </div>\n                    }\n\n                    @* Invites *@\n                    @{\n                        var inviteCodes = MarkdownParser\n                            .ExtractLinks(message.Content)\n                            .Select(l => l.Url)\n                            .Select(Invite.TryGetCodeFromUrl)\n                            .WhereNotNull()\n                            .ToArray();\n\n                        foreach (var inviteCode in inviteCodes)\n                        {\n                            var invite = await Context.Discord.TryGetInviteAsync(inviteCode, CancellationToken);\n                            if (invite is null)\n                            {\n                                continue;\n                            }\n\n                            <div class=\"chatlog__embed\">\n                                <div class=\"chatlog__embed-invite-container\">\n                                    <div class=\"chatlog__embed-invite-title\">@(invite.Channel?.IsDirect == true ? \"Invite to join a group DM\" : \"Invite to join a server\")</div>\n                                    <div class=\"chatlog__embed-invite\">\n                                        <div class=\"chatlog__embed-invite-guild-icon-container\">\n                                            <img class=\"chatlog__embed-invite-guild-icon\" src=\"@await ResolveAssetUrlAsync(invite.Channel?.IconUrl ?? invite.Guild.IconUrl)\" alt=\"Guild icon\" loading=\"lazy\">\n                                        </div>\n                                        <div class=\"chatlog__embed-invite-info\">\n                                            <div class=\"chatlog__embed-invite-guild-name\">\n                                                <a href=\"https://discord.gg/@invite.Code\">\n                                                    @(invite.Guild.Name)\n                                                </a>\n                                            </div>\n                                            <div class=\"chatlog__embed-invite-channel-name\">\n                                                <svg class=\"chatlog__embed-invite-channel-icon\">\n                                                    <use href=\"#channel-icon\"></use>\n                                                </svg>\n                                                <span> @(invite.Channel?.Name ?? \"Unknown Channel\")</span>\n                                            </div>\n                                        </div>\n                                    </div>\n                                </div>\n                            </div>\n                        }\n                    }\n\n                    @* Embeds *@\n                    @foreach (var embed in message.Embeds)\n                    {\n                        // Spotify embed\n                        if (embed.TryGetSpotifyTrack() is { } spotifyTrackEmbed)\n                        {\n                            <div class=\"chatlog__embed\">\n                                <div class=\"chatlog__embed-spotify-container\">\n                                    <iframe class=\"chatlog__embed-spotify\" src=\"@spotifyTrackEmbed.Url\" width=\"400\" height=\"80\" allowtransparency=\"true\" allow=\"encrypted-media\"></iframe>\n                                </div>\n                            </div>\n                        }\n                        // YouTube embed\n                        else if (embed.TryGetYouTubeVideo() is { } youTubeVideoEmbed)\n                        {\n                            <div class=\"chatlog__embed\">\n                                @* Color pill *@\n                                @if (embed.Color is not null)\n                                {\n                                    <div class=\"chatlog__embed-color-pill\" style=\"background-color: rgba(@embed.Color.Value.R, @embed.Color.Value.G, @embed.Color.Value.B, @embed.Color.Value.A)\"></div>\n                                }\n                                else\n                                {\n                                    <div class=\"chatlog__embed-color-pill chatlog__embed-color-pill--default\"></div>\n                                }\n\n                                <div class=\"chatlog__embed-content-container\">\n                                    <div class=\"chatlog__embed-content\">\n                                        <div class=\"chatlog__embed-text\">\n                                            @* Embed author *@\n                                            @if (embed.Author is not null)\n                                            {\n                                                <div class=\"chatlog__embed-author-container\">\n                                                    @if (!string.IsNullOrWhiteSpace(embed.Author.IconUrl))\n                                                    {\n                                                        <img class=\"chatlog__embed-author-icon\" src=\"@await ResolveAssetUrlAsync(embed.Author.IconProxyUrl ?? embed.Author.IconUrl)\" alt=\"Author icon\" loading=\"lazy\" onerror=\"this.style.visibility='hidden'\" data-canonical-url=\"@embed.Author.IconUrl\">\n                                                    }\n\n                                                    @if (!string.IsNullOrWhiteSpace(embed.Author.Name))\n                                                    {\n                                                        if (!string.IsNullOrWhiteSpace(embed.Author.Url))\n                                                        {\n                                                            <a class=\"chatlog__embed-author-link\" href=\"@embed.Author.Url\">\n                                                                <div class=\"chatlog__embed-author\">@embed.Author.Name</div>\n                                                            </a>\n                                                        }\n                                                        else\n                                                        {\n                                                            <div class=\"chatlog__embed-author\">@embed.Author.Name</div>\n                                                        }\n                                                    }\n                                                </div>\n                                            }\n\n                                            @* Embed title *@\n                                            @if (!string.IsNullOrWhiteSpace(embed.Title))\n                                            {\n                                                <div class=\"chatlog__embed-title\">\n                                                    @if (!string.IsNullOrWhiteSpace(embed.Url))\n                                                    {\n                                                        <a class=\"chatlog__embed-title-link\" href=\"@embed.Url\">\n                                                            <div class=\"chatlog__markdown chatlog__markdown-preserve\"><!--wmm:ignore-->@Html.Raw(await FormatEmbedMarkdownAsync(embed.Title))<!--/wmm:ignore--></div>\n                                                        </a>\n                                                    }\n                                                    else\n                                                    {\n                                                        <div class=\"chatlog__markdown chatlog__markdown-preserve\"><!--wmm:ignore-->@Html.Raw(await FormatEmbedMarkdownAsync(embed.Title))<!--/wmm:ignore--></div>\n                                                    }\n                                                </div>\n                                            }\n\n                                            @* Video thumbnail *@\n                                            <div class=\"chatlog__embed-youtube-container\">\n                                                <a href=\"@youTubeVideoEmbed.Url\">\n                                                    <img class=\"chatlog__embed-youtube-thumbnail\" src=\"@await ResolveAssetUrlAsync(embed.Thumbnail?.ProxyUrl ?? embed.Thumbnail?.Url ?? youTubeVideoEmbed.ThumbnailUrl)\" alt=\"YouTube video thumbnail\" loading=\"lazy\" onerror=\"this.style.visibility='hidden'\" data-canonical-url=\"@(embed.Thumbnail?.Url ?? youTubeVideoEmbed.ThumbnailUrl)\">\n                                                </a>\n                                            </div>\n                                        </div>\n                                    </div>\n                                </div>\n                            </div>\n                        }\n                        // Generic image embed\n                        else if (embed.Kind == EmbedKind.Image && !string.IsNullOrWhiteSpace(embed.Url))\n                        {\n                            var embedImageUrl =\n                                embed.Image?.ProxyUrl ?? embed.Image?.Url ??\n                                embed.Thumbnail?.ProxyUrl ?? embed.Thumbnail?.Url ??\n                                embed.Url;\n\n                            var embedImageCanonicalUrl =\n                                embed.Image?.Url ?? embed.Thumbnail?.Url ?? embed.Url;\n\n                            <div class=\"chatlog__embed\">\n                                <a href=\"@await ResolveAssetUrlAsync(embedImageUrl)\">\n                                    <img class=\"chatlog__embed-generic-image\" src=\"@await ResolveAssetUrlAsync(embedImageUrl)\" alt=\"Embedded image\" loading=\"lazy\" data-canonical-url=\"@embedImageCanonicalUrl\">\n                                </a>\n                            </div>\n                        }\n                        // Generic video embed\n                        else if (embed.Kind == EmbedKind.Video \n                                 && !string.IsNullOrWhiteSpace(embed.Url)\n                                 // Twitch clips cannot be embedded in local HTML files\n                                 && embed.TryGetTwitchClip() is null)\n                        {\n                            var embedVideoUrl =\n                                embed.Video?.ProxyUrl ?? embed.Video?.Url ??\n                                embed.Url;\n\n                            var embedVideoCanonicalUrl = embed.Video?.Url ?? embed.Url;\n\n                            <div class=\"chatlog__embed\">\n                                <video class=\"chatlog__embed-generic-video\" width=\"@embed.Video?.Width\" height=\"@embed.Video?.Height\" controls data-canonical-url=\"@embedVideoCanonicalUrl\">\n                                    <source src=\"@await ResolveAssetUrlAsync(embedVideoUrl)\" alt=\"Embedded video\">\n                                </video>\n                            </div>\n                        }\n                        // Generic gifv embed\n                        else if (embed.Kind == EmbedKind.Gifv && !string.IsNullOrWhiteSpace(embed.Url))\n                        {\n                            var embedVideoUrl =\n                                embed.Video?.ProxyUrl ?? embed.Video?.Url ??\n                                embed.Url;\n\n                            var embedVideoCanonicalUrl = embed.Video?.Url ?? embed.Url;\n\n                            <div class=\"chatlog__embed\">\n                                <video class=\"chatlog__embed-generic-gifv\" width=\"@embed.Video?.Width\" height=\"@embed.Video?.Height\" loop onmouseover=\"this.play()\" onmouseout=\"this.pause()\" data-canonical-url=\"@embedVideoCanonicalUrl\">\n                                    <source src=\"@await ResolveAssetUrlAsync(embedVideoUrl)\" alt=\"Embedded gifv\">\n                                </video>\n                            </div>\n                        }\n                        // Rich embed\n                        else\n                        {\n                            <div class=\"chatlog__embed\">\n                                @* Color pill *@\n                                @if (embed.Color is not null)\n                                {\n                                    <div class=\"chatlog__embed-color-pill\" style=\"background-color: rgba(@embed.Color.Value.R, @embed.Color.Value.G, @embed.Color.Value.B, @embed.Color.Value.A)\"></div>\n                                }\n                                else\n                                {\n                                    <div class=\"chatlog__embed-color-pill chatlog__embed-color-pill--default\"></div>\n                                }\n\n                                <div class=\"chatlog__embed-content-container\">\n                                    <div class=\"chatlog__embed-content\">\n                                        <div class=\"chatlog__embed-text\">\n                                            @* Embed author *@\n                                            @if (embed.Author is not null)\n                                            {\n                                                <div class=\"chatlog__embed-author-container\">\n                                                    @if (!string.IsNullOrWhiteSpace(embed.Author.IconUrl))\n                                                    {\n                                                        <img class=\"chatlog__embed-author-icon\" src=\"@await ResolveAssetUrlAsync(embed.Author.IconProxyUrl ?? embed.Author.IconUrl)\" alt=\"Author icon\" loading=\"lazy\" onerror=\"this.style.visibility='hidden'\" data-canonical-url=\"@embed.Author.IconUrl\">\n                                                    }\n\n                                                    @if (!string.IsNullOrWhiteSpace(embed.Author.Name))\n                                                    {\n                                                        if (!string.IsNullOrWhiteSpace(embed.Author.Url))\n                                                        {\n                                                            <a class=\"chatlog__embed-author-link\" href=\"@embed.Author.Url\">\n                                                                <div class=\"chatlog__embed-author\">@embed.Author.Name</div>\n                                                            </a>\n                                                        }\n                                                        else\n                                                        {\n                                                            <div class=\"chatlog__embed-author\">@embed.Author.Name</div>\n                                                        }\n                                                    }\n                                                </div>\n                                            }\n\n                                            @* Embed title *@\n                                            @if (!string.IsNullOrWhiteSpace(embed.Title))\n                                            {\n                                                <div class=\"chatlog__embed-title\">\n                                                    @if (!string.IsNullOrWhiteSpace(embed.Url))\n                                                    {\n                                                        <a class=\"chatlog__embed-title-link\" href=\"@embed.Url\">\n                                                            <div class=\"chatlog__markdown chatlog__markdown-preserve\"><!--wmm:ignore-->@Html.Raw(await FormatEmbedMarkdownAsync(embed.Title))<!--/wmm:ignore--></div>\n                                                        </a>\n                                                    }\n                                                    else\n                                                    {\n                                                        <div class=\"chatlog__markdown chatlog__markdown-preserve\"><!--wmm:ignore-->@Html.Raw(await FormatEmbedMarkdownAsync(embed.Title))<!--/wmm:ignore--></div>\n                                                    }\n                                                </div>\n                                            }\n\n                                            @* Embed description *@\n                                            @if (!string.IsNullOrWhiteSpace(embed.Description))\n                                            {\n                                                <div class=\"chatlog__embed-description\">\n                                                    <div class=\"chatlog__markdown chatlog__markdown-preserve\"><!--wmm:ignore-->@Html.Raw(await FormatEmbedMarkdownAsync(embed.Description))<!--/wmm:ignore--></div>\n                                                </div>\n                                            }\n\n                                            @* Embed fields *@\n                                            @if (embed.Fields.Any())\n                                            {\n                                                <div class=\"chatlog__embed-fields\">\n                                                    @foreach (var field in embed.Fields)\n                                                    {\n                                                        <div class=\"chatlog__embed-field @(field.IsInline ? \"chatlog__embed-field--inline\" : null)\">\n                                                            @if (!string.IsNullOrWhiteSpace(field.Name))\n                                                            {\n                                                                <div class=\"chatlog__embed-field-name\">\n                                                                    <div class=\"chatlog__markdown chatlog__markdown-preserve\"><!--wmm:ignore-->@Html.Raw(await FormatEmbedMarkdownAsync(field.Name))<!--/wmm:ignore--></div>\n                                                                </div>\n                                                            }\n\n                                                            @if (!string.IsNullOrWhiteSpace(field.Value))\n                                                            {\n                                                                <div class=\"chatlog__embed-field-value\">\n                                                                    <div class=\"chatlog__markdown chatlog__markdown-preserve\"><!--wmm:ignore-->@Html.Raw(await FormatEmbedMarkdownAsync(field.Value))<!--/wmm:ignore--></div>\n                                                                </div>\n                                                            }\n                                                        </div>\n                                                    }\n                                                </div>\n                                            }\n                                        </div>\n\n                                        @* Embed content *@\n                                        @if (embed.Thumbnail is not null && !string.IsNullOrWhiteSpace(embed.Thumbnail.Url))\n                                        {\n                                            <div class=\"chatlog__embed-thumbnail-container\">\n                                                <a class=\"chatlog__embed-thumbnail-link\" href=\"@await ResolveAssetUrlAsync(embed.Thumbnail.ProxyUrl ?? embed.Thumbnail.Url)\">\n                                                    <img class=\"chatlog__embed-thumbnail\" src=\"@await ResolveAssetUrlAsync(embed.Thumbnail.ProxyUrl ?? embed.Thumbnail.Url)\" alt=\"Thumbnail\" loading=\"lazy\" data-canonical-url=\"@embed.Thumbnail.Url\">\n                                                </a>\n                                            </div>\n                                        }\n                                    </div>\n\n                                    @* Embed images *@\n                                    @if (embed.Images.Any())\n                                    {\n                                        <div class=\"chatlog__embed-images @(embed.Images.Count == 1 ? \"chatlog__embed-images--single\" : null)\">\n                                            @foreach (var image in embed.Images)\n                                            {\n                                                if (!string.IsNullOrWhiteSpace(image.Url))\n                                                {\n                                                    <div class=\"chatlog__embed-image-container\">\n                                                        <a class=\"chatlog__embed-image-link\" href=\"@await ResolveAssetUrlAsync(image.ProxyUrl ?? image.Url)\">\n                                                            <img class=\"chatlog__embed-image\" src=\"@await ResolveAssetUrlAsync(image.ProxyUrl ?? image.Url)\" alt=\"Image\" loading=\"lazy\" data-canonical-url=\"@image.Url\">\n                                                        </a>\n                                                    </div>\n                                                }\n                                            }\n                                        </div>\n                                    }\n\n                                    @* Embed footer & icon *@\n                                    @if (embed.Footer is not null || embed.Timestamp is not null)\n                                    {\n                                        <div class=\"chatlog__embed-footer\">\n                                            @* Footer icon *@\n                                            @if (!string.IsNullOrWhiteSpace(embed.Footer?.IconUrl))\n                                            {\n                                                <img class=\"chatlog__embed-footer-icon\" src=\"@await ResolveAssetUrlAsync(embed.Footer.IconProxyUrl ?? embed.Footer.IconUrl)\" alt=\"Footer icon\" loading=\"lazy\" data-canonical-url=\"@embed.Footer.IconUrl\">\n                                            }\n\n                                            <span class=\"chatlog__embed-footer-text\">\n                                                @* Footer text *@\n                                                @if (!string.IsNullOrWhiteSpace(embed.Footer?.Text))\n                                                {\n                                                    @embed.Footer.Text\n                                                }\n\n                                                @if (!string.IsNullOrWhiteSpace(embed.Footer?.Text) && embed.Timestamp is not null)\n                                                {\n                                                    @(\" • \")\n                                                }\n\n                                                @* Embed timestamp *@\n                                                @if (embed.Timestamp is not null)\n                                                {\n                                                    @FormatDate(embed.Timestamp.Value)\n                                                }\n                                            </span>\n                                        </div>\n                                    }\n                                </div>\n                            </div>\n                        }\n                    }\n\n                    @* Stickers *@\n                    @foreach (var sticker in message.Stickers)\n                    {\n                        <div class=\"chatlog__sticker\" title=\"@sticker.Name\">\n                            @if (sticker.IsImage)\n                            {\n                                <img class=\"chatlog__sticker--media\" src=\"@await ResolveAssetUrlAsync(sticker.SourceUrl)\" alt=\"Sticker\">\n                            }\n                            else if (sticker.Format == StickerFormat.Lottie)\n                            {\n                                <div class=\"chatlog__sticker--media\" data-source=\"@await ResolveAssetUrlAsync(sticker.SourceUrl)\"></div>\n                            }\n                        </div>\n                    }\n\n                    @* Message reactions *@\n                    @if (message.Reactions.Any())\n                    {\n                        <div class=\"chatlog__reactions\">\n                            @foreach (var reaction in message.Reactions)\n                            {\n                                <div class=\"chatlog__reaction\" title=\"@reaction.Emoji.Code\">\n                                    <img class=\"chatlog__emoji chatlog__emoji--small\" alt=\"@reaction.Emoji.Name\" src=\"@await ResolveAssetUrlAsync(reaction.Emoji.ImageUrl)\" loading=\"lazy\">\n                                    <span class=\"chatlog__reaction-count\">@reaction.Count</span>\n                                </div>\n                            }\n                        </div>\n                    }\n                </div>\n            }\n        </div>\n    </div>\n}\n</div>"
  },
  {
    "path": "DiscordChatExporter.Core/Exporting/MessageWriter.cs",
    "content": "﻿using System;\nusing System.IO;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing DiscordChatExporter.Core.Discord.Data;\n\nnamespace DiscordChatExporter.Core.Exporting;\n\ninternal abstract class MessageWriter(Stream stream, ExportContext context) : IAsyncDisposable\n{\n    protected Stream Stream { get; } = stream;\n\n    protected ExportContext Context { get; } = context;\n\n    public long MessagesWritten { get; private set; }\n\n    public long BytesWritten => Stream.Length;\n\n    public virtual ValueTask WritePreambleAsync(CancellationToken cancellationToken = default) =>\n        default;\n\n    public virtual ValueTask WriteMessageAsync(\n        Message message,\n        CancellationToken cancellationToken = default\n    )\n    {\n        MessagesWritten++;\n        return default;\n    }\n\n    public virtual ValueTask WritePostambleAsync(CancellationToken cancellationToken = default) =>\n        default;\n\n    public virtual async ValueTask DisposeAsync() => await Stream.DisposeAsync();\n}\n"
  },
  {
    "path": "DiscordChatExporter.Core/Exporting/Partitioning/FileSizePartitionLimit.cs",
    "content": "﻿namespace DiscordChatExporter.Core.Exporting.Partitioning;\n\ninternal class FileSizePartitionLimit(long limit) : PartitionLimit\n{\n    public override bool IsReached(long messagesWritten, long bytesWritten) =>\n        bytesWritten >= limit;\n}\n"
  },
  {
    "path": "DiscordChatExporter.Core/Exporting/Partitioning/MessageCountPartitionLimit.cs",
    "content": "﻿namespace DiscordChatExporter.Core.Exporting.Partitioning;\n\ninternal class MessageCountPartitionLimit(long limit) : PartitionLimit\n{\n    public override bool IsReached(long messagesWritten, long bytesWritten) =>\n        messagesWritten >= limit;\n}\n"
  },
  {
    "path": "DiscordChatExporter.Core/Exporting/Partitioning/NullPartitionLimit.cs",
    "content": "﻿namespace DiscordChatExporter.Core.Exporting.Partitioning;\n\ninternal class NullPartitionLimit : PartitionLimit\n{\n    public override bool IsReached(long messagesWritten, long bytesWritten) => false;\n}\n"
  },
  {
    "path": "DiscordChatExporter.Core/Exporting/Partitioning/PartitionLimit.cs",
    "content": "﻿using System;\nusing System.Globalization;\nusing System.Text.RegularExpressions;\n\nnamespace DiscordChatExporter.Core.Exporting.Partitioning;\n\npublic abstract partial class PartitionLimit\n{\n    public abstract bool IsReached(long messagesWritten, long bytesWritten);\n}\n\npublic partial class PartitionLimit\n{\n    public static PartitionLimit Null { get; } = new NullPartitionLimit();\n\n    private static long? TryParseFileSizeBytes(string value, IFormatProvider? formatProvider = null)\n    {\n        var match = Regex.Match(value, @\"^\\s*(\\d+[\\.,]?\\d*)\\s*(\\w)?b\\s*$\", RegexOptions.IgnoreCase);\n\n        // Number part\n        if (\n            !double.TryParse(\n                match.Groups[1].Value,\n                NumberStyles.Float,\n                formatProvider,\n                out var number\n            )\n        )\n        {\n            return null;\n        }\n\n        // Magnitude part\n        var magnitude = match.Groups[2].Value.ToUpperInvariant() switch\n        {\n            \"G\" => 1_000_000_000,\n            \"M\" => 1_000_000,\n            \"K\" => 1_000,\n            \"\" => 1,\n            _ => -1,\n        };\n\n        if (magnitude < 0)\n        {\n            return null;\n        }\n\n        return (long)(number * magnitude);\n    }\n\n    public static PartitionLimit? TryParse(string value, IFormatProvider? formatProvider = null)\n    {\n        var fileSizeLimit = TryParseFileSizeBytes(value, formatProvider);\n        if (fileSizeLimit is not null)\n            return new FileSizePartitionLimit(fileSizeLimit.Value);\n\n        if (int.TryParse(value, NumberStyles.Integer, formatProvider, out var messageCountLimit))\n            return new MessageCountPartitionLimit(messageCountLimit);\n\n        return null;\n    }\n\n    public static PartitionLimit Parse(string value, IFormatProvider? formatProvider = null) =>\n        TryParse(value, formatProvider)\n        ?? throw new FormatException($\"Invalid partition limit '{value}'.\");\n}\n"
  },
  {
    "path": "DiscordChatExporter.Core/Exporting/PlainTextMarkdownVisitor.cs",
    "content": "﻿using System.Text;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing DiscordChatExporter.Core.Markdown;\nusing DiscordChatExporter.Core.Markdown.Parsing;\nusing DiscordChatExporter.Core.Utils.Extensions;\n\nnamespace DiscordChatExporter.Core.Exporting;\n\ninternal partial class PlainTextMarkdownVisitor(ExportContext context, StringBuilder buffer)\n    : MarkdownVisitor\n{\n    protected override ValueTask VisitTextAsync(\n        TextNode text,\n        CancellationToken cancellationToken = default\n    )\n    {\n        buffer.Append(text.Text);\n        return default;\n    }\n\n    protected override ValueTask VisitEmojiAsync(\n        EmojiNode emoji,\n        CancellationToken cancellationToken = default\n    )\n    {\n        buffer.Append(emoji.IsCustomEmoji ? $\":{emoji.Name}:\" : emoji.Name);\n\n        return default;\n    }\n\n    protected override async ValueTask VisitMentionAsync(\n        MentionNode mention,\n        CancellationToken cancellationToken = default\n    )\n    {\n        if (mention.Kind == MentionKind.Everyone)\n        {\n            buffer.Append(\"@everyone\");\n        }\n        else if (mention.Kind == MentionKind.Here)\n        {\n            buffer.Append(\"@here\");\n        }\n        else if (mention.Kind == MentionKind.User)\n        {\n            // User mentions are not always included in the message object,\n            // which means they need to be populated on demand.\n            // https://github.com/Tyrrrz/DiscordChatExporter/issues/304\n            if (mention.TargetId is not null)\n                await context.PopulateMemberAsync(mention.TargetId.Value, cancellationToken);\n\n            var member = mention.TargetId?.Pipe(context.TryGetMember);\n            var displayName = member?.DisplayName ?? member?.User.DisplayName ?? \"Unknown\";\n\n            buffer.Append($\"@{displayName}\");\n        }\n        else if (mention.Kind == MentionKind.Channel)\n        {\n            // Channel/thread mentions may reference threads that are not preloaded,\n            // so we resolve them on demand.\n            // https://github.com/Tyrrrz/DiscordChatExporter/issues/1261\n            if (mention.TargetId is not null)\n                await context.PopulateChannelAsync(mention.TargetId.Value, cancellationToken);\n\n            var channel = mention.TargetId?.Pipe(context.TryGetChannel);\n            var name = channel?.Name ?? \"deleted-channel\";\n\n            buffer.Append($\"#{name}\");\n\n            // Voice channel marker\n            if (channel?.IsVoice == true)\n                buffer.Append(\" [voice]\");\n        }\n        else if (mention.Kind == MentionKind.Role)\n        {\n            var role = mention.TargetId?.Pipe(context.TryGetRole);\n            var name = role?.Name ?? \"deleted-role\";\n\n            buffer.Append($\"@{name}\");\n        }\n    }\n\n    protected override ValueTask VisitTimestampAsync(\n        TimestampNode timestamp,\n        CancellationToken cancellationToken = default\n    )\n    {\n        buffer.Append(\n            timestamp.Instant is not null\n                ? context.FormatDate(timestamp.Instant.Value, timestamp.Format ?? \"g\")\n                : \"Invalid date\"\n        );\n\n        return default;\n    }\n}\n\ninternal partial class PlainTextMarkdownVisitor\n{\n    public static async ValueTask<string> FormatAsync(\n        ExportContext context,\n        string markdown,\n        CancellationToken cancellationToken = default\n    )\n    {\n        var nodes = MarkdownParser.ParseMinimal(markdown);\n\n        var buffer = new StringBuilder();\n        await new PlainTextMarkdownVisitor(context, buffer).VisitAsync(nodes, cancellationToken);\n\n        return buffer.ToString();\n    }\n}\n"
  },
  {
    "path": "DiscordChatExporter.Core/Exporting/PlainTextMessageExtensions.cs",
    "content": "﻿using System.Globalization;\nusing System.Linq;\nusing DiscordChatExporter.Core.Discord.Data;\nusing DiscordChatExporter.Core.Utils.Extensions;\n\nnamespace DiscordChatExporter.Core.Exporting;\n\ninternal static class PlainTextMessageExtensions\n{\n    extension(Message message)\n    {\n        public string GetFallbackContent() =>\n            message.Kind switch\n            {\n                MessageKind.RecipientAdd => message.MentionedUsers.Any()\n                    ? $\"Added {message.MentionedUsers.First().DisplayName} to the group.\"\n                    : \"Added a recipient.\",\n\n                MessageKind.RecipientRemove => message.MentionedUsers.Any()\n                    ? message.Author.Id == message.MentionedUsers.First().Id\n                        ? \"Left the group.\"\n                        : $\"Removed {message.MentionedUsers.First().DisplayName} from the group.\"\n                    : \"Removed a recipient.\",\n\n                MessageKind.Call =>\n                    $\"Started a call that lasted {message\n                            .CallEndedTimestamp?\n                            .Pipe(t => t - message.Timestamp)\n                            .Pipe(t => t.TotalMinutes)\n                            .ToString(\"n0\", CultureInfo.InvariantCulture) ?? \"0\"} minutes.\",\n\n                MessageKind.ChannelNameChange => !string.IsNullOrWhiteSpace(message.Content)\n                    ? $\"Changed the channel name: {message.Content}\"\n                    : \"Changed the channel name.\",\n\n                MessageKind.ChannelIconChange => \"Changed the channel icon.\",\n                MessageKind.ChannelPinnedMessage => \"Pinned a message.\",\n                MessageKind.ThreadCreated => \"Started a thread.\",\n                MessageKind.GuildMemberJoin => \"Joined the server.\",\n\n                _ => message.Content,\n            };\n    }\n}\n"
  },
  {
    "path": "DiscordChatExporter.Core/Exporting/PlainTextMessageWriter.cs",
    "content": "﻿using System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Linq;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing DiscordChatExporter.Core.Discord.Data;\nusing DiscordChatExporter.Core.Discord.Data.Embeds;\nusing DiscordChatExporter.Core.Utils.Extensions;\n\nnamespace DiscordChatExporter.Core.Exporting;\n\ninternal class PlainTextMessageWriter(Stream stream, ExportContext context)\n    : MessageWriter(stream, context)\n{\n    private readonly TextWriter _writer = new StreamWriter(stream);\n\n    private async ValueTask<string> FormatMarkdownAsync(\n        string markdown,\n        CancellationToken cancellationToken = default\n    ) =>\n        Context.Request.ShouldFormatMarkdown\n            ? await PlainTextMarkdownVisitor.FormatAsync(Context, markdown, cancellationToken)\n            : markdown;\n\n    private async ValueTask WriteMessageHeaderAsync(Message message)\n    {\n        // Timestamp & author\n        await _writer.WriteAsync($\"[{Context.FormatDate(message.Timestamp)}]\");\n        await _writer.WriteAsync($\" {message.Author.FullName}\");\n\n        // Whether the message is pinned\n        if (message.IsPinned)\n            await _writer.WriteAsync(\" (pinned)\");\n\n        await _writer.WriteLineAsync();\n    }\n\n    private async ValueTask WriteAttachmentsAsync(\n        IReadOnlyList<Attachment> attachments,\n        CancellationToken cancellationToken = default\n    )\n    {\n        if (!attachments.Any())\n            return;\n\n        await _writer.WriteLineAsync(\"{Attachments}\");\n\n        foreach (var attachment in attachments)\n        {\n            cancellationToken.ThrowIfCancellationRequested();\n\n            await _writer.WriteLineAsync(\n                await Context.ResolveAssetUrlAsync(attachment.Url, cancellationToken)\n            );\n        }\n\n        await _writer.WriteLineAsync();\n    }\n\n    private async ValueTask WriteEmbedsAsync(\n        IReadOnlyList<Embed> embeds,\n        CancellationToken cancellationToken = default\n    )\n    {\n        foreach (var embed in embeds)\n        {\n            cancellationToken.ThrowIfCancellationRequested();\n\n            await _writer.WriteLineAsync(\"{Embed}\");\n\n            if (!string.IsNullOrWhiteSpace(embed.Author?.Name))\n            {\n                await _writer.WriteLineAsync(embed.Author.Name);\n            }\n\n            if (!string.IsNullOrWhiteSpace(embed.Url))\n            {\n                await _writer.WriteLineAsync(embed.Url);\n            }\n\n            if (!string.IsNullOrWhiteSpace(embed.Title))\n            {\n                await _writer.WriteLineAsync(\n                    await FormatMarkdownAsync(embed.Title, cancellationToken)\n                );\n            }\n\n            if (!string.IsNullOrWhiteSpace(embed.Description))\n            {\n                await _writer.WriteLineAsync(\n                    await FormatMarkdownAsync(embed.Description, cancellationToken)\n                );\n            }\n\n            foreach (var field in embed.Fields)\n            {\n                if (!string.IsNullOrWhiteSpace(field.Name))\n                {\n                    await _writer.WriteLineAsync(\n                        await FormatMarkdownAsync(field.Name, cancellationToken)\n                    );\n                }\n\n                if (!string.IsNullOrWhiteSpace(field.Value))\n                {\n                    await _writer.WriteLineAsync(\n                        await FormatMarkdownAsync(field.Value, cancellationToken)\n                    );\n                }\n            }\n\n            if (!string.IsNullOrWhiteSpace(embed.Thumbnail?.Url))\n            {\n                await _writer.WriteLineAsync(\n                    await Context.ResolveAssetUrlAsync(\n                        embed.Thumbnail.ProxyUrl ?? embed.Thumbnail.Url,\n                        cancellationToken\n                    )\n                );\n            }\n\n            foreach (var image in embed.Images)\n            {\n                if (!string.IsNullOrWhiteSpace(image.Url))\n                {\n                    await _writer.WriteLineAsync(\n                        await Context.ResolveAssetUrlAsync(\n                            image.ProxyUrl ?? image.Url,\n                            cancellationToken\n                        )\n                    );\n                }\n            }\n\n            if (!string.IsNullOrWhiteSpace(embed.Footer?.Text))\n            {\n                await _writer.WriteLineAsync(embed.Footer.Text);\n            }\n\n            await _writer.WriteLineAsync();\n        }\n    }\n\n    private async ValueTask WriteStickersAsync(\n        IReadOnlyList<Sticker> stickers,\n        CancellationToken cancellationToken = default\n    )\n    {\n        if (!stickers.Any())\n            return;\n\n        await _writer.WriteLineAsync(\"{Stickers}\");\n\n        foreach (var sticker in stickers)\n        {\n            cancellationToken.ThrowIfCancellationRequested();\n\n            await _writer.WriteLineAsync(\n                await Context.ResolveAssetUrlAsync(sticker.SourceUrl, cancellationToken)\n            );\n        }\n\n        await _writer.WriteLineAsync();\n    }\n\n    private async ValueTask WriteReactionsAsync(\n        IReadOnlyList<Reaction> reactions,\n        CancellationToken cancellationToken = default\n    )\n    {\n        if (!reactions.Any())\n            return;\n\n        await _writer.WriteLineAsync(\"{Reactions}\");\n\n        foreach (var (i, reaction) in reactions.Index())\n        {\n            cancellationToken.ThrowIfCancellationRequested();\n\n            if (i > 0)\n            {\n                await _writer.WriteAsync(' ');\n            }\n\n            await _writer.WriteAsync(reaction.Emoji.Name);\n\n            if (reaction.Count > 1)\n            {\n                await _writer.WriteAsync($\" ({reaction.Count})\");\n            }\n        }\n\n        await _writer.WriteLineAsync();\n    }\n\n    public override async ValueTask WritePreambleAsync(\n        CancellationToken cancellationToken = default\n    )\n    {\n        await _writer.WriteLineAsync(new string('=', 62));\n        await _writer.WriteLineAsync($\"Guild: {Context.Request.Guild.Name}\");\n        await _writer.WriteLineAsync($\"Channel: {Context.Request.Channel.GetHierarchicalName()}\");\n\n        if (!string.IsNullOrWhiteSpace(Context.Request.Channel.Topic))\n        {\n            await _writer.WriteLineAsync($\"Topic: {Context.Request.Channel.Topic}\");\n        }\n\n        if (Context.Request.After is not null)\n        {\n            await _writer.WriteLineAsync(\n                $\"After: {Context.FormatDate(Context.Request.After.Value.ToDate())}\"\n            );\n        }\n\n        if (Context.Request.Before is not null)\n        {\n            await _writer.WriteLineAsync(\n                $\"Before: {Context.FormatDate(Context.Request.Before.Value.ToDate())}\"\n            );\n        }\n\n        await _writer.WriteLineAsync(new string('=', 62));\n        await _writer.WriteLineAsync();\n    }\n\n    private async ValueTask WriteForwardedMessageAsync(\n        MessageSnapshot forwardedMessage,\n        CancellationToken cancellationToken = default\n    )\n    {\n        await _writer.WriteLineAsync(\"{Forwarded Message}\");\n\n        if (!string.IsNullOrWhiteSpace(forwardedMessage.Content))\n        {\n            await _writer.WriteLineAsync(\n                await FormatMarkdownAsync(forwardedMessage.Content, cancellationToken)\n            );\n        }\n\n        await _writer.WriteLineAsync(\n            $\"Originally sent: {Context.FormatDate(forwardedMessage.Timestamp)}\"\n        );\n\n        await WriteAttachmentsAsync(forwardedMessage.Attachments, cancellationToken);\n        await WriteEmbedsAsync(forwardedMessage.Embeds, cancellationToken);\n        await WriteStickersAsync(forwardedMessage.Stickers, cancellationToken);\n\n        await _writer.WriteLineAsync();\n    }\n\n    public override async ValueTask WriteMessageAsync(\n        Message message,\n        CancellationToken cancellationToken = default\n    )\n    {\n        await base.WriteMessageAsync(message, cancellationToken);\n\n        // Header\n        await WriteMessageHeaderAsync(message);\n\n        // Content\n        if (message.IsSystemNotification)\n        {\n            await _writer.WriteLineAsync(message.GetFallbackContent());\n        }\n        else\n        {\n            await _writer.WriteLineAsync(\n                await FormatMarkdownAsync(message.Content, cancellationToken)\n            );\n        }\n\n        await _writer.WriteLineAsync();\n\n        // Forwarded message content\n        if (message.ForwardedMessage is not null)\n        {\n            await WriteForwardedMessageAsync(message.ForwardedMessage, cancellationToken);\n        }\n\n        // Attachments, embeds, reactions, etc.\n        await WriteAttachmentsAsync(message.Attachments, cancellationToken);\n        await WriteEmbedsAsync(message.Embeds, cancellationToken);\n        await WriteStickersAsync(message.Stickers, cancellationToken);\n        await WriteReactionsAsync(message.Reactions, cancellationToken);\n\n        await _writer.WriteLineAsync();\n    }\n\n    public override async ValueTask WritePostambleAsync(\n        CancellationToken cancellationToken = default\n    )\n    {\n        await _writer.WriteLineAsync(new string('=', 62));\n        await _writer.WriteLineAsync($\"Exported {MessagesWritten:N0} message(s)\");\n        await _writer.WriteLineAsync(new string('=', 62));\n    }\n\n    public override async ValueTask DisposeAsync()\n    {\n        await _writer.DisposeAsync();\n        await base.DisposeAsync();\n    }\n}\n"
  },
  {
    "path": "DiscordChatExporter.Core/Exporting/PostambleTemplate.cshtml",
    "content": "﻿@using System\n\n@inherits RazorBlade.HtmlTemplate\n\n@functions {\n    public required ExportContext Context { get; init; }\n\n    public required long MessagesWritten { get; init; }\n}\n\n@{\n    /* Close elements opened by preamble */\n}\n<!--wmm:ignore-->\n</div>\n<!--/wmm:ignore-->\n\n<div class=\"postamble\">\n    <div class=\"postamble__entry\">Exported @MessagesWritten.ToString(\"n0\", Context.Request.CultureInfo) message(s)</div>\n    <div class=\"postamble__entry\">Timezone: UTC@((Context.Request.IsUtcNormalizationEnabled ? 0 : TimeZoneInfo.Local.BaseUtcOffset.TotalHours).ToString(\"+#.#;-#.#;+0\", Context.Request.CultureInfo))</div>\n</div>\n\n</body>\n\n</html>"
  },
  {
    "path": "DiscordChatExporter.Core/Exporting/PreambleTemplate.cshtml",
    "content": "﻿@using System\n@using System.Threading.Tasks\n\n@inherits RazorBlade.HtmlTemplate\n\n@functions {\n    public required ExportContext Context { get; init; }\n\n    public required string ThemeName { get; init; }\n}\n\n@{\n    string Themed(string darkVariant, string lightVariant) =>\n        string.Equals(ThemeName, \"Dark\", StringComparison.OrdinalIgnoreCase)\n            ? darkVariant\n            : lightVariant;\n\n    string GetFontUrl(string style, int weight) =>\n        $\"https://cdn.jsdelivr.net/gh/Tyrrrz/DiscordFonts@master/ggsans-{style}-{weight}.woff2\";\n\n    ValueTask<string> ResolveAssetUrlAsync(string url) =>\n        Context.ResolveAssetUrlAsync(url, CancellationToken);\n\n    string FormatDate(DateTimeOffset instant, string format = \"g\") =>\n        Context.FormatDate(instant, format);\n\n    async ValueTask<string> FormatMarkdownAsync(string markdown) =>\n        Context.Request.ShouldFormatMarkdown\n            ? await HtmlMarkdownVisitor.FormatAsync(Context, markdown, true, CancellationToken)\n            : markdown;\n}\n\n<!DOCTYPE html>\n<html lang=\"en\">\n\n<head>\n    <title>@Context.Request.Guild.Name - @Context.Request.Channel.Name</title>\n    <meta charset=\"utf-8\">\n    <meta name=\"viewport\" content=\"width=device-width\">\n\n    @* Styling *@\n    <style>\n        @@font-face {\n            src: url(\"@await ResolveAssetUrlAsync(GetFontUrl(\"normal\", 400))\");\n            font-family: gg sans;\n            font-weight: 400;\n            font-style: normal;\n        }\n\n        @@font-face {\n            src: url(\"@await ResolveAssetUrlAsync(GetFontUrl(\"normal\", 500))\");\n            font-family: gg sans;\n            font-weight: 500;\n            font-style: normal;\n        }\n\n        @@font-face {\n            src: url(\"@await ResolveAssetUrlAsync(GetFontUrl(\"normal\", 600))\");\n            font-family: gg sans;\n            font-weight: 600;\n            font-style: normal;\n        }\n\n        @@font-face {\n            src: url(\"@await ResolveAssetUrlAsync(GetFontUrl(\"normal\", 700))\");\n            font-family: gg sans;\n            font-weight: 700;\n            font-style: normal;\n        }\n\n        @@font-face {\n            src: url(\"@await ResolveAssetUrlAsync(GetFontUrl(\"normal\", 800))\");\n            font-family: gg sans;\n            font-weight: 800;\n            font-style: normal;\n        }\n\n        @@font-face {\n            src: url(\"@await ResolveAssetUrlAsync(GetFontUrl(\"italic\", 400))\");\n            font-family: gg sans;\n            font-weight: 400;\n            font-style: italic;\n        }\n\n        @@font-face {\n            src: url(\"@await ResolveAssetUrlAsync(GetFontUrl(\"italic\", 500))\");\n            font-family: gg sans;\n            font-weight: 500;\n            font-style: italic;\n        }\n\n        @@font-face {\n            src: url(\"@await ResolveAssetUrlAsync(GetFontUrl(\"italic\", 600))\");\n            font-family: gg sans;\n            font-weight: 600;\n            font-style: italic;\n        }\n\n        @@font-face {\n            src: url(\"@await ResolveAssetUrlAsync(GetFontUrl(\"italic\", 700))\");\n            font-family: gg sans;\n            font-weight: 700;\n            font-style: italic;\n        }\n\n        @@font-face {\n            src: url(\"@await ResolveAssetUrlAsync(GetFontUrl(\"italic\", 800))\");\n            font-family: gg sans;\n            font-weight: 800;\n            font-style: italic;\n        }\n\n        html, body {\n            margin: 0;\n            padding: 0;\n            background-color: @Themed(\"#36393e\", \"#ffffff\");\n            color: @Themed(\"#dcddde\", \"#23262a\");\n            font-family: \"gg sans\", \"Helvetica Neue\", Helvetica, Arial, sans-serif;\n            font-size: 17px;\n            font-weight: @Themed(\"400\", \"500\");\n            scroll-behavior: smooth;\n        }\n\n        a {\n            color: @Themed(\"#00aff4\", \"#0068e0\");\n            text-decoration: none;\n        }\n\n        a:hover {\n            text-decoration: underline;\n        }\n\n        img {\n            object-fit: contain;\n            image-rendering: high-quality;\n            image-rendering: -webkit-optimize-contrast;\n        }\n\n        .preamble {\n            display: grid;\n            grid-template-columns: auto 1fr;\n            max-width: 100%;\n            padding: 1rem;\n        }\n\n        .preamble__guild-icon-container {\n            grid-column: 1;\n        }\n\n        .preamble__guild-icon {\n            max-width: 88px;\n            max-height: 88px;\n        }\n\n        .preamble__entries-container {\n            grid-column: 2;\n            margin-left: 1rem;\n        }\n\n        .preamble__entry {\n            margin-bottom: 0.15rem;\n            color: @Themed(\"#ffffff\", \"#2f3136\");\n            font-size: 1.4rem;\n        }\n\n        .preamble__entry--small {\n            font-size: 1rem;\n        }\n\n        .chatlog {\n            padding: 1rem 0;\n            width: 100%;\n            border-top: 1px solid @Themed(\"rgba(255, 255, 255, 0.1)\", \"#eceeef\");\n            border-bottom: 1px solid @Themed(\"rgba(255, 255, 255, 0.1)\", \"#eceeef\");\n        }\n\n        .chatlog__message-group {\n            margin-bottom: 1rem;\n        }\n\n        .chatlog__message-container {\n            background-color: transparent;\n            transition: background-color 1s ease;\n        }\n\n        .chatlog__message-container--highlighted {\n            background-color: @Themed(\"rgba(114, 137, 218, 0.2)\", \"rgba(114, 137, 218, 0.2)\");\n        }\n\n        .chatlog__message-container--pinned {\n            background-color: @Themed(\"rgba(249, 168, 37, 0.05)\", \"rgba(249, 168, 37, 0.05)\");\n        }\n\n        .chatlog__message {\n            display: grid;\n            grid-template-columns: auto 1fr;\n            padding: 0.15rem 0;\n            direction: ltr;\n            unicode-bidi: bidi-override;\n        }\n\n        .chatlog__message:hover {\n            background-color: @Themed(\"#32353b\", \"#fafafa\");\n        }\n\n        .chatlog__message:hover .chatlog__short-timestamp {\n            display: block;\n        }\n\n        .chatlog__message-aside {\n            grid-column: 1;\n            width: 72px;\n            padding: 0.15rem 0.15rem 0 0.15rem;\n            text-align: center;\n        }\n\n        .chatlog__reply-symbol {\n            height: 10px;\n            margin: 6px 4px 4px 36px;\n            border-left: 2px solid @Themed(\"#4f545c\", \"#c7ccd1\");\n            border-top: 2px solid @Themed(\"#4f545c\", \"#c7ccd1\");\n            border-radius: 8px 0 0 0;\n        }\n\n        .chatlog__avatar {\n            width: 40px;\n            height: 40px;\n            border-radius: 50%;\n        }\n\n        .chatlog__short-timestamp {\n            display: none;\n            color: @Themed(\"#a3a6aa\", \"#5e6772\");\n            font-size: 0.75rem;\n            font-weight: 500;\n            direction: ltr;\n            unicode-bidi: bidi-override;\n        }\n\n        .chatlog__message-primary {\n            grid-column: 2;\n            min-width: 0;\n        }\n\n        .chatlog__reply {\n            display: flex;\n            margin-bottom: 0.15rem;\n            align-items: center;\n            color: @Themed(\"#b5b6b8\", \"#5f5f60\");\n            font-size: 0.875rem;\n            white-space: nowrap;\n            overflow: hidden;\n            text-overflow: ellipsis;\n        }\n\n        .chatlog__reply-avatar {\n            width: 16px;\n            height: 16px;\n            margin-right: 0.25rem;\n            border-radius: 50%;\n        }\n\n        .chatlog__reply-author {\n            margin-right: 0.3rem;\n            font-weight: 600;\n        }\n\n        .chatlog__reply-content {\n            overflow: hidden;\n            text-overflow: ellipsis;\n        }\n\n        .chatlog__reply-link {\n            cursor: pointer;\n        }\n\n        .chatlog__reply-link * {\n            display: inline;\n            pointer-events: none;\n        }\n\n        .chatlog__reply-link .chatlog__markdown-quote {\n            display: inline;\n        }\n\n        .chatlog__reply-link .chatlog__markdown-pre {\n            display: inline;\n        }\n\n        .chatlog__reply-link:hover {\n            color: @Themed(\"#ffffff\", \"#2f3136\");\n        }\n\n        .chatlog__reply-link:hover *:not(.chatlog__markdown-spoiler) {\n            color: inherit;\n        }\n\n        .chatlog__reply-edited-timestamp {\n            margin-left: 0.25rem;\n            color: @Themed(\"#a3a6aa\", \"#5e6772\");\n            font-size: 0.75rem;\n            font-weight: 500;\n            direction: ltr;\n            unicode-bidi: bidi-override;\n        }\n\n        .chatlog__forwarded {\n            display: flex;\n            flex-direction: column;\n            margin-bottom: 0.15rem;\n            padding: 0.5rem;\n            border-left: 4px solid @Themed(\"#4f545c\", \"#c7ccd1\");\n            border-radius: 4px;\n            background-color: @Themed(\"rgba(46, 48, 54, 0.3)\", \"rgba(249, 249, 249, 0.3)\");\n        }\n\n        .chatlog__forwarded-header {\n            display: flex;\n            align-items: center;\n            margin-bottom: 0.25rem;\n            color: @Themed(\"#b5b6b8\", \"#5f5f60\");\n            font-size: 0.75rem;\n            font-weight: 600;\n        }\n\n        .chatlog__forwarded-icon {\n            width: 16px;\n            height: 16px;\n            margin-right: 0.25rem;\n        }\n\n        .chatlog__forwarded-content {\n            color: @Themed(\"#dcddde\", \"#2e3338\");\n            font-size: 0.95rem;\n        }\n\n        .chatlog__forwarded-attachments {\n            margin-top: 0.3rem;\n        }\n\n        .chatlog__forwarded-attachment {\n            max-width: 300px;\n            max-height: 200px;\n            border-radius: 3px;\n        }\n\n        .chatlog__forwarded-timestamp {\n            margin-top: 0.25rem;\n            color: @Themed(\"#a3a6aa\", \"#5e6772\");\n            font-size: 0.75rem;\n        }\n\n        .chatlog__system-notification-icon {\n            width: 18px;\n            height: 18px;\n        }\n\n        .chatlog__system-notification-author {\n            font-weight: @Themed(\"500\", \"600\");\n            color: @Themed(\"#ffffff\", \"#2f3136\");\n        }\n\n        .chatlog__system-notification-content {\n            color: @Themed(\"#96989d\", \"#5e6772\")\n        }\n\n        .chatlog__system-notification-link {\n            font-weight: 500;\n            color: @Themed(\"#ffffff\", \"#2f3136\");\n        }\n\n        .chatlog__system-notification-timestamp {\n            margin-left: 0.3rem;\n            color: @Themed(\"#a3a6aa\", \"#5e6772\");\n            font-size: 0.75rem;\n            font-weight: 500;\n            direction: ltr;\n            unicode-bidi: bidi-override;\n        }\n\n        .chatlog__system-notification-timestamp a {\n            color: inherit;\n        }\n\n        .chatlog__header {\n            margin-bottom: 0.1rem;\n        }\n\n        .chatlog__author {\n            font-weight: @Themed(\"500\", \"600\");\n            color: @Themed(\"#ffffff\", \"#2f3136\");\n        }\n\n        .chatlog__author-tag {\n            position: relative;\n            top: -0.1rem;\n            margin-left: 0.3rem;\n            padding: 0.05rem 0.3rem;\n            border-radius: 3px;\n            background-color: #5865F2;\n            color: #ffffff;\n            font-size: 0.625rem;\n            font-weight: 500;\n            line-height: 1.3;\n        }\n\n        .chatlog__timestamp {\n            margin-left: 0.3rem;\n            color: @Themed(\"#a3a6aa\", \"#5e6772\");\n            font-size: 0.75rem;\n            font-weight: 500;\n            direction: ltr;\n            unicode-bidi: bidi-override;\n        }\n\n        .chatlog__timestamp a {\n            color: inherit;\n        }\n\n        .chatlog__content {\n            padding-right: 1rem;\n            font-size: 0.95rem;\n            word-wrap: break-word;\n        }\n\n        .chatlog__edited-timestamp {\n            margin-left: 0.15rem;\n            color: @Themed(\"#a3a6aa\", \"#5e6772\");\n            font-size: 0.75rem;\n            font-weight: 500;\n        }\n\n        .chatlog__attachment {\n            position: relative;\n            width: fit-content;\n            margin-top: 0.3rem;\n            border-radius: 3px;\n            overflow: hidden;\n        }\n\n        .chatlog__attachment--hidden {\n            cursor: pointer;\n            box-shadow: 0 0 1px 1px rgba(0, 0, 0, 0.1);\n        }\n\n        .chatlog__attachment--hidden * {\n            pointer-events: none;\n        }\n\n        .chatlog__attachment-spoiler-caption {\n            display: none;\n            position: absolute;\n            left: 50%;\n            top: 50%;\n            z-index: 999;\n            padding: 0.4rem 0.8rem;\n            border-radius: 20px;\n            transform: translate(-50%, -50%);\n            background-color: rgba(0, 0, 0, 0.9);\n            color: #dcddde;\n            font-size: 0.9rem;\n            font-weight: 600;\n            letter-spacing: 0.05rem;\n        }\n\n        .chatlog__attachment--hidden .chatlog__attachment-spoiler-caption {\n            display: block;\n        }\n\n        .chatlog__attachment--hidden:hover .chatlog__attachment-spoiler-caption {\n            color: #fff;\n        }\n\n        .chatlog__attachment-media {\n            max-width: 45vw;\n            max-height: 500px;\n            vertical-align: top;\n            border-radius: 3px;\n        }\n\n        .chatlog__attachment--hidden .chatlog__attachment-media {\n            filter: blur(44px);\n        }\n\n        .chatlog__attachment-generic {\n            max-width: 520px;\n            width: 100%;\n            height: 40px;\n            padding: 10px;\n            border: 1px solid @Themed(\"#292b2f\", \"#ebedef\");\n            border-radius: 3px;\n            background-color: @Themed(\"#2f3136\", \"#f2f3f5\");\n            overflow: hidden;\n        }\n\n        .chatlog__attachment--hidden .chatlog__attachment-generic {\n            filter: blur(44px);\n        }\n\n        .chatlog__attachment-generic-icon {\n            float: left;\n            width: 30px;\n            height: 100%;\n            margin-right: 10px;\n        }\n\n        .chatlog__attachment-generic-size {\n            color: #72767d;\n            font-size: 12px;\n        }\n\n        .chatlog__attachment-generic-name {\n            overflow: hidden;\n            white-space: nowrap;\n            text-overflow: ellipsis;\n        }\n\n        .chatlog__embed {\n            display: flex;\n            margin-top: 0.3rem;\n            max-width: 520px;\n        }\n\n        .chatlog__embed-color-pill {\n            flex-shrink: 0;\n            width: 0.25rem;\n            border-top-left-radius: 3px;\n            border-bottom-left-radius: 3px;\n        }\n\n        .chatlog__embed-color-pill--default {\n            background-color: @Themed(\"#202225\", \"rgba(227, 229, 232, 1)\");\n        }\n\n        .chatlog__embed-content-container {\n            display: flex;\n            flex-direction: column;\n            padding: 0.5rem 0.6rem;\n            border: 1px solid @Themed(\"rgba(46, 48, 54, 0.6)\", \"rgba(204, 204, 204, 0.3)\");\n            border-top-right-radius: 3px;\n            border-bottom-right-radius: 3px;\n            background-color: @Themed(\"rgba(46, 48, 54, 0.3)\", \"rgba(249, 249, 249, 0.3)\");\n        }\n\n        .chatlog__embed-content {\n            display: flex;\n            width: 100%;\n        }\n\n        .chatlog__embed-text {\n            flex: 1;\n        }\n\n        .chatlog__embed-author-container {\n            display: flex;\n            margin-bottom: 0.5rem;\n            align-items: center;\n        }\n\n        .chatlog__embed-author-icon {\n            width: 20px;\n            height: 20px;\n            margin-right: 0.5rem;\n            border-radius: 50%;\n        }\n\n        .chatlog__embed-author {\n            color: @Themed(\"#ffffff\", \"#4f545c\");\n            font-size: 0.875rem;\n            font-weight: 600;\n            direction: ltr;\n            unicode-bidi: bidi-override;\n        }\n\n        .chatlog__embed-author-link {\n            color: @Themed(\"#ffffff\", \"#4f545c\");\n        }\n\n        .chatlog__embed-title {\n            margin-bottom: 0.5rem;\n            color: @Themed(\"#ffffff\", \"#4f545c\");\n            font-size: 0.875rem;\n            font-weight: 600;\n        }\n\n        .chatlog__embed-description {\n            color: @Themed(\"#dcddde\", \"#2e3338\");\n            font-weight: 500;\n            font-size: 0.85rem;\n        }\n\n        .chatlog__embed-fields {\n            display: flex;\n            flex-wrap: wrap;\n            gap: 0 0.5rem;\n        }\n\n        .chatlog__embed-field {\n            flex: 0;\n            min-width: 100%;\n            max-width: 506px;\n            padding-top: 0.6rem;\n            font-size: 0.875rem;\n        }\n\n        .chatlog__embed-field--inline {\n            flex: 1;\n            flex-basis: auto;\n            min-width: 50px;\n        }\n\n        .chatlog__embed-field-name {\n            margin-bottom: 0.2rem;\n            color: @Themed(\"#ffffff\", \"#36393e\");\n            font-weight: 600;\n        }\n\n        .chatlog__embed-field-value {\n            color: @Themed(\"#dcddde\", \"#2e3338\");\n            font-weight: 500;\n        }\n\n        .chatlog__embed-thumbnail {\n            flex: 0;\n            max-width: 80px;\n            max-height: 80px;\n            margin-left: 1.2rem;\n            border-radius: 3px;\n        }\n\n        .chatlog__embed-images {\n            display: grid;\n            margin-top: 0.6rem;\n            grid-template-columns: repeat(2, 1fr);\n            gap: 0.25rem;\n        }\n\n        .chatlog__embed-images--single {\n            display: block;\n        }\n\n        .chatlog__embed-image {\n            object-fit: cover;\n            object-position: center;\n            max-width: 500px;\n            max-height: 400px;\n            width: 100%;\n            height: 100%;\n            border-radius: 3px;\n        }\n\n        .chatlog__embed-footer {\n            margin-top: 0.6rem;\n            color: @Themed(\"#dcddde\", \"#2e3338\");\n        }\n\n        .chatlog__embed-footer-icon {\n            width: 20px;\n            height: 20px;\n            margin-right: 0.2rem;\n            border-radius: 50%;\n            vertical-align: middle;\n        }\n\n        .chatlog__embed-footer-text {\n            vertical-align: middle;\n            font-size: 0.75rem;\n            font-weight: 500;\n        }\n\n        .chatlog__embed-invite-container {\n            min-width: 320px;\n            padding: 0.6rem 0.7rem;\n            border: 1px solid @Themed(\"rgba(46, 48, 54, 0.6)\", \"rgba(204, 204, 204, 0.3)\");\n            border-radius: 3px;\n            background-color: @Themed(\"rgba(46, 48, 54, 0.3)\", \"rgba(249, 249, 249, 0.3)\");\n        }\n\n        .chatlog__embed-invite-title {\n            margin: 0 0 0.8rem 0;\n            color: @Themed(\"#b9bbbe\", \"#4f5660\");\n            font-size: 0.75rem;\n            font-weight: 700;\n            text-transform: uppercase;\n        }\n\n        .chatlog__embed-invite {\n            display: flex;\n        }\n\n        .chatlog__embed-invite-guild-icon {\n            width: 50px;\n            height: 50px;\n            border-radius: 0.85rem;\n        }\n\n        .chatlog__embed-invite-info {\n            margin-left: 1rem;\n        }\n\n        .chatlog__embed-invite-guild-name {\n            color: @Themed(\"#ffffff\", \"#36393e\");\n            font-weight: 600;\n        }\n\n        .chatlog__embed-invite-guild-name a {\n            color: inherit;\n        }\n\n        .chatlog__embed-invite-channel-icon {\n            width: 18px;\n            height: 18px;\n            vertical-align: bottom;\n        }\n\n        .chatlog__embed-invite-channel-name {\n            font-size: 0.9rem;\n            font-weight: 600;\n        }\n\n        .chatlog__embed-generic-image {\n            object-fit: contain;\n            object-position: left;\n            max-width: 45vw;\n            max-height: 500px;\n            vertical-align: top;\n            border-radius: 3px;\n        }\n\n        .chatlog__embed-generic-video {\n            object-fit: contain;\n            object-position: left;\n            max-width: 45vw;\n            max-height: 500px;\n            vertical-align: top;\n            border-radius: 3px;\n        }\n\n        .chatlog__embed-generic-gifv {\n            object-fit: contain;\n            object-position: left;\n            max-width: 45vw;\n            max-height: 500px;\n            vertical-align: top;\n            border-radius: 3px;\n        }\n\n        .chatlog__embed-spotify {\n            border: 0;\n        }\n        \n        .chatlog__embed-twitch {\n            border: 0;\n        }\n\n        .chatlog__embed-youtube-container {\n            margin-top: 0.6rem;\n        }\n\n        .chatlog__embed-youtube-thumbnail {\n            max-width: 400px;\n            max-height: 225px;\n            border-radius: 3px;\n            cursor: pointer;\n        }\n\n        .chatlog__sticker {\n            width: 180px;\n            height: 180px;\n        }\n\n        .chatlog__sticker--media {\n            max-width: 100%;\n            max-height: 100%;\n        }\n\n        .chatlog__reactions {\n            display: flex;\n        }\n\n        .chatlog__reaction {\n            display: flex;\n            margin: 0.35rem 0.1rem 0.1rem 0;\n            padding: 0.125rem 0.375rem;\n            border: 1px solid transparent;\n            border-radius: 8px;\n            background-color: @Themed(\"#2f3136\", \"#f2f3f5\");\n            align-items: center;\n        }\n\n        .chatlog__reaction:hover {\n            border: 1px solid @Themed(\"hsla(0,0%,100%,.2)\", \"rgba(0, 0, 0, 0.2)\");\n            background-color: @Themed(\"transparent\", \"white\");\n        }\n\n        .chatlog__reaction-count {\n            min-width: 9px;\n            margin-left: 0.35rem;\n            color: @Themed(\"#b9bbbe\", \"#4f5660\");\n            font-size: 0.875rem;\n        }\n\n        .chatlog__reaction:hover .chatlog__reaction-count {\n            color: @Themed(\"#dcddde\", \"#2e3338\");\n        }\n\n        .chatlog__markdown {\n            max-width: 100%;\n            line-height: 1.3;\n            overflow-wrap: break-word;\n        }\n\n        .chatlog__markdown h1 {\n            margin: 1rem 0 0.5rem;\n            color: @Themed(\"#f2f3f5\", \"#060607\");\n            font-size: 1.5rem;\n            line-height: 1;\n        }\n\n        .chatlog__markdown h2 {\n            margin: 1rem 0 0.5rem;\n            color: @Themed(\"#f2f3f5\", \"#060607\");\n            font-size: 1.25rem;\n            line-height: 1;\n        }\n\n        .chatlog__markdown h3 {\n            margin: 1rem 0 0.5rem;\n            color: @Themed(\"#f2f3f5\", \"#060607\");\n            font-size: 1rem;\n            line-height: 1;\n        }\n\n        .chatlog__markdown h1:first-child, h2:first-child, h3:first-child {\n            margin-top: 0.5rem;\n        }\n\n        .chatlog__markdown ul, ol {\n            margin: 0 0 0 1rem;\n            padding: 0;\n        }\n\n        .chatlog__markdown-preserve {\n            white-space: pre-wrap;\n        }\n\n        .chatlog__markdown-spoiler {\n            background-color: @Themed(\"rgba(255, 255, 255, 0.1)\", \"rgba(0, 0, 0, 0.1)\");\n            padding: 0 2px;\n            border-radius: 3px;\n        }\n\n        .chatlog__markdown-spoiler--hidden {\n            cursor: pointer;\n            background-color: @Themed(\"#202225\", \"#b9bbbe\");\n            color: rgba(0, 0, 0, 0);\n        }\n\n        .chatlog__markdown-spoiler--hidden:hover {\n            background-color: @Themed(\"rgba(32, 34, 37, 0.8)\", \"rgba(185, 187, 190, 0.8)\");\n        }\n\n        .chatlog__markdown-spoiler--hidden::selection {\n            color: rgba(0, 0, 0, 0);\n        }\n\n        .chatlog__markdown-quote {\n            display: flex;\n            margin: 0.05rem 0;\n        }\n\n        .chatlog__markdown-quote-border {\n            margin-right: 0.5rem;\n            border: 2px solid @Themed(\"#4f545c\", \"#c7ccd1\");\n            border-radius: 3px;\n        }\n\n        .chatlog__markdown-pre {\n            background-color: @Themed(\"#2f3136\", \"#f9f9f9\");\n            font-family: \"Consolas\", \"Courier New\", Courier, monospace;\n            font-size: 0.85rem;\n            text-decoration: inherit;\n        }\n\n        .chatlog__markdown-pre--multiline {\n            display: block;\n            margin-top: 0.25rem;\n            padding: 0.5rem;\n            border: 2px solid @Themed(\"#282b30\", \"#f3f3f3\");\n            border-radius: 5px;\n            color: @Themed(\"#b9bbbe\", \"#657b83\");\n        }\n\n        .chatlog__markdown-pre--multiline.hljs {\n            background-color: @Themed(\"#2f3136\", \"#f9f9f9\");\n            color: @Themed(\"#b9bbbe\", \"#657b83\");\n        }\n\n        .chatlog__markdown-pre--inline {\n            display: inline-block;\n            padding: 2px;\n            border-radius: 3px;\n        }\n\n        .chatlog__markdown-mention {\n            border-radius: 3px;\n            padding: 0 2px;\n            background-color: @Themed(\"rgba(88, 101, 242, .3)\", \"rgba(88, 101, 242, .15)\");\n            color: @Themed(\"#dee0fc\", \"#505cdc\");\n            font-weight: 500;\n        }\n\n        .chatlog__markdown-mention:hover {\n            background-color: #5865f2;\n            color: #ffffff\n        }\n\n        .chatlog__markdown-timestamp {\n            background-color: @Themed(\"rgba(255, 255, 255, 0.1)\", \"rgba(0, 0, 0, 0.1)\");\n            padding: 0 2px;\n            border-radius: 3px;\n        }\n\n        .chatlog__emoji {\n            width: 1.325rem;\n            height: 1.325rem;\n            margin: 0 0.06rem;\n            vertical-align: -0.4rem;\n        }\n\n        .chatlog__emoji--small {\n            width: 1rem;\n            height: 1rem;\n        }\n\n        .chatlog__emoji--large {\n            width: 2.8rem;\n            height: 2.8rem;\n        }\n\n        .postamble {\n            padding: 1.25rem;\n        }\n\n        .postamble__entry {\n            color: @Themed(\"#ffffff\", \"#2f3136\");\n        }\n    </style>\n\n    @* Syntax highlighting *@\n    <link rel=\"stylesheet\" href=\"@await ResolveAssetUrlAsync($\"https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.15.6/styles/solarized-{ThemeName.ToLowerInvariant()}.min.css\")\">\n    <script src=\"@await ResolveAssetUrlAsync(\"https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.15.6/highlight.min.js\")\"></script>\n    <script>\n        document.addEventListener('DOMContentLoaded', () => {\n            document.querySelectorAll('.chatlog__markdown-pre--multiline').forEach(e => hljs.highlightBlock(e));\n        });\n    </script>\n\n    @* Lottie animation support *@\n    <script src=\"@await ResolveAssetUrlAsync(\"https://cdnjs.cloudflare.com/ajax/libs/lottie-web/5.8.1/lottie.min.js\")\"></script>\n    <script>\n        document.addEventListener('DOMContentLoaded', () => {\n            document.querySelectorAll('.chatlog__sticker--media[data-source]').forEach(e => {\n                const anim = lottie.loadAnimation({\n                    container: e,\n                    renderer: 'svg',\n                    loop: true,\n                    autoplay: true,\n                    path: e.getAttribute('data-source')\n                });\n\n                anim.addEventListener(\n                    'data_failed',\n                    () => e.innerHTML = '<strong>[Sticker cannot be rendered]</strong>'\n                );\n            });\n        });\n    </script>\n\n    @* Scripts *@\n    <script>\n        function scrollToMessage(event, id) {\n            const element = document.getElementById('chatlog__message-container-' + id);\n            if (!element)\n                return;\n\n            event.preventDefault();\n            element.classList.add('chatlog__message-container--highlighted');\n\n            window.scrollTo({\n                top: element.getBoundingClientRect().top - document.body.getBoundingClientRect().top - (window.innerHeight / 2),\n                behavior: 'smooth'\n            });\n\n            window.setTimeout(\n                () => element.classList.remove('chatlog__message-container--highlighted'),\n                2000\n            );\n        }\n\n        function showSpoiler(event, element) {\n            if (!element)\n                return;\n\n            if (element.classList.contains('chatlog__attachment--hidden')) {\n                event.preventDefault();\n                element.classList.remove('chatlog__attachment--hidden');\n            }\n\n            if (element.classList.contains('chatlog__markdown-spoiler--hidden')) {\n                event.preventDefault();\n                element.classList.remove('chatlog__markdown-spoiler--hidden');\n            }\n        }\n    </script>\n\n    @* Icons *@\n    <svg style=\"display: none\" xmlns=\"http://www.w3.org/2000/svg\">\n        <defs>\n            <symbol id=\"attachment-icon\" viewBox=\"0 0 720 960\">\n                <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\" />\n                <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\" />\n                <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\" />\n                <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\" />\n            </symbol>\n            <symbol id=\"join-icon\" viewBox=\"0 0 18 18\">\n                <path fill=\"#3ba55c\" d=\"m0 8h14.2l-3.6-3.6 1.4-1.4 6 6-6 6-1.4-1.4 3.6-3.6h-14.2\" />\n            </symbol>\n            <symbol id=\"leave-icon\" viewBox=\"0 0 18 18\">\n                <path fill=\"#ed4245\" d=\"m3.8 8 3.6-3.6-1.4-1.4-6 6 6 6 1.4-1.4-3.6-3.6h14.2v-2\" />\n            </symbol>\n            <symbol id=\"call-icon\" viewBox=\"0 0 18 18\">\n                <path fill=\"#3ba55c\" fill-rule=\"evenodd\" d=\"M17.7163041 15.36645368c-.0190957.02699568-1.9039523 2.6680735-2.9957762 2.63320406-3.0676659-.09785935-6.6733809-3.07188394-9.15694343-5.548738C3.08002193 9.9740657.09772497 6.3791404 0 3.3061316v-.024746C0 2.2060575 2.61386252.3152347 2.64082114.2972376c.7110335-.4971705 1.4917101-.3149497 1.80959713.1372281.19320342.2744561 2.19712724 3.2811005 2.42290565 3.6489167.09884826.1608492.14714912.3554431.14714912.5702838 0 .2744561-.07975258.5770327-.23701117.8751101-.1527655.2902036-.65262318 1.1664385-.89862055 1.594995.2673396.3768148.94804468 1.26429792 2.351016 2.66357424 1.39173858 1.39027775 2.28923588 2.07641807 2.67002628 2.34187563.4302146-.2452108 1.3086162-.74238132 1.5972981-.89423205.5447887-.28682915 1.0907006-.31944893 1.4568885-.08661115.3459689.2182151 3.3383754 2.21027167 3.6225641 2.41611376.2695862.19234426.4144887.5399137.4144887.91672846 0 .2969525-.089862.61190215-.2808189.88523346\" />\n            </symbol>\n            <symbol id=\"pencil-icon\" viewBox=\"0 0 18 18\">\n                <path fill=\"#99aab5\" d=\"m0 14.25v3.75h3.75l11.06-11.06-3.75-3.75zm17.71-10.21c.39-.39.39-1.02 0-1.41l-2.34-2.34c-.39-.39-1.02-.39-1.41 0l-1.83 1.83 3.75 3.75z\" />\n            </symbol>\n            <symbol id=\"pin-icon\" viewBox=\"0 0 18 18\">\n                <path fill=\"#b9bbbe\" d=\"m16.908 8.39684-8.29587-8.295827-1.18584 1.184157 1.18584 1.18584-4.14834 4.1475v.00167l-1.18583-1.18583-1.185 1.18583 3.55583 3.55502-4.740831 4.74 1.185001 1.185 4.74083-4.74 3.55581 3.555 1.185-1.185-1.185-1.185 4.1475-4.14836h.0009l1.185 1.185z\" />\n            </symbol>\n            <symbol id=\"channel-icon\" viewBox=\"0 0 24 24\">\n                <path fill=\"#b9bbbe\" d=\"M5.88657 21C5.57547 21 5.3399 20.7189 5.39427 20.4126L6.00001 17H2.59511C2.28449 17 2.04905 16.7198 2.10259 16.4138L2.27759 15.4138C2.31946 15.1746 2.52722 15 2.77011 15H6.35001L7.41001 9H4.00511C3.69449 9 3.45905 8.71977 3.51259 8.41381L3.68759 7.41381C3.72946 7.17456 3.93722 7 4.18011 7H7.76001L8.39677 3.41262C8.43914 3.17391 8.64664 3 8.88907 3H9.87344C10.1845 3 10.4201 3.28107 10.3657 3.58738L9.76001 7H15.76L16.3968 3.41262C16.4391 3.17391 16.6466 3 16.8891 3H17.8734C18.1845 3 18.4201 3.28107 18.3657 3.58738L17.76 7H21.1649C21.4755 7 21.711 7.28023 21.6574 7.58619L21.4824 8.58619C21.4406 8.82544 21.2328 9 20.9899 9H17.41L16.35 15H19.7549C20.0655 15 20.301 15.2802 20.2474 15.5862L20.0724 16.5862C20.0306 16.8254 19.8228 17 19.5799 17H16L15.3632 20.5874C15.3209 20.8261 15.1134 21 14.8709 21H13.8866C13.5755 21 13.3399 20.7189 13.3943 20.4126L14 17H8.00001L7.36325 20.5874C7.32088 20.8261 7.11337 21 6.87094 21H5.88657ZM9.41045 9L8.35045 15H14.3504L15.4104 9H9.41045Z\" />\n            </symbol>\n            <symbol id=\"thread-icon\" viewBox=\"0 0 24 24\">\n                <path fill=\"#b9bbbe\" d=\"M5.43309 21C5.35842 21 5.30189 20.9325 5.31494 20.859L5.99991 17H2.14274C2.06819 17 2.01168 16.9327 2.02453 16.8593L2.33253 15.0993C2.34258 15.0419 2.39244 15 2.45074 15H6.34991L7.40991 9H3.55274C3.47819 9 3.42168 8.93274 3.43453 8.85931L3.74253 7.09931C3.75258 7.04189 3.80244 7 3.86074 7H7.75991L8.45234 3.09903C8.46251 3.04174 8.51231 3 8.57049 3H10.3267C10.4014 3 10.4579 3.06746 10.4449 3.14097L9.75991 7H15.7599L16.4523 3.09903C16.4625 3.04174 16.5123 3 16.5705 3H18.3267C18.4014 3 18.4579 3.06746 18.4449 3.14097L17.7599 7H21.6171C21.6916 7 21.7481 7.06725 21.7353 7.14069L21.4273 8.90069C21.4172 8.95811 21.3674 9 21.3091 9H17.4099L17.0495 11.04H15.05L15.4104 9H9.41035L8.35035 15H10.5599V17H7.99991L7.30749 20.901C7.29732 20.9583 7.24752 21 7.18934 21H5.43309Z\" />\n                <path fill=\"#b9bbbe\" d=\"M13.4399 12.96C12.9097 12.96 12.4799 13.3898 12.4799 13.92V20.2213C12.4799 20.7515 12.9097 21.1813 13.4399 21.1813H14.3999C14.5325 21.1813 14.6399 21.2887 14.6399 21.4213V23.4597C14.6399 23.6677 14.8865 23.7773 15.0408 23.6378L17.4858 21.4289C17.6622 21.2695 17.8916 21.1813 18.1294 21.1813H22.5599C23.0901 21.1813 23.5199 20.7515 23.5199 20.2213V13.92C23.5199 13.3898 23.0901 12.96 22.5599 12.96H13.4399Z\" />\n            </symbol>\n            <symbol id=\"forward-icon\" viewBox=\"0 0 24 24\">\n                <path fill=\"#b9bbbe\" d=\"M13 4L21 12L13 20V15C8 15 4 17 1 22C2 16 6 10 13 9V4Z\" />\n            </symbol>\n        </defs>\n    </svg>\n</head>\n<body>\n\n<div class=\"preamble\">\n    <div class=\"preamble__guild-icon-container\">\n        <img class=\"preamble__guild-icon\" src=\"@await ResolveAssetUrlAsync(Context.Request.Channel.IconUrl ?? Context.Request.Guild.IconUrl)\" alt=\"Guild icon\" loading=\"lazy\">\n    </div>\n    <div class=\"preamble__entries-container\">\n        <div class=\"preamble__entry\">@Context.Request.Guild.Name</div>\n        <div class=\"preamble__entry\">@Context.Request.Channel.GetHierarchicalName()</div>\n\n        @if (!string.IsNullOrWhiteSpace(Context.Request.Channel.Topic))\n        {\n            <div class=\"preamble__entry preamble__entry--small\">@Html.Raw(await FormatMarkdownAsync(Context.Request.Channel.Topic))</div>\n        }\n\n        @if (Context.Request.After is not null || Context.Request.Before is not null)\n        {\n            <div class=\"preamble__entry preamble__entry--small\">\n                @if (Context.Request.After is not null && Context.Request.Before is not null)\n                {\n                    @($\"Between {FormatDate(Context.Request.After.Value.ToDate())} and {FormatDate(Context.Request.Before.Value.ToDate())}\")\n                }\n                else if (Context.Request.After is not null)\n                {\n                    @($\"After {FormatDate(Context.Request.After.Value.ToDate())}\")\n                }\n                else if (Context.Request.Before is not null)\n                {\n                    @($\"Before {FormatDate(Context.Request.Before.Value.ToDate())}\")\n                }\n            </div>\n        }\n    </div>\n</div>\n\n@* Preamble cuts off at this point *@\n<!--wmm:ignore-->\n<div class=\"chatlog\">\n<!--/wmm:ignore-->"
  },
  {
    "path": "DiscordChatExporter.Core/Markdown/EmojiNode.cs",
    "content": "﻿using DiscordChatExporter.Core.Discord;\nusing DiscordChatExporter.Core.Discord.Data;\n\nnamespace DiscordChatExporter.Core.Markdown;\n\ninternal record EmojiNode(\n    // Only present on custom emoji\n    Snowflake? Id,\n    // Name of a custom emoji (e.g. LUL) or actual representation of a standard emoji (e.g. 🙂)\n    string Name,\n    bool IsAnimated\n) : MarkdownNode\n{\n    // This coupling is unsound from the domain-design perspective, but it helps us reuse\n    // some code for now. We can refactor this later, if the coupling becomes a problem.\n    private readonly Emoji _emoji = new(Id, Name, IsAnimated);\n\n    public EmojiNode(string name)\n        : this(null, name, false) { }\n\n    public bool IsCustomEmoji => _emoji.IsCustomEmoji;\n\n    // Name of a custom emoji (e.g. LUL) or name of a standard emoji (e.g. slight_smile)\n    public string Code => _emoji.Code;\n\n    public string ImageUrl => _emoji.ImageUrl;\n}\n"
  },
  {
    "path": "DiscordChatExporter.Core/Markdown/FormattingKind.cs",
    "content": "﻿namespace DiscordChatExporter.Core.Markdown;\n\ninternal enum FormattingKind\n{\n    Bold,\n    Italic,\n    Underline,\n    Strikethrough,\n    Spoiler,\n    Quote,\n}\n"
  },
  {
    "path": "DiscordChatExporter.Core/Markdown/FormattingNode.cs",
    "content": "﻿using System.Collections.Generic;\n\nnamespace DiscordChatExporter.Core.Markdown;\n\ninternal record FormattingNode(FormattingKind Kind, IReadOnlyList<MarkdownNode> Children)\n    : MarkdownNode,\n        IContainerNode;\n"
  },
  {
    "path": "DiscordChatExporter.Core/Markdown/HeadingNode.cs",
    "content": "﻿using System.Collections.Generic;\n\nnamespace DiscordChatExporter.Core.Markdown;\n\ninternal record HeadingNode(int Level, IReadOnlyList<MarkdownNode> Children)\n    : MarkdownNode,\n        IContainerNode;\n"
  },
  {
    "path": "DiscordChatExporter.Core/Markdown/IContainerNode.cs",
    "content": "﻿using System.Collections.Generic;\n\nnamespace DiscordChatExporter.Core.Markdown;\n\ninternal interface IContainerNode\n{\n    IReadOnlyList<MarkdownNode> Children { get; }\n}\n"
  },
  {
    "path": "DiscordChatExporter.Core/Markdown/InlineCodeBlockNode.cs",
    "content": "﻿namespace DiscordChatExporter.Core.Markdown;\n\ninternal record InlineCodeBlockNode(string Code) : MarkdownNode;\n"
  },
  {
    "path": "DiscordChatExporter.Core/Markdown/LinkNode.cs",
    "content": "﻿using System.Collections.Generic;\n\nnamespace DiscordChatExporter.Core.Markdown;\n\n// Named links can contain child nodes (e.g. [**bold URL**](https://test.com))\ninternal record LinkNode(string Url, IReadOnlyList<MarkdownNode> Children)\n    : MarkdownNode,\n        IContainerNode\n{\n    public LinkNode(string url)\n        : this(url, [new TextNode(url)]) { }\n}\n"
  },
  {
    "path": "DiscordChatExporter.Core/Markdown/ListItemNode.cs",
    "content": "﻿using System.Collections.Generic;\n\nnamespace DiscordChatExporter.Core.Markdown;\n\ninternal record ListItemNode(IReadOnlyList<MarkdownNode> Children) : MarkdownNode, IContainerNode;\n"
  },
  {
    "path": "DiscordChatExporter.Core/Markdown/ListNode.cs",
    "content": "﻿using System.Collections.Generic;\n\nnamespace DiscordChatExporter.Core.Markdown;\n\ninternal record ListNode(IReadOnlyList<ListItemNode> Items) : MarkdownNode;\n"
  },
  {
    "path": "DiscordChatExporter.Core/Markdown/MarkdownNode.cs",
    "content": "﻿namespace DiscordChatExporter.Core.Markdown;\n\ninternal abstract record MarkdownNode;\n"
  },
  {
    "path": "DiscordChatExporter.Core/Markdown/MentionKind.cs",
    "content": "﻿namespace DiscordChatExporter.Core.Markdown;\n\ninternal enum MentionKind\n{\n    Everyone,\n    Here,\n    User,\n    Channel,\n    Role,\n}\n"
  },
  {
    "path": "DiscordChatExporter.Core/Markdown/MentionNode.cs",
    "content": "﻿using DiscordChatExporter.Core.Discord;\n\nnamespace DiscordChatExporter.Core.Markdown;\n\n// Null ID means it's a meta mention or an invalid mention\ninternal record MentionNode(Snowflake? TargetId, MentionKind Kind) : MarkdownNode;\n"
  },
  {
    "path": "DiscordChatExporter.Core/Markdown/MultiLineCodeBlockNode.cs",
    "content": "﻿namespace DiscordChatExporter.Core.Markdown;\n\ninternal record MultiLineCodeBlockNode(string Language, string Code) : MarkdownNode;\n"
  },
  {
    "path": "DiscordChatExporter.Core/Markdown/Parsing/AggregateMatcher.cs",
    "content": "﻿using System.Collections.Generic;\n\nnamespace DiscordChatExporter.Core.Markdown.Parsing;\n\ninternal class AggregateMatcher<TContext, TValue>(\n    params IReadOnlyList<IMatcher<TContext, TValue>> matchers\n) : IMatcher<TContext, TValue>\n{\n    public ParsedMatch<TValue>? TryMatch(TContext context, StringSegment segment)\n    {\n        ParsedMatch<TValue>? earliestMatch = null;\n\n        // Try to match the input with each matcher and get the match with the lowest start index\n        foreach (var matcher in matchers)\n        {\n            // Try to match\n            var match = matcher.TryMatch(context, segment);\n\n            // If there's no match - continue\n            if (match is null)\n                continue;\n\n            // If this match is earlier than previous earliest - replace\n            if (\n                earliestMatch is null\n                || match.Segment.StartIndex < earliestMatch.Segment.StartIndex\n            )\n            {\n                earliestMatch = match;\n            }\n\n            // If the earliest match starts at the very beginning - break,\n            // because it's impossible to find a match earlier than that\n            if (earliestMatch.Segment.StartIndex == segment.StartIndex)\n                break;\n        }\n\n        return earliestMatch;\n    }\n}\n"
  },
  {
    "path": "DiscordChatExporter.Core/Markdown/Parsing/IMatcher.cs",
    "content": "﻿using System;\nusing System.Collections.Generic;\n\nnamespace DiscordChatExporter.Core.Markdown.Parsing;\n\ninternal interface IMatcher<in TContext, TValue>\n{\n    ParsedMatch<TValue>? TryMatch(TContext context, StringSegment segment);\n}\n\ninternal static class MatcherExtensions\n{\n    public static IEnumerable<ParsedMatch<TValue>> MatchAll<TContext, TValue>(\n        this IMatcher<TContext, TValue> matcher,\n        TContext context,\n        StringSegment segment,\n        Func<TContext, StringSegment, TValue> fallbackTransform\n    )\n    {\n        // Loop through segments divided by individual matches\n        var currentIndex = segment.StartIndex;\n        while (currentIndex < segment.EndIndex)\n        {\n            // Find a match within this segment\n            var match = matcher.TryMatch(\n                context,\n                segment.Relocate(currentIndex, segment.EndIndex - currentIndex)\n            );\n\n            if (match is null)\n                break;\n\n            // If this match doesn't start immediately at the current position - transform and yield fallback first\n            if (match.Segment.StartIndex > currentIndex)\n            {\n                var fallbackSegment = segment.Relocate(\n                    currentIndex,\n                    match.Segment.StartIndex - currentIndex\n                );\n\n                yield return new ParsedMatch<TValue>(\n                    fallbackSegment,\n                    fallbackTransform(context, fallbackSegment)\n                );\n            }\n\n            yield return match;\n\n            // Shift current index to the end of the match\n            currentIndex = match.Segment.StartIndex + match.Segment.Length;\n        }\n\n        // If EOL hasn't been reached - transform and yield remaining part as fallback\n        if (currentIndex < segment.EndIndex)\n        {\n            var fallbackSegment = segment.Relocate(currentIndex, segment.EndIndex - currentIndex);\n\n            yield return new ParsedMatch<TValue>(\n                fallbackSegment,\n                fallbackTransform(context, fallbackSegment)\n            );\n        }\n    }\n}\n"
  },
  {
    "path": "DiscordChatExporter.Core/Markdown/Parsing/MarkdownContext.cs",
    "content": "﻿namespace DiscordChatExporter.Core.Markdown.Parsing;\n\ninternal readonly record struct MarkdownContext(int Depth = 0);\n"
  },
  {
    "path": "DiscordChatExporter.Core/Markdown/Parsing/MarkdownParser.cs",
    "content": "﻿using System;\nusing System.Collections.Generic;\nusing System.Globalization;\nusing System.Linq;\nusing System.Text.RegularExpressions;\nusing DiscordChatExporter.Core.Discord;\nusing DiscordChatExporter.Core.Utils;\nusing DiscordChatExporter.Core.Utils.Extensions;\n\nnamespace DiscordChatExporter.Core.Markdown.Parsing;\n\n// Discord does NOT use a recursive-descent parser for markdown which becomes evident in some\n// scenarios, like when multiple formatting nodes are nested together.\n// To replicate Discord's behavior, we're employing a special parser that uses a set of regular\n// expressions that are executed sequentially in a first-matched-first-served manner.\ninternal static partial class MarkdownParser\n{\n    private const RegexOptions DefaultRegexOptions =\n        RegexOptions.Compiled\n        | RegexOptions.IgnorePatternWhitespace\n        | RegexOptions.CultureInvariant\n        | RegexOptions.Multiline;\n\n    /* Formatting */\n\n    private static readonly IMatcher<MarkdownContext, MarkdownNode> BoldFormattingNodeMatcher =\n        new RegexMatcher<MarkdownContext, MarkdownNode>(\n            // There must be exactly two closing asterisks.\n            new Regex(@\"\\*\\*(.+?)\\*\\*(?!\\*)\", DefaultRegexOptions | RegexOptions.Singleline),\n            (c, s, m) => new FormattingNode(FormattingKind.Bold, Parse(c, s.Relocate(m.Groups[1])))\n        );\n\n    private static readonly IMatcher<MarkdownContext, MarkdownNode> ItalicFormattingNodeMatcher =\n        new RegexMatcher<MarkdownContext, MarkdownNode>(\n            // There must be exactly one closing asterisk.\n            // Opening asterisk must not be followed by whitespace.\n            // Closing asterisk must not be preceded by whitespace.\n            new Regex(\n                @\"\\*(?!\\s)(.+?)(?<!\\s|\\*)\\*(?!\\*)\",\n                DefaultRegexOptions | RegexOptions.Singleline\n            ),\n            (c, s, m) =>\n                new FormattingNode(FormattingKind.Italic, Parse(c, s.Relocate(m.Groups[1])))\n        );\n\n    private static readonly IMatcher<\n        MarkdownContext,\n        MarkdownNode\n    > ItalicBoldFormattingNodeMatcher = new RegexMatcher<MarkdownContext, MarkdownNode>(\n        // There must be exactly three closing asterisks.\n        new Regex(@\"\\*(\\*\\*.+?\\*\\*)\\*(?!\\*)\", DefaultRegexOptions | RegexOptions.Singleline),\n        (c, s, m) =>\n            new FormattingNode(\n                FormattingKind.Italic,\n                Parse(c, s.Relocate(m.Groups[1]), BoldFormattingNodeMatcher)\n            )\n    );\n\n    private static readonly IMatcher<MarkdownContext, MarkdownNode> ItalicAltFormattingNodeMatcher =\n        new RegexMatcher<MarkdownContext, MarkdownNode>(\n            // Closing underscore must not be followed by a word character.\n            new Regex(@\"_(.+?)_(?!\\w)\", DefaultRegexOptions | RegexOptions.Singleline),\n            (c, s, m) =>\n                new FormattingNode(FormattingKind.Italic, Parse(c, s.Relocate(m.Groups[1])))\n        );\n\n    private static readonly IMatcher<MarkdownContext, MarkdownNode> UnderlineFormattingNodeMatcher =\n        new RegexMatcher<MarkdownContext, MarkdownNode>(\n            // There must be exactly two closing underscores.\n            new Regex(@\"__(.+?)__(?!_)\", DefaultRegexOptions | RegexOptions.Singleline),\n            (c, s, m) =>\n                new FormattingNode(FormattingKind.Underline, Parse(c, s.Relocate(m.Groups[1])))\n        );\n\n    private static readonly IMatcher<\n        MarkdownContext,\n        MarkdownNode\n    > ItalicUnderlineFormattingNodeMatcher = new RegexMatcher<MarkdownContext, MarkdownNode>(\n        // There must be exactly three closing underscores.\n        new Regex(@\"_(__.+?__)_(?!_)\", DefaultRegexOptions | RegexOptions.Singleline),\n        (c, s, m) =>\n            new FormattingNode(\n                FormattingKind.Italic,\n                Parse(c, s.Relocate(m.Groups[1]), UnderlineFormattingNodeMatcher)\n            )\n    );\n\n    private static readonly IMatcher<\n        MarkdownContext,\n        MarkdownNode\n    > StrikethroughFormattingNodeMatcher = new RegexMatcher<MarkdownContext, MarkdownNode>(\n        new Regex(@\"~~(.+?)~~\", DefaultRegexOptions | RegexOptions.Singleline),\n        (c, s, m) =>\n            new FormattingNode(FormattingKind.Strikethrough, Parse(c, s.Relocate(m.Groups[1])))\n    );\n\n    private static readonly IMatcher<MarkdownContext, MarkdownNode> SpoilerFormattingNodeMatcher =\n        new RegexMatcher<MarkdownContext, MarkdownNode>(\n            new Regex(@\"\\|\\|(.+?)\\|\\|\", DefaultRegexOptions | RegexOptions.Singleline),\n            (c, s, m) =>\n                new FormattingNode(FormattingKind.Spoiler, Parse(c, s.Relocate(m.Groups[1])))\n        );\n\n    private static readonly IMatcher<MarkdownContext, MarkdownNode> SingleLineQuoteNodeMatcher =\n        new RegexMatcher<MarkdownContext, MarkdownNode>(\n            // Include the linebreak in the content so that the lines are preserved in quotes.\n            new Regex(@\"^>\\s(.+\\n?)\", DefaultRegexOptions),\n            (c, s, m) => new FormattingNode(FormattingKind.Quote, Parse(c, s.Relocate(m.Groups[1])))\n        );\n\n    private static readonly IMatcher<\n        MarkdownContext,\n        MarkdownNode\n    > RepeatedSingleLineQuoteNodeMatcher = new RegexMatcher<MarkdownContext, MarkdownNode>(\n        // Include the linebreaks in the content, so that the lines are preserved in quotes.\n        // Empty content is allowed within quotes.\n        // https://github.com/Tyrrrz/DiscordChatExporter/issues/1115\n        new Regex(@\"(?:^>\\s(.*\\n?)){2,}\", DefaultRegexOptions),\n        (c, s, m) =>\n            new FormattingNode(\n                FormattingKind.Quote,\n                m.Groups[1].Captures.SelectMany(r => Parse(c, s.Relocate(r))).ToArray()\n            )\n    );\n\n    private static readonly IMatcher<MarkdownContext, MarkdownNode> MultiLineQuoteNodeMatcher =\n        new RegexMatcher<MarkdownContext, MarkdownNode>(\n            new Regex(@\"^>>>\\s(.+)\", DefaultRegexOptions | RegexOptions.Singleline),\n            (c, s, m) => new FormattingNode(FormattingKind.Quote, Parse(c, s.Relocate(m.Groups[1])))\n        );\n\n    private static readonly IMatcher<MarkdownContext, MarkdownNode> HeadingNodeMatcher =\n        new RegexMatcher<MarkdownContext, MarkdownNode>(\n            // Consume the linebreak so that it's not attached to following nodes.\n            new Regex(@\"^(\\#{1,3})\\s(.+)\\n\", DefaultRegexOptions),\n            (c, s, m) => new HeadingNode(m.Groups[1].Length, Parse(c, s.Relocate(m.Groups[2])))\n        );\n\n    private static readonly IMatcher<MarkdownContext, MarkdownNode> ListNodeMatcher =\n        new RegexMatcher<MarkdownContext, MarkdownNode>(\n            // Can be preceded by whitespace, which specifies the list's nesting level.\n            // Following lines that start with (level+1) whitespace are considered part of the list item.\n            // Consume the linebreak so that it's not attached to following nodes.\n            new Regex(@\"^(\\s*)(?:[\\-\\*]\\s(.+(?:\\n\\s\\1.*)*)?\\n?)+\", DefaultRegexOptions),\n            (c, s, m) =>\n                new ListNode(\n                    m.Groups[2]\n                        .Captures.Select(x => new ListItemNode(Parse(c, s.Relocate(x))))\n                        .ToArray()\n                )\n        );\n\n    /* Code blocks */\n\n    private static readonly IMatcher<MarkdownContext, MarkdownNode> InlineCodeBlockNodeMatcher =\n        new RegexMatcher<MarkdownContext, MarkdownNode>(\n            // One or two backticks are allowed, but they must match on both sides.\n            new Regex(@\"(`{1,2})([^`]+)\\1\", DefaultRegexOptions | RegexOptions.Singleline),\n            (_, _, m) => new InlineCodeBlockNode(m.Groups[2].Value)\n        );\n\n    private static readonly IMatcher<MarkdownContext, MarkdownNode> MultiLineCodeBlockNodeMatcher =\n        new RegexMatcher<MarkdownContext, MarkdownNode>(\n            // Language identifier is one word immediately after opening backticks, followed immediately by a linebreak.\n            // Blank lines at the beginning and at the end of content are trimmed.\n            new Regex(@\"```(?:(\\w*)\\n)?(.+?)```\", DefaultRegexOptions | RegexOptions.Singleline),\n            (_, _, m) =>\n                new MultiLineCodeBlockNode(m.Groups[1].Value, m.Groups[2].Value.Trim('\\r', '\\n'))\n        );\n\n    /* Mentions */\n\n    private static readonly IMatcher<MarkdownContext, MarkdownNode> EveryoneMentionNodeMatcher =\n        new StringMatcher<MarkdownContext, MarkdownNode>(\n            \"@everyone\",\n            (_, _) => new MentionNode(null, MentionKind.Everyone)\n        );\n\n    private static readonly IMatcher<MarkdownContext, MarkdownNode> HereMentionNodeMatcher =\n        new StringMatcher<MarkdownContext, MarkdownNode>(\n            \"@here\",\n            (_, _) => new MentionNode(null, MentionKind.Here)\n        );\n\n    private static readonly IMatcher<MarkdownContext, MarkdownNode> UserMentionNodeMatcher =\n        new RegexMatcher<MarkdownContext, MarkdownNode>(\n            // Capture <@123456> or <@!123456>\n            new Regex(@\"<@!?(\\d+)>\", DefaultRegexOptions),\n            (_, _, m) => new MentionNode(Snowflake.TryParse(m.Groups[1].Value), MentionKind.User)\n        );\n\n    private static readonly IMatcher<MarkdownContext, MarkdownNode> ChannelMentionNodeMatcher =\n        new RegexMatcher<MarkdownContext, MarkdownNode>(\n            // Capture <#123456>\n            new Regex(@\"<\\#!?(\\d+)>\", DefaultRegexOptions),\n            (_, _, m) => new MentionNode(Snowflake.TryParse(m.Groups[1].Value), MentionKind.Channel)\n        );\n\n    private static readonly IMatcher<MarkdownContext, MarkdownNode> RoleMentionNodeMatcher =\n        new RegexMatcher<MarkdownContext, MarkdownNode>(\n            // Capture <@&123456>\n            new Regex(@\"<@&(\\d+)>\", DefaultRegexOptions),\n            (_, _, m) => new MentionNode(Snowflake.TryParse(m.Groups[1].Value), MentionKind.Role)\n        );\n\n    /* Emoji */\n\n    private static readonly IMatcher<MarkdownContext, MarkdownNode> StandardEmojiNodeMatcher =\n        new RegexMatcher<MarkdownContext, MarkdownNode>(\n            new Regex(\n                // Build a pattern from all known emoji, sorted longest-first so that compound\n                // emoji (e.g. sequences with ZWJ or skin-tone modifiers) are matched before\n                // their individual components.\n                \"(\"\n                    + string.Join(\n                        \"|\",\n                        EmojiIndex\n                            .GetAllNames()\n                            .OrderByDescending(e => e.Length)\n                            .Select(Regex.Escape)\n                    )\n                    + \")\",\n                DefaultRegexOptions\n            ),\n            (_, _, m) => new EmojiNode(m.Groups[1].Value)\n        );\n\n    private static readonly IMatcher<MarkdownContext, MarkdownNode> CodedStandardEmojiNodeMatcher =\n        new RegexMatcher<MarkdownContext, MarkdownNode>(\n            // Capture :thinking:\n            new Regex(@\":([\\w_]+):\", DefaultRegexOptions),\n            (_, _, m) => EmojiIndex.TryGetName(m.Groups[1].Value)?.Pipe(n => new EmojiNode(n))\n        );\n\n    private static readonly IMatcher<MarkdownContext, MarkdownNode> CustomEmojiNodeMatcher =\n        new RegexMatcher<MarkdownContext, MarkdownNode>(\n            // Capture <:lul:123456> or <a:lul:123456>\n            new Regex(@\"<(a)?:(.+?):(\\d+?)>\", DefaultRegexOptions),\n            (_, _, m) =>\n                new EmojiNode(\n                    Snowflake.TryParse(m.Groups[3].Value),\n                    m.Groups[2].Value,\n                    !string.IsNullOrWhiteSpace(m.Groups[1].Value)\n                )\n        );\n\n    /* Links */\n\n    private static readonly IMatcher<MarkdownContext, MarkdownNode> AutoLinkNodeMatcher =\n        new RegexMatcher<MarkdownContext, MarkdownNode>(\n            // Any non-whitespace character after http:// or https://\n            // until the last punctuation character or whitespace.\n            new Regex(@\"(https?://\\S*[^\\.,:;\"\"'\\s])\", DefaultRegexOptions),\n            (_, _, m) => new LinkNode(m.Groups[1].Value)\n        );\n\n    private static readonly IMatcher<MarkdownContext, MarkdownNode> HiddenLinkNodeMatcher =\n        new RegexMatcher<MarkdownContext, MarkdownNode>(\n            // Same as auto link but also surrounded by angular brackets\n            new Regex(@\"<(https?://\\S*[^\\.,:;\"\"'\\s])>\", DefaultRegexOptions),\n            (_, _, m) => new LinkNode(m.Groups[1].Value)\n        );\n\n    private static readonly IMatcher<MarkdownContext, MarkdownNode> MaskedLinkNodeMatcher =\n        new RegexMatcher<MarkdownContext, MarkdownNode>(\n            // Capture [title](link)\n            new Regex(@\"\\[(.+?)\\]\\((.+?)\\)\", DefaultRegexOptions),\n            (c, s, m) => new LinkNode(m.Groups[2].Value, Parse(c, s.Relocate(m.Groups[1])))\n        );\n\n    /* Text */\n\n    private static readonly IMatcher<MarkdownContext, MarkdownNode> ShrugTextNodeMatcher =\n        new StringMatcher<MarkdownContext, MarkdownNode>(\n            // Capture the shrug kaomoji.\n            // This escapes it from matching for formatting.\n            @\"¯\\_(ツ)_/¯\",\n            (_, s) => new TextNode(s.ToString())\n        );\n\n    private static readonly IMatcher<MarkdownContext, MarkdownNode> IgnoredEmojiTextNodeMatcher =\n        new RegexMatcher<MarkdownContext, MarkdownNode>(\n            // Capture some specific emoji that don't get rendered.\n            // This escapes them from matching for emoji.\n            new Regex(@\"([\\u26A7\\u2640\\u2642\\u2695\\u267E\\u00A9\\u00AE\\u2122])\", DefaultRegexOptions),\n            (_, _, m) => new TextNode(m.Groups[1].Value)\n        );\n\n    private static readonly IMatcher<MarkdownContext, MarkdownNode> EscapedSymbolTextNodeMatcher =\n        new RegexMatcher<MarkdownContext, MarkdownNode>(\n            // Capture any \"symbol/other\" character or surrogate pair preceded by a backslash.\n            // This escapes them from matching for emoji.\n            // https://github.com/Tyrrrz/DiscordChatExporter/issues/230\n            new Regex(@\"\\\\(\\p{So}|\\p{Cs}{2})\", DefaultRegexOptions),\n            (_, _, m) => new TextNode(m.Groups[1].Value)\n        );\n\n    private static readonly IMatcher<\n        MarkdownContext,\n        MarkdownNode\n    > EscapedCharacterTextNodeMatcher = new RegexMatcher<MarkdownContext, MarkdownNode>(\n        // Capture any non-whitespace, non latin alphanumeric character preceded by a backslash.\n        // This escapes them from matching for formatting or other tokens.\n        new Regex(@\"\\\\([^a-zA-Z0-9\\s])\", DefaultRegexOptions),\n        (_, _, m) => new TextNode(m.Groups[1].Value)\n    );\n\n    /* Misc */\n\n    private static readonly IMatcher<MarkdownContext, MarkdownNode> TimestampNodeMatcher =\n        new RegexMatcher<MarkdownContext, MarkdownNode>(\n            // Capture <t:12345678> or <t:12345678:R>\n            new Regex(@\"<t:(-?\\d+)(?::(\\w))?>\", DefaultRegexOptions),\n            (_, _, m) =>\n            {\n                try\n                {\n                    var instant =\n                        DateTimeOffset.UnixEpoch\n                        + TimeSpan.FromSeconds(\n                            long.Parse(\n                                m.Groups[1].Value,\n                                NumberStyles.Integer,\n                                CultureInfo.InvariantCulture\n                            )\n                        );\n\n                    // https://discord.com/developers/docs/reference#message-formatting-timestamp-styles\n                    var format = m.Groups[2].Value.NullIfWhiteSpace() switch\n                    {\n                        // Known formats\n                        \"t\" => \"t\",\n                        \"T\" => \"T\",\n                        \"d\" => \"d\",\n                        \"D\" => \"D\",\n                        \"f\" => \"f\",\n                        \"F\" => \"F\",\n                        // Relative format: ignore because it doesn't make sense in a static export\n                        \"r\" => null,\n                        \"R\" => null,\n                        // Unspecified format: will be mapped to the default format\n                        null => null,\n                        // Unknown format: throw an exception to consider this timestamp invalid\n                        // https://github.com/Tyrrrz/DiscordChatExporter/issues/1156\n                        var f => throw new InvalidOperationException(\n                            $\"Unknown timestamp format '{f}'.\"\n                        ),\n                    };\n\n                    return new TimestampNode(instant, format);\n                }\n                // https://github.com/Tyrrrz/DiscordChatExporter/issues/681\n                // https://github.com/Tyrrrz/DiscordChatExporter/issues/766\n                catch (Exception ex)\n                    when (ex\n                            is FormatException\n                                or ArgumentOutOfRangeException\n                                or OverflowException\n                                or InvalidOperationException\n                    )\n                {\n                    // For invalid timestamps, Discord renders \"Invalid Date\" instead of ignoring the markdown\n                    return TimestampNode.Invalid;\n                }\n            }\n        );\n\n    // Matchers that have similar patterns are ordered from most specific to least specific\n    private static readonly IMatcher<MarkdownContext, MarkdownNode> NodeMatcher =\n        new AggregateMatcher<MarkdownContext, MarkdownNode>(\n            // Escaped text\n            ShrugTextNodeMatcher,\n            IgnoredEmojiTextNodeMatcher,\n            EscapedSymbolTextNodeMatcher,\n            EscapedCharacterTextNodeMatcher,\n            // Formatting\n            ItalicBoldFormattingNodeMatcher,\n            ItalicUnderlineFormattingNodeMatcher,\n            BoldFormattingNodeMatcher,\n            ItalicFormattingNodeMatcher,\n            UnderlineFormattingNodeMatcher,\n            ItalicAltFormattingNodeMatcher,\n            StrikethroughFormattingNodeMatcher,\n            SpoilerFormattingNodeMatcher,\n            MultiLineQuoteNodeMatcher,\n            RepeatedSingleLineQuoteNodeMatcher,\n            SingleLineQuoteNodeMatcher,\n            HeadingNodeMatcher,\n            ListNodeMatcher,\n            // Code blocks\n            MultiLineCodeBlockNodeMatcher,\n            InlineCodeBlockNodeMatcher,\n            // Mentions\n            EveryoneMentionNodeMatcher,\n            HereMentionNodeMatcher,\n            UserMentionNodeMatcher,\n            ChannelMentionNodeMatcher,\n            RoleMentionNodeMatcher,\n            // Links\n            MaskedLinkNodeMatcher,\n            AutoLinkNodeMatcher,\n            HiddenLinkNodeMatcher,\n            // Emoji\n            StandardEmojiNodeMatcher,\n            CustomEmojiNodeMatcher,\n            CodedStandardEmojiNodeMatcher,\n            // Misc\n            TimestampNodeMatcher\n        );\n\n    // Minimal set of matchers for non-multimedia formats (e.g. plain text)\n    private static readonly IMatcher<MarkdownContext, MarkdownNode> MinimalNodeMatcher =\n        new AggregateMatcher<MarkdownContext, MarkdownNode>(\n            // Mentions\n            EveryoneMentionNodeMatcher,\n            HereMentionNodeMatcher,\n            UserMentionNodeMatcher,\n            ChannelMentionNodeMatcher,\n            RoleMentionNodeMatcher,\n            // Emoji\n            CustomEmojiNodeMatcher,\n            // Misc\n            TimestampNodeMatcher\n        );\n\n    private static IReadOnlyList<MarkdownNode> Parse(\n        MarkdownContext context,\n        StringSegment segment,\n        IMatcher<MarkdownContext, MarkdownNode> matcher\n    )\n    {\n        // Limit recursion depth to a reasonable number to prevent\n        // stack overflow on messages with inadvertently deep nesting.\n        // Example: ********************************* (repeat ad nauseam)\n        // https://github.com/Tyrrrz/DiscordChatExporter/issues/1214\n        if (context.Depth >= 32)\n            return [new TextNode(segment.ToString())];\n\n        return matcher\n            .MatchAll(\n                new MarkdownContext(context.Depth + 1),\n                segment,\n                (_, s) => new TextNode(s.ToString())\n            )\n            .Select(r => r.Value)\n            .ToArray();\n    }\n}\n\ninternal static partial class MarkdownParser\n{\n    private static void Extract<TNode>(\n        IEnumerable<MarkdownNode> nodes,\n        ICollection<TNode> extractedNodes\n    )\n        where TNode : MarkdownNode\n    {\n        foreach (var node in nodes)\n        {\n            if (node is TNode extractedNode)\n                extractedNodes.Add(extractedNode);\n\n            if (node is IContainerNode containerNode)\n                Extract(containerNode.Children, extractedNodes);\n        }\n    }\n\n    public static IReadOnlyList<TNode> Extract<TNode>(string markdown)\n        where TNode : MarkdownNode\n    {\n        var extractedNodes = new List<TNode>();\n        Extract(Parse(markdown), extractedNodes);\n\n        return extractedNodes;\n    }\n\n    public static IReadOnlyList<LinkNode> ExtractLinks(string markdown) =>\n        Extract<LinkNode>(markdown);\n\n    public static IReadOnlyList<EmojiNode> ExtractEmojis(string markdown) =>\n        Extract<EmojiNode>(markdown);\n\n    private static IReadOnlyList<MarkdownNode> Parse(\n        MarkdownContext context,\n        StringSegment segment\n    ) => Parse(context, segment, NodeMatcher);\n\n    public static IReadOnlyList<MarkdownNode> Parse(string markdown) =>\n        Parse(new MarkdownContext(), new StringSegment(markdown));\n\n    private static IReadOnlyList<MarkdownNode> ParseMinimal(\n        MarkdownContext context,\n        StringSegment segment\n    ) => Parse(context, segment, MinimalNodeMatcher);\n\n    public static IReadOnlyList<MarkdownNode> ParseMinimal(string markdown) =>\n        ParseMinimal(new MarkdownContext(), new StringSegment(markdown));\n}\n"
  },
  {
    "path": "DiscordChatExporter.Core/Markdown/Parsing/MarkdownVisitor.cs",
    "content": "﻿using System;\nusing System.Collections.Generic;\nusing System.Threading;\nusing System.Threading.Tasks;\n\nnamespace DiscordChatExporter.Core.Markdown.Parsing;\n\ninternal abstract class MarkdownVisitor\n{\n    protected virtual ValueTask VisitTextAsync(\n        TextNode text,\n        CancellationToken cancellationToken = default\n    ) => default;\n\n    protected virtual async ValueTask VisitFormattingAsync(\n        FormattingNode formatting,\n        CancellationToken cancellationToken = default\n    ) => await VisitAsync(formatting.Children, cancellationToken);\n\n    protected virtual async ValueTask VisitHeadingAsync(\n        HeadingNode heading,\n        CancellationToken cancellationToken = default\n    ) => await VisitAsync(heading.Children, cancellationToken);\n\n    protected virtual async ValueTask VisitListAsync(\n        ListNode list,\n        CancellationToken cancellationToken = default\n    ) => await VisitAsync(list.Items, cancellationToken);\n\n    protected virtual async ValueTask VisitListItemAsync(\n        ListItemNode listItem,\n        CancellationToken cancellationToken = default\n    ) => await VisitAsync(listItem.Children, cancellationToken);\n\n    protected virtual ValueTask VisitInlineCodeBlockAsync(\n        InlineCodeBlockNode inlineCodeBlock,\n        CancellationToken cancellationToken = default\n    ) => default;\n\n    protected virtual ValueTask VisitMultiLineCodeBlockAsync(\n        MultiLineCodeBlockNode multiLineCodeBlock,\n        CancellationToken cancellationToken = default\n    ) => default;\n\n    protected virtual async ValueTask VisitLinkAsync(\n        LinkNode link,\n        CancellationToken cancellationToken = default\n    ) => await VisitAsync(link.Children, cancellationToken);\n\n    protected virtual ValueTask VisitEmojiAsync(\n        EmojiNode emoji,\n        CancellationToken cancellationToken = default\n    ) => default;\n\n    protected virtual ValueTask VisitMentionAsync(\n        MentionNode mention,\n        CancellationToken cancellationToken = default\n    ) => default;\n\n    protected virtual ValueTask VisitTimestampAsync(\n        TimestampNode timestamp,\n        CancellationToken cancellationToken = default\n    ) => default;\n\n    public async ValueTask VisitAsync(\n        MarkdownNode node,\n        CancellationToken cancellationToken = default\n    )\n    {\n        if (node is TextNode text)\n        {\n            await VisitTextAsync(text, cancellationToken);\n            return;\n        }\n\n        if (node is FormattingNode formatting)\n        {\n            await VisitFormattingAsync(formatting, cancellationToken);\n            return;\n        }\n\n        if (node is HeadingNode heading)\n        {\n            await VisitHeadingAsync(heading, cancellationToken);\n            return;\n        }\n\n        if (node is ListNode list)\n        {\n            await VisitListAsync(list, cancellationToken);\n            return;\n        }\n\n        if (node is ListItemNode listItem)\n        {\n            await VisitListItemAsync(listItem, cancellationToken);\n            return;\n        }\n\n        if (node is InlineCodeBlockNode inlineCodeBlock)\n        {\n            await VisitInlineCodeBlockAsync(inlineCodeBlock, cancellationToken);\n            return;\n        }\n\n        if (node is MultiLineCodeBlockNode multiLineCodeBlock)\n        {\n            await VisitMultiLineCodeBlockAsync(multiLineCodeBlock, cancellationToken);\n            return;\n        }\n\n        if (node is LinkNode link)\n        {\n            await VisitLinkAsync(link, cancellationToken);\n            return;\n        }\n\n        if (node is EmojiNode emoji)\n        {\n            await VisitEmojiAsync(emoji, cancellationToken);\n            return;\n        }\n\n        if (node is MentionNode mention)\n        {\n            await VisitMentionAsync(mention, cancellationToken);\n            return;\n        }\n\n        if (node is TimestampNode timestamp)\n        {\n            await VisitTimestampAsync(timestamp, cancellationToken);\n            return;\n        }\n\n        throw new ArgumentOutOfRangeException(nameof(node));\n    }\n\n    public async ValueTask VisitAsync(\n        IEnumerable<MarkdownNode> nodes,\n        CancellationToken cancellationToken = default\n    )\n    {\n        foreach (var node in nodes)\n            await VisitAsync(node, cancellationToken);\n    }\n}\n"
  },
  {
    "path": "DiscordChatExporter.Core/Markdown/Parsing/ParsedMatch.cs",
    "content": "﻿namespace DiscordChatExporter.Core.Markdown.Parsing;\n\ninternal class ParsedMatch<T>(StringSegment segment, T value)\n{\n    public StringSegment Segment { get; } = segment;\n\n    public T Value { get; } = value;\n}\n"
  },
  {
    "path": "DiscordChatExporter.Core/Markdown/Parsing/RegexMatcher.cs",
    "content": "﻿using System;\nusing System.Text.RegularExpressions;\n\nnamespace DiscordChatExporter.Core.Markdown.Parsing;\n\ninternal class RegexMatcher<TContext, TValue>(\n    Regex regex,\n    Func<TContext, StringSegment, Match, TValue?> transform\n) : IMatcher<TContext, TValue>\n{\n    public ParsedMatch<TValue>? TryMatch(TContext context, StringSegment segment)\n    {\n        var match = regex.Match(segment.Source, segment.StartIndex, segment.Length);\n        if (!match.Success)\n            return null;\n\n        // Overload regex.Match(string, int, int) doesn't take the whole string into account,\n        // it effectively functions as a match check on a substring.\n        // Which is super weird because regex.Match(string, int) takes the whole input in context.\n        // So in order to properly account for ^/$ regex tokens, we need to make sure that\n        // the expression also matches on the bigger part of the input.\n        if (!regex.IsMatch(segment.Source[..segment.EndIndex], segment.StartIndex))\n            return null;\n\n        var segmentMatch = segment.Relocate(match);\n        var value = transform(context, segmentMatch, match);\n\n        return value is not null ? new ParsedMatch<TValue>(segmentMatch, value) : null;\n    }\n}\n"
  },
  {
    "path": "DiscordChatExporter.Core/Markdown/Parsing/StringMatcher.cs",
    "content": "﻿using System;\n\nnamespace DiscordChatExporter.Core.Markdown.Parsing;\n\ninternal class StringMatcher<TContext, TValue>(\n    string needle,\n    StringComparison comparison,\n    Func<TContext, StringSegment, TValue?> transform\n) : IMatcher<TContext, TValue>\n{\n    public StringMatcher(string needle, Func<TContext, StringSegment, TValue> transform)\n        : this(needle, StringComparison.Ordinal, transform) { }\n\n    public ParsedMatch<TValue>? TryMatch(TContext context, StringSegment segment)\n    {\n        var index = segment.Source.IndexOf(needle, segment.StartIndex, segment.Length, comparison);\n\n        if (index < 0)\n            return null;\n\n        var segmentMatch = segment.Relocate(index, needle.Length);\n        var value = transform(context, segmentMatch);\n\n        return value is not null ? new ParsedMatch<TValue>(segmentMatch, value) : null;\n    }\n}\n"
  },
  {
    "path": "DiscordChatExporter.Core/Markdown/Parsing/StringSegment.cs",
    "content": "﻿using System.Text.RegularExpressions;\n\nnamespace DiscordChatExporter.Core.Markdown.Parsing;\n\ninternal readonly record struct StringSegment(string Source, int StartIndex, int Length)\n{\n    public int EndIndex => StartIndex + Length;\n\n    public StringSegment(string target)\n        : this(target, 0, target.Length) { }\n\n    public StringSegment Relocate(int newStartIndex, int newLength) =>\n        new(Source, newStartIndex, newLength);\n\n    public StringSegment Relocate(Capture capture) => Relocate(capture.Index, capture.Length);\n\n    public override string ToString() => Source[StartIndex..EndIndex];\n}\n"
  },
  {
    "path": "DiscordChatExporter.Core/Markdown/TextNode.cs",
    "content": "﻿namespace DiscordChatExporter.Core.Markdown;\n\ninternal record TextNode(string Text) : MarkdownNode;\n"
  },
  {
    "path": "DiscordChatExporter.Core/Markdown/TimestampNode.cs",
    "content": "﻿using System;\n\nnamespace DiscordChatExporter.Core.Markdown;\n\n// Null date means invalid timestamp\ninternal record TimestampNode(DateTimeOffset? Instant, string? Format) : MarkdownNode\n{\n    public static TimestampNode Invalid { get; } = new(null, null);\n}\n"
  },
  {
    "path": "DiscordChatExporter.Core/Utils/Docker.cs",
    "content": "﻿using System;\n\nnamespace DiscordChatExporter.Core.Utils;\n\npublic static class Docker\n{\n    public static bool IsRunningInContainer { get; } =\n        Environment.GetEnvironmentVariable(\"DOTNET_RUNNING_IN_CONTAINER\") == \"true\";\n}\n"
  },
  {
    "path": "DiscordChatExporter.Core/Utils/Extensions/AsyncCollectionExtensions.cs",
    "content": "﻿using System.Collections.Generic;\nusing System.Runtime.CompilerServices;\nusing System.Threading.Tasks;\n\nnamespace DiscordChatExporter.Core.Utils.Extensions;\n\npublic static class AsyncCollectionExtensions\n{\n    extension<T>(IAsyncEnumerable<T> asyncEnumerable)\n    {\n        private async ValueTask<IReadOnlyList<T>> CollectAsync()\n        {\n            var list = new List<T>();\n\n            await foreach (var i in asyncEnumerable)\n                list.Add(i);\n\n            return list;\n        }\n\n        public ValueTaskAwaiter<IReadOnlyList<T>> GetAwaiter() =>\n            asyncEnumerable.CollectAsync().GetAwaiter();\n    }\n}\n"
  },
  {
    "path": "DiscordChatExporter.Core/Utils/Extensions/CollectionExtensions.cs",
    "content": "﻿using System.Collections.Generic;\n\nnamespace DiscordChatExporter.Core.Utils.Extensions;\n\npublic static class CollectionExtensions\n{\n    extension<T>(T obj)\n    {\n        public IEnumerable<T> ToSingletonEnumerable()\n        {\n            yield return obj;\n        }\n    }\n\n    extension<T>(IEnumerable<T?> source)\n        where T : class\n    {\n        public IEnumerable<T> WhereNotNull()\n        {\n            foreach (var o in source)\n            {\n                if (o is not null)\n                    yield return o;\n            }\n        }\n    }\n\n    extension<T>(IEnumerable<T?> source)\n        where T : struct\n    {\n        public IEnumerable<T> WhereNotNull()\n        {\n            foreach (var o in source)\n            {\n                if (o is not null)\n                    yield return o.Value;\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "DiscordChatExporter.Core/Utils/Extensions/ColorExtensions.cs",
    "content": "﻿using System.Drawing;\n\nnamespace DiscordChatExporter.Core.Utils.Extensions;\n\npublic static class ColorExtensions\n{\n    extension(Color color)\n    {\n        public Color WithAlpha(int alpha) => Color.FromArgb(alpha, color);\n\n        public Color ResetAlpha() => color.WithAlpha(255);\n\n        public int ToRgb() => color.ToArgb() & 0xffffff;\n\n        public string ToHex() => $\"#{color.R:X2}{color.G:X2}{color.B:X2}\";\n    }\n}\n"
  },
  {
    "path": "DiscordChatExporter.Core/Utils/Extensions/ExceptionExtensions.cs",
    "content": "﻿using System;\nusing System.Collections.Generic;\n\nnamespace DiscordChatExporter.Core.Utils.Extensions;\n\npublic static class ExceptionExtensions\n{\n    extension(Exception exception)\n    {\n        private void PopulateChildren(ICollection<Exception> children)\n        {\n            if (exception is AggregateException aggregateException)\n            {\n                foreach (var innerException in aggregateException.InnerExceptions)\n                {\n                    children.Add(innerException);\n                    PopulateChildren(innerException, children);\n                }\n            }\n            else if (exception.InnerException is not null)\n            {\n                children.Add(exception.InnerException);\n                PopulateChildren(exception.InnerException, children);\n            }\n        }\n\n        public IReadOnlyList<Exception> GetSelfAndChildren()\n        {\n            var children = new List<Exception> { exception };\n            PopulateChildren(exception, children);\n            return children;\n        }\n    }\n}\n"
  },
  {
    "path": "DiscordChatExporter.Core/Utils/Extensions/GenericExtensions.cs",
    "content": "﻿using System;\nusing System.Collections.Generic;\n\nnamespace DiscordChatExporter.Core.Utils.Extensions;\n\npublic static class GenericExtensions\n{\n    extension<TIn>(TIn input)\n    {\n        public TOut Pipe<TOut>(Func<TIn, TOut> transform) => transform(input);\n    }\n\n    extension<T>(T value)\n        where T : struct\n    {\n        public T? NullIf(Func<T, bool> predicate) => !predicate(value) ? value : null;\n\n        public T? NullIfDefault() =>\n            value.NullIf(v => EqualityComparer<T>.Default.Equals(v, default));\n    }\n}\n"
  },
  {
    "path": "DiscordChatExporter.Core/Utils/Extensions/HttpExtensions.cs",
    "content": "﻿using System.Net.Http.Headers;\n\nnamespace DiscordChatExporter.Core.Utils.Extensions;\n\npublic static class HttpExtensions\n{\n    extension(HttpHeaders headers)\n    {\n        public string? TryGetValue(string name) =>\n            headers.TryGetValues(name, out var values) ? string.Concat(values) : null;\n    }\n}\n"
  },
  {
    "path": "DiscordChatExporter.Core/Utils/Extensions/PathExtensions.cs",
    "content": "﻿using System;\nusing System.IO;\nusing System.Text;\n\nnamespace DiscordChatExporter.Core.Utils.Extensions;\n\npublic static class PathExtensions\n{\n    // This is a union of invalid characters from Windows (NTFS/FAT32), Linux (ext4/XFS), and macOS (HFS+/APFS).\n    // We use this instead of Path.GetInvalidFileNameChars() because that only returns OS-specific characters,\n    // not filesystem-specific characters. It's possible to use, for example, an NTFS drive on Linux,\n    // which would make some additional characters invalid that are otherwise valid on Linux.\n    // https://github.com/Tyrrrz/DiscordChatExporter/issues/1452\n    private static readonly char[] InvalidFileNameChars =\n    [\n        '\\0', // Null character - invalid on all filesystems\n        '/', // Path separator on Unix and Windows\n        '\\\\', // Path separator on Windows\n        ':', // Reserved on Windows (drive letters, NTFS streams)\n        '*', // Wildcard on Windows\n        '?', // Wildcard on Windows\n        '\"', // Reserved on Windows\n        '<', // Redirection on Windows\n        '>', // Redirection on Windows\n        '|', // Pipe on Windows\n    ];\n\n    extension(Path)\n    {\n        public static string EscapeFileName(string path)\n        {\n            var buffer = new StringBuilder(path.Length);\n\n            foreach (var c in path)\n                buffer.Append(!InvalidFileNameChars.Contains(c) ? c : '_');\n\n            // File names cannot end with a dot on Windows\n            // https://github.com/Tyrrrz/DiscordChatExporter/issues/977\n            if (OperatingSystem.IsWindows())\n            {\n                while (buffer.Length > 0 && buffer[^1] == '.')\n                    buffer.Remove(buffer.Length - 1, 1);\n            }\n\n            return buffer.ToString();\n        }\n    }\n}\n"
  },
  {
    "path": "DiscordChatExporter.Core/Utils/Extensions/StringExtensions.cs",
    "content": "﻿using System.Text;\n\nnamespace DiscordChatExporter.Core.Utils.Extensions;\n\npublic static class StringExtensions\n{\n    extension(string str)\n    {\n        public string? NullIfWhiteSpace() => !string.IsNullOrWhiteSpace(str) ? str : null;\n\n        public string Truncate(int charCount) => str.Length > charCount ? str[..charCount] : str;\n\n        public string ToSpaceSeparatedWords()\n        {\n            var builder = new StringBuilder(str.Length * 2);\n\n            foreach (var c in str)\n            {\n                if (char.IsUpper(c) && builder.Length > 0)\n                    builder.Append(' ');\n\n                builder.Append(c);\n            }\n\n            return builder.ToString();\n        }\n    }\n\n    extension(StringBuilder builder)\n    {\n        public StringBuilder AppendIfNotEmpty(char value) =>\n            builder.Length > 0 ? builder.Append(value) : builder;\n    }\n}\n"
  },
  {
    "path": "DiscordChatExporter.Core/Utils/Extensions/SuperpowerExtensions.cs",
    "content": "﻿using System;\nusing System.Diagnostics.CodeAnalysis;\nusing Superpower;\nusing Superpower.Parsers;\n\nnamespace DiscordChatExporter.Core.Utils.Extensions;\n\npublic static class SuperpowerExtensions\n{\n    extension<T>(TextParser<T> parser)\n    {\n        public TextParser<T> Token() =>\n            parser.Between(Character.WhiteSpace.IgnoreMany(), Character.WhiteSpace.IgnoreMany());\n\n        // Only used for debugging while writing Superpower parsers.\n        // From https://twitter.com/nblumhardt/status/1389349059786264578\n        [ExcludeFromCodeCoverage]\n        public TextParser<T> Log(string description) =>\n            i =>\n            {\n                Console.WriteLine($\"Trying {description} ->\");\n                var r = parser(i);\n                Console.WriteLine($\"Result was {r}\");\n                return r;\n            };\n    }\n}\n"
  },
  {
    "path": "DiscordChatExporter.Core/Utils/Extensions/TimeSpanExtensions.cs",
    "content": "﻿using System;\n\nnamespace DiscordChatExporter.Core.Utils.Extensions;\n\npublic static class TimeSpanExtensions\n{\n    extension(TimeSpan value)\n    {\n        public TimeSpan Clamp(TimeSpan min, TimeSpan max)\n        {\n            if (value < min)\n                return min;\n\n            if (value > max)\n                return max;\n\n            return value;\n        }\n    }\n}\n"
  },
  {
    "path": "DiscordChatExporter.Core/Utils/Http.cs",
    "content": "﻿using System;\nusing System.Linq;\nusing System.Net;\nusing System.Net.Http;\nusing System.Net.Sockets;\nusing System.Security.Authentication;\nusing System.Threading.Tasks;\nusing DiscordChatExporter.Core.Utils.Extensions;\nusing Polly;\nusing Polly.Retry;\n\nnamespace DiscordChatExporter.Core.Utils;\n\npublic static class Http\n{\n    public static HttpClient Client { get; } = new();\n\n    private static bool IsRetryableStatusCode(HttpStatusCode statusCode) =>\n        statusCode is HttpStatusCode.TooManyRequests or HttpStatusCode.RequestTimeout\n        ||\n        // Treat all server-side errors as retryable\n        // https://github.com/Tyrrrz/DiscordChatExporter/issues/908\n        (int)statusCode >= 500;\n\n    private static bool IsRetryableException(Exception exception) =>\n        exception\n            .GetSelfAndChildren()\n            .Any(ex =>\n                ex is TimeoutException or SocketException or AuthenticationException\n                || ex is HttpRequestException hrex\n                    && IsRetryableStatusCode(hrex.StatusCode ?? HttpStatusCode.OK)\n            );\n\n    public static ResiliencePipeline ResiliencePipeline { get; } =\n        new ResiliencePipelineBuilder()\n            .AddRetry(\n                new RetryStrategyOptions\n                {\n                    ShouldHandle = new PredicateBuilder().Handle<Exception>(IsRetryableException),\n                    MaxRetryAttempts = 4,\n                    BackoffType = DelayBackoffType.Exponential,\n                    Delay = TimeSpan.FromSeconds(1),\n                }\n            )\n            .Build();\n\n    public static ResiliencePipeline<HttpResponseMessage> ResponseResiliencePipeline { get; } =\n        new ResiliencePipelineBuilder<HttpResponseMessage>()\n            .AddRetry(\n                new RetryStrategyOptions<HttpResponseMessage>\n                {\n                    ShouldHandle = new PredicateBuilder<HttpResponseMessage>()\n                        .Handle<Exception>(IsRetryableException)\n                        .HandleResult(m => IsRetryableStatusCode(m.StatusCode)),\n                    MaxRetryAttempts = 8,\n                    DelayGenerator = args =>\n                    {\n                        // If rate-limited, use retry-after header as the guide.\n                        // The response can be null here if an exception was thrown.\n                        if (args.Outcome.Result?.Headers.RetryAfter?.Delta is { } retryAfter)\n                        {\n                            // Add some buffer just in case\n                            return ValueTask.FromResult<TimeSpan?>(\n                                retryAfter + TimeSpan.FromSeconds(1)\n                            );\n                        }\n\n                        return ValueTask.FromResult<TimeSpan?>(\n                            TimeSpan.FromSeconds(Math.Pow(2, args.AttemptNumber) + 1)\n                        );\n                    },\n                }\n            )\n            .Build();\n}\n"
  },
  {
    "path": "DiscordChatExporter.Core/Utils/Url.cs",
    "content": "﻿using System;\nusing System.IO;\nusing System.Text;\n\nnamespace DiscordChatExporter.Core.Utils;\n\npublic static class Url\n{\n    public static string EncodeFilePath(string filePath)\n    {\n        var buffer = new StringBuilder();\n        var position = 0;\n\n        // For absolute paths, prepend file:// protocol for proper browser handling\n        if (Path.IsPathFullyQualified(filePath))\n        {\n            buffer.Append(\"file://\");\n\n            // On Windows, we need to add an extra slash before the drive letter\n            // e.g., file:///C:/path instead of file://C:/path\n            if (!filePath.StartsWith('/') && !filePath.StartsWith('\\\\'))\n            {\n                buffer.Append('/');\n            }\n        }\n\n        while (true)\n        {\n            if (position >= filePath.Length)\n                break;\n\n            var separatorIndex = filePath.IndexOfAny([':', '/', '\\\\'], position);\n            if (separatorIndex < 0)\n            {\n                buffer.Append(Uri.EscapeDataString(filePath[position..]));\n                break;\n            }\n\n            // Append the segment\n            buffer.Append(Uri.EscapeDataString(filePath[position..separatorIndex]));\n\n            // Append the separator\n            buffer.Append(\n                filePath[separatorIndex] switch\n                {\n                    // Normalize slashes\n                    '\\\\' => '/',\n                    var c => c,\n                }\n            );\n\n            position = separatorIndex + 1;\n        }\n\n        return buffer.ToString();\n    }\n}\n"
  },
  {
    "path": "DiscordChatExporter.Core/Utils/UrlBuilder.cs",
    "content": "﻿using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Text;\nusing DiscordChatExporter.Core.Utils.Extensions;\n\nnamespace DiscordChatExporter.Core.Utils;\n\npublic class UrlBuilder\n{\n    private string _path = \"\";\n\n    private readonly Dictionary<string, string?> _queryParameters = new(\n        StringComparer.OrdinalIgnoreCase\n    );\n\n    public UrlBuilder SetPath(string path)\n    {\n        _path = path;\n        return this;\n    }\n\n    public UrlBuilder SetQueryParameter(string key, string? value, bool ignoreUnsetValue = true)\n    {\n        if (ignoreUnsetValue && string.IsNullOrWhiteSpace(value))\n            return this;\n\n        var keyEncoded = Uri.EscapeDataString(key);\n        var valueEncoded = value?.Pipe(Uri.EscapeDataString);\n        _queryParameters[keyEncoded] = valueEncoded;\n\n        return this;\n    }\n\n    public string Build()\n    {\n        var buffer = new StringBuilder();\n\n        buffer.Append(_path);\n\n        if (_queryParameters.Any())\n        {\n            buffer\n                .Append('?')\n                .AppendJoin('&', _queryParameters.Select(kvp => $\"{kvp.Key}={kvp.Value}\"));\n        }\n\n        return buffer.ToString();\n    }\n}\n"
  },
  {
    "path": "DiscordChatExporter.Gui/App.axaml",
    "content": "﻿<Application\n    x:Class=\"DiscordChatExporter.Gui.App\"\n    xmlns=\"https://github.com/avaloniaui\"\n    xmlns:x=\"http://schemas.microsoft.com/winfx/2006/xaml\"\n    xmlns:dialogHostAvalonia=\"clr-namespace:DialogHostAvalonia;assembly=DialogHost.Avalonia\"\n    xmlns:framework=\"clr-namespace:DiscordChatExporter.Gui.Framework\"\n    xmlns:materialAssists=\"clr-namespace:Material.Styles.Assists;assembly=Material.Styles\"\n    xmlns:materialControls=\"clr-namespace:Material.Styles.Controls;assembly=Material.Styles\"\n    xmlns:materialIcons=\"clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia\"\n    xmlns:materialStyles=\"clr-namespace:Material.Styles.Themes;assembly=Material.Styles\"\n    Name=\"DiscordChatExporter\"\n    ActualThemeVariantChanged=\"Application_OnActualThemeVariantChanged\">\n    <Application.DataTemplates>\n        <framework:ViewManager />\n    </Application.DataTemplates>\n\n    <Application.Styles>\n        <!--  This theme is used as a stub to pre-load default resources, the actual colors are set through code  -->\n        <materialStyles:MaterialTheme\n            BaseTheme=\"Light\"\n            PrimaryColor=\"Grey\"\n            SecondaryColor=\"DeepOrange\" />\n        <materialIcons:MaterialIconStyles />\n        <dialogHostAvalonia:DialogHostStyles />\n\n        <!--  Combo box  -->\n        <Style Selector=\"ComboBox\">\n            <Setter Property=\"FontSize\" Value=\"14\" />\n\n            <Style Selector=\"^ /template/ Panel#PART_RootPanel\">\n                <Setter Property=\"Height\" Value=\"22\" />\n            </Style>\n\n            <Style Selector=\"^ /template/ ToggleButton\">\n                <Style Selector=\"^:checked, ^:unchecked\">\n                    <Setter Property=\"Margin\" Value=\"0\" />\n                    <Setter Property=\"CornerRadius\" Value=\"0\" />\n\n                    <Style Selector=\"^ ContentPresenter#contentPresenter\">\n                        <Setter Property=\"Margin\" Value=\"12,8\" />\n                    </Style>\n                </Style>\n            </Style>\n        </Style>\n\n        <!--  Dialog host  -->\n        <Style Selector=\"dialogHostAvalonia|DialogHost\">\n            <Setter Property=\"DialogMargin\" Value=\"0\" />\n        </Style>\n\n        <Style Selector=\"dialogHostAvalonia|DialogOverlayPopupHost\">\n            <Setter Property=\"Margin\" Value=\"48\" />\n            <Setter Property=\"Background\" Value=\"{DynamicResource MaterialPaperBrush}\" />\n        </Style>\n\n        <!--  Snack bar host  -->\n        <Style Selector=\"materialControls|SnackbarHost\">\n            <Setter Property=\"SnackbarHorizontalAlignment\" Value=\"Stretch\" />\n            <Setter Property=\"VerticalContentAlignment\" Value=\"Center\" />\n\n            <Style Selector=\"^ /template/ ItemsControl#PART_SnackbarHostItemsContainer materialControls|Card\">\n                <Setter Property=\"Background\" Value=\"{DynamicResource MaterialDarkBackgroundBrush}\" />\n                <Setter Property=\"Foreground\" Value=\"{DynamicResource MaterialDarkForegroundBrush}\" />\n            </Style>\n\n            <Style Selector=\"^ /template/ ItemsControl#PART_SnackbarHostItemsContainer Button\">\n                <Setter Property=\"Foreground\" Value=\"{DynamicResource MaterialSecondaryMidBrush}\" />\n            </Style>\n        </Style>\n\n        <!--  Progress bar  -->\n        <Style Selector=\"ProgressBar\">\n            <Setter Property=\"Minimum\" Value=\"0\" />\n            <Setter Property=\"Maximum\" Value=\"1\" />\n            <Setter Property=\"Foreground\" Value=\"{DynamicResource MaterialSecondaryMidBrush}\" />\n            <Setter Property=\"materialAssists:TransitionAssist.DisableTransitions\" Value=\"True\" />\n\n            <Style Selector=\"^:horizontal\">\n                <Setter Property=\"MinHeight\" Value=\"0\" />\n            </Style>\n        </Style>\n\n        <!--  Slider  -->\n        <Style Selector=\"Slider\">\n            <Style Selector=\"^ /template/ ProgressBar#PART_ProgressLayer\">\n                <Style Selector=\"^:horizontal\">\n                    <Style Selector=\"^ Panel#PART_InnerPanel\">\n                        <Setter Property=\"Height\" Value=\"2\" />\n\n                        <Style Selector=\"^ Border#PART_InactiveState\">\n                            <Setter Property=\"Margin\" Value=\"0\" />\n                            <Setter Property=\"Height\" Value=\"2\" />\n                        </Style>\n\n                        <Style Selector=\"^ Border#PART_Indicator\">\n                            <Setter Property=\"Margin\" Value=\"0\" />\n                        </Style>\n                    </Style>\n                </Style>\n            </Style>\n\n            <Style Selector=\"^ /template/ Track#PART_Track\">\n                <Style Selector=\"^:horizontal\">\n                    <Setter Property=\"Margin\" Value=\"4,0\" />\n                </Style>\n\n                <Style Selector=\"^ Border#PART_HoverEffect\">\n                    <Setter Property=\"Width\" Value=\"24\" />\n                    <Setter Property=\"Height\" Value=\"24\" />\n                </Style>\n\n                <Style Selector=\"^ Border#PART_ThumbGrip\">\n                    <Setter Property=\"Width\" Value=\"12\" />\n                    <Setter Property=\"Height\" Value=\"12\" />\n                </Style>\n            </Style>\n        </Style>\n\n        <!--  Run  -->\n        <Style Selector=\"Run\">\n            <Setter Property=\"BaselineAlignment\" Value=\"Center\" />\n        </Style>\n\n        <!--  Text box  -->\n        <Style Selector=\"TextBox\">\n            <Setter Property=\"FontSize\" Value=\"14\" />\n        </Style>\n\n        <!--  Toggle button  -->\n        <Style Selector=\"ToggleButton\">\n            <Setter Property=\"TextElement.FontWeight\" Value=\"Medium\" />\n        </Style>\n\n        <!--  Toggle switch  -->\n        <Style Selector=\"ToggleSwitch\">\n            <Setter Property=\"materialAssists:ToggleSwitchAssist.SwitchThumbOffBackground\" Value=\"{DynamicResource ToggleBackgroundBrush}\" />\n        </Style>\n\n        <!--  Tooltip  -->\n        <Style Selector=\"ToolTip\">\n            <Setter Property=\"TextElement.FontSize\" Value=\"14\" />\n            <Setter Property=\"TextElement.FontWeight\" Value=\"Normal\" />\n            <Setter Property=\"TextElement.FontStyle\" Value=\"Normal\" />\n            <Setter Property=\"TextElement.FontStretch\" Value=\"Normal\" />\n        </Style>\n    </Application.Styles>\n\n    <Application.Resources>\n        <ResourceDictionary>\n            <ResourceDictionary.ThemeDictionaries>\n                <ResourceDictionary x:Key=\"Default\">\n                    <SolidColorBrush x:Key=\"ToggleBackgroundBrush\" Color=\"#FFFFFF\" />\n                </ResourceDictionary>\n                <ResourceDictionary x:Key=\"Dark\">\n                    <SolidColorBrush x:Key=\"ToggleBackgroundBrush\" Color=\"#8E8E8E\" />\n                </ResourceDictionary>\n            </ResourceDictionary.ThemeDictionaries>\n        </ResourceDictionary>\n    </Application.Resources>\n</Application>"
  },
  {
    "path": "DiscordChatExporter.Gui/App.axaml.cs",
    "content": "﻿using System;\nusing Avalonia;\nusing Avalonia.Controls.ApplicationLifetimes;\nusing Avalonia.Markup.Xaml;\nusing Avalonia.Media;\nusing Avalonia.Platform;\nusing DiscordChatExporter.Gui.Framework;\nusing DiscordChatExporter.Gui.Localization;\nusing DiscordChatExporter.Gui.Services;\nusing DiscordChatExporter.Gui.Utils;\nusing DiscordChatExporter.Gui.Utils.Extensions;\nusing DiscordChatExporter.Gui.ViewModels;\nusing DiscordChatExporter.Gui.ViewModels.Components;\nusing DiscordChatExporter.Gui.ViewModels.Dialogs;\nusing DiscordChatExporter.Gui.Views;\nusing Material.Styles.Themes;\nusing Microsoft.Extensions.DependencyInjection;\n\nnamespace DiscordChatExporter.Gui;\n\npublic class App : Application, IDisposable\n{\n    private readonly DisposableCollector _eventRoot = new();\n\n    private readonly ServiceProvider _services;\n    private readonly SettingsService _settingsService;\n    private readonly MainViewModel _mainViewModel;\n\n    private bool _isDisposed;\n\n    public App()\n    {\n        var services = new ServiceCollection();\n\n        // Framework\n        services.AddSingleton<DialogManager>();\n        services.AddSingleton<SnackbarManager>();\n        services.AddSingleton<ViewManager>();\n        services.AddSingleton<ViewModelManager>();\n\n        // Services\n        services.AddSingleton<SettingsService>();\n        services.AddSingleton<UpdateService>();\n\n        // Localization\n        services.AddSingleton<LocalizationManager>();\n\n        // View models\n        services.AddTransient<MainViewModel>();\n        services.AddTransient<DashboardViewModel>();\n        services.AddTransient<ExportSetupViewModel>();\n        services.AddTransient<MessageBoxViewModel>();\n        services.AddTransient<SettingsViewModel>();\n\n        _services = services.BuildServiceProvider(true);\n        _settingsService = _services.GetRequiredService<SettingsService>();\n        _mainViewModel = _services.GetRequiredService<ViewModelManager>().CreateMainViewModel();\n\n        // Re-initialize the theme when the user changes it\n        _eventRoot.Add(\n            _settingsService.WatchProperty(\n                o => o.Theme,\n                () =>\n                {\n                    RequestedThemeVariant = _settingsService.Theme switch\n                    {\n                        ThemeVariant.Light => Avalonia.Styling.ThemeVariant.Light,\n                        ThemeVariant.Dark => Avalonia.Styling.ThemeVariant.Dark,\n                        _ => Avalonia.Styling.ThemeVariant.Default,\n                    };\n\n                    InitializeTheme();\n                }\n            )\n        );\n    }\n\n    public override void Initialize()\n    {\n        base.Initialize();\n\n        AvaloniaXamlLoader.Load(this);\n    }\n\n    private void InitializeTheme()\n    {\n        var actualTheme = RequestedThemeVariant?.Key switch\n        {\n            \"Light\" => PlatformThemeVariant.Light,\n            \"Dark\" => PlatformThemeVariant.Dark,\n            _ => PlatformSettings?.GetColorValues().ThemeVariant ?? PlatformThemeVariant.Light,\n        };\n\n        this.LocateMaterialTheme<MaterialThemeBase>().CurrentTheme =\n            actualTheme == PlatformThemeVariant.Light\n                ? Theme.Create(Theme.Light, Color.Parse(\"#343838\"), Color.Parse(\"#F9A825\"))\n                : Theme.Create(Theme.Dark, Color.Parse(\"#E8E8E8\"), Color.Parse(\"#F9A825\"));\n    }\n\n    public override void OnFrameworkInitializationCompleted()\n    {\n        if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)\n        {\n            desktop.MainWindow = new MainView { DataContext = _mainViewModel };\n\n            void OnExit(object? sender, ControlledApplicationLifetimeExitEventArgs args)\n            {\n                if (sender is IControlledApplicationLifetime lifetime)\n                    lifetime.Exit -= OnExit;\n\n                Dispose();\n            }\n\n            // Although `App.Dispose()` is invoked from `Program.Main(...)`, on some platforms\n            // it may be called too late in the shutdown lifecycle. Attach an exit\n            // handler to ensure timely disposal as a safeguard.\n            // https://github.com/Tyrrrz/YoutubeDownloader/issues/795\n            desktop.Exit += OnExit;\n        }\n\n        base.OnFrameworkInitializationCompleted();\n\n        // Set up custom theme colors\n        InitializeTheme();\n\n        // Load settings\n        _settingsService.Load();\n    }\n\n    private void Application_OnActualThemeVariantChanged(object? sender, EventArgs args) =>\n        // Re-initialize the theme when the system theme changes\n        InitializeTheme();\n\n    public void Dispose()\n    {\n        if (_isDisposed)\n            return;\n\n        _isDisposed = true;\n\n        _eventRoot.Dispose();\n        _services.Dispose();\n    }\n}\n"
  },
  {
    "path": "DiscordChatExporter.Gui/Converters/ChannelToHierarchicalNameStringConverter.cs",
    "content": "﻿using System;\nusing System.Globalization;\nusing Avalonia.Data.Converters;\nusing DiscordChatExporter.Core.Discord.Data;\n\nnamespace DiscordChatExporter.Gui.Converters;\n\npublic class ChannelToHierarchicalNameStringConverter : IValueConverter\n{\n    public static ChannelToHierarchicalNameStringConverter Instance { get; } = new();\n\n    public object? Convert(\n        object? value,\n        Type targetType,\n        object? parameter,\n        CultureInfo culture\n    ) => value is Channel channel ? channel.GetHierarchicalName() : null;\n\n    public object ConvertBack(\n        object? value,\n        Type targetType,\n        object? parameter,\n        CultureInfo culture\n    ) => throw new NotSupportedException();\n}\n"
  },
  {
    "path": "DiscordChatExporter.Gui/Converters/ExportFormatToStringConverter.cs",
    "content": "﻿using System;\nusing System.Globalization;\nusing Avalonia.Data.Converters;\nusing DiscordChatExporter.Core.Exporting;\n\nnamespace DiscordChatExporter.Gui.Converters;\n\npublic class ExportFormatToStringConverter : IValueConverter\n{\n    public static ExportFormatToStringConverter Instance { get; } = new();\n\n    public object? Convert(\n        object? value,\n        Type targetType,\n        object? parameter,\n        CultureInfo culture\n    ) => value is ExportFormat format ? format.GetDisplayName() : default;\n\n    public object ConvertBack(\n        object? value,\n        Type targetType,\n        object? parameter,\n        CultureInfo culture\n    ) => throw new NotSupportedException();\n}\n"
  },
  {
    "path": "DiscordChatExporter.Gui/Converters/LocaleToDisplayNameStringConverter.cs",
    "content": "﻿using System;\nusing System.Globalization;\nusing Avalonia.Data.Converters;\n\nnamespace DiscordChatExporter.Gui.Converters;\n\npublic class LocaleToDisplayNameStringConverter : IValueConverter\n{\n    public static LocaleToDisplayNameStringConverter Instance { get; } = new();\n\n    public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture) =>\n        value is string locale && !string.IsNullOrWhiteSpace(locale)\n            ? CultureInfo.GetCultureInfo(locale).DisplayName\n            : \"System\";\n\n    public object ConvertBack(\n        object? value,\n        Type targetType,\n        object? parameter,\n        CultureInfo culture\n    ) => throw new NotSupportedException();\n}\n"
  },
  {
    "path": "DiscordChatExporter.Gui/Converters/MarkdownToInlinesConverter.cs",
    "content": "using System;\nusing System.Globalization;\nusing System.Linq;\nusing Avalonia.Controls.Documents;\nusing Avalonia.Data.Converters;\nusing Avalonia.Media;\nusing DiscordChatExporter.Gui.Utils.Extensions;\nusing DiscordChatExporter.Gui.Views.Controls;\nusing Markdig;\nusing Markdig.Syntax;\nusing Markdig.Syntax.Inlines;\nusing MarkdownInline = Markdig.Syntax.Inlines.Inline;\n\nnamespace DiscordChatExporter.Gui.Converters;\n\npublic class MarkdownToInlinesConverter : IValueConverter\n{\n    public static readonly MarkdownToInlinesConverter Instance = new();\n\n    private static readonly MarkdownPipeline MarkdownPipeline = new MarkdownPipelineBuilder()\n        .UseEmphasisExtras()\n        .Build();\n\n    private static void ProcessInline(\n        InlineCollection inlines,\n        MarkdownInline markdownInline,\n        FontWeight? fontWeight = null,\n        FontStyle? fontStyle = null,\n        TextDecorationCollection? textDecorations = null\n    )\n    {\n        switch (markdownInline)\n        {\n            case LiteralInline literal:\n            {\n                var run = new Run(literal.Content.ToString());\n\n                if (fontWeight is not null)\n                    run.FontWeight = fontWeight.Value;\n                if (fontStyle is not null)\n                    run.FontStyle = fontStyle.Value;\n                if (textDecorations is not null)\n                    run.TextDecorations = textDecorations;\n\n                inlines.Add(run);\n                break;\n            }\n\n            case LineBreakInline:\n            {\n                inlines.Add(new LineBreak());\n                break;\n            }\n\n            case EmphasisInline emphasis:\n            {\n                var newWeight = fontWeight;\n                var newStyle = fontStyle;\n                var newDecorations = textDecorations;\n\n                switch (emphasis.DelimiterChar)\n                {\n                    case '*' or '_' when emphasis.DelimiterCount == 2:\n                        newWeight = FontWeight.SemiBold;\n                        break;\n                    case '*' or '_':\n                        newStyle = FontStyle.Italic;\n                        break;\n                    case '~':\n                        newDecorations = TextDecorations.Strikethrough;\n                        break;\n                    case '+':\n                        newDecorations = TextDecorations.Underline;\n                        break;\n                }\n\n                foreach (var child in emphasis)\n                    ProcessInline(inlines, child, newWeight, newStyle, newDecorations);\n\n                break;\n            }\n\n            case LinkInline link:\n            {\n                inlines.Add(new HyperLink { Text = link.GetInnerText(), Url = link.Url });\n                break;\n            }\n\n            case ContainerInline container:\n            {\n                foreach (var child in container)\n                    ProcessInline(inlines, child, fontWeight, fontStyle, textDecorations);\n\n                break;\n            }\n        }\n    }\n\n    public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)\n    {\n        var inlines = new InlineCollection();\n        if (value is not string { Length: > 0 } text)\n            return inlines;\n\n        var isFirst = true;\n\n        foreach (var block in Markdown.Parse(text, MarkdownPipeline))\n        {\n            switch (block)\n            {\n                case ParagraphBlock { Inline: not null } paragraph:\n                {\n                    if (!isFirst)\n                    {\n                        // Insert a blank line between paragraphs\n                        inlines.Add(new LineBreak());\n                        inlines.Add(new LineBreak());\n                    }\n\n                    isFirst = false;\n\n                    foreach (var markdownInline in paragraph.Inline!)\n                        ProcessInline(inlines, markdownInline);\n\n                    break;\n                }\n\n                case ListBlock list:\n                {\n                    var itemOrder = 1;\n                    if (list.IsOrdered && int.TryParse(list.OrderedStart, out var startNum))\n                        itemOrder = startNum;\n\n                    foreach (var listItem in list.OfType<ListItemBlock>())\n                    {\n                        if (!isFirst)\n                            inlines.Add(new LineBreak());\n                        isFirst = false;\n\n                        var prefix = list.IsOrdered ? $\"{itemOrder++}. \" : $\"{list.BulletType}  \";\n\n                        inlines.Add(new Run(prefix));\n\n                        foreach (var subBlock in listItem.OfType<ParagraphBlock>())\n                        {\n                            if (subBlock is { Inline: not null })\n                            {\n                                foreach (var markdownInline in subBlock.Inline)\n                                    ProcessInline(inlines, markdownInline);\n                            }\n                        }\n                    }\n\n                    break;\n                }\n            }\n        }\n\n        return inlines;\n    }\n\n    public object? ConvertBack(\n        object? value,\n        Type targetType,\n        object? parameter,\n        CultureInfo culture\n    ) => throw new NotSupportedException();\n}\n"
  },
  {
    "path": "DiscordChatExporter.Gui/Converters/RateLimitPreferenceToStringConverter.cs",
    "content": "﻿using System;\nusing System.Globalization;\nusing Avalonia.Data.Converters;\nusing DiscordChatExporter.Core.Discord;\n\nnamespace DiscordChatExporter.Gui.Converters;\n\npublic class RateLimitPreferenceToStringConverter : IValueConverter\n{\n    public static RateLimitPreferenceToStringConverter Instance { get; } = new();\n\n    public object? Convert(\n        object? value,\n        Type targetType,\n        object? parameter,\n        CultureInfo culture\n    ) =>\n        value is RateLimitPreference rateLimitPreference\n            ? rateLimitPreference.GetDisplayName()\n            : default;\n\n    public object ConvertBack(\n        object? value,\n        Type targetType,\n        object? parameter,\n        CultureInfo culture\n    ) => throw new NotSupportedException();\n}\n"
  },
  {
    "path": "DiscordChatExporter.Gui/Converters/SnowflakeToTimestampStringConverter.cs",
    "content": "﻿using System;\nusing System.Globalization;\nusing Avalonia.Data.Converters;\nusing DiscordChatExporter.Core.Discord;\n\nnamespace DiscordChatExporter.Gui.Converters;\n\npublic class SnowflakeToTimestampStringConverter : IValueConverter\n{\n    public static SnowflakeToTimestampStringConverter Instance { get; } = new();\n\n    public object? Convert(\n        object? value,\n        Type targetType,\n        object? parameter,\n        CultureInfo culture\n    ) => value is Snowflake snowflake ? snowflake.ToDate().ToString(\"g\", culture) : null;\n\n    public object ConvertBack(\n        object? value,\n        Type targetType,\n        object? parameter,\n        CultureInfo culture\n    ) => throw new NotSupportedException();\n}\n"
  },
  {
    "path": "DiscordChatExporter.Gui/DiscordChatExporter.Gui.csproj",
    "content": "<Project Sdk=\"Microsoft.NET.Sdk\">\n  <PropertyGroup>\n    <OutputType>WinExe</OutputType>\n    <AssemblyName>DiscordChatExporter</AssemblyName>\n    <ApplicationIcon>..\\favicon.ico</ApplicationIcon>\n    <PublishTrimmed>true</PublishTrimmed>\n    <CopyOutputSymbolsToPublishDirectory>false</CopyOutputSymbolsToPublishDirectory>\n    <AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>\n  </PropertyGroup>\n\n  <PropertyGroup>\n    <EncryptionSalt>HimalayanPinkSalt</EncryptionSalt>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <!-- Expose this property in code -->\n    <ProjectProperty Include=\"EncryptionSalt\" />\n  </ItemGroup>\n\n  <PropertyGroup>\n    <PublishMacOSBundle>false</PublishMacOSBundle>\n  </PropertyGroup>\n\n  <!-- HACK: Disable trim warnings because they seem to break when the code contains C# 14 extension blocks -->\n  <PropertyGroup>\n    <EnableTrimAnalyzer>false</EnableTrimAnalyzer>\n    <EnableAotAnalyzer>false</EnableAotAnalyzer>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <AvaloniaResource Include=\"..\\favicon.ico\" Link=\"favicon.ico\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <PackageReference Include=\"AsyncImageLoader.Avalonia\" />\n    <PackageReference Include=\"Avalonia\" />\n    <PackageReference Include=\"Avalonia.Desktop\" />\n    <PackageReference Include=\"Avalonia.Diagnostics\" Condition=\"'$(Configuration)' == 'Debug'\" />\n    <PackageReference Include=\"Cogwheel\" />\n    <PackageReference Include=\"CommunityToolkit.Mvvm\" />\n    <PackageReference Include=\"CSharpier.MsBuild\" PrivateAssets=\"all\" />\n    <PackageReference Include=\"Deorcify\" PrivateAssets=\"all\" />\n    <PackageReference Include=\"DialogHost.Avalonia\" />\n    <PackageReference Include=\"Gress\" />\n    <PackageReference Include=\"Markdig\" />\n    <PackageReference Include=\"Material.Avalonia\" />\n    <PackageReference Include=\"Material.Icons.Avalonia\" />\n    <PackageReference Include=\"Microsoft.Extensions.DependencyInjection\" />\n    <PackageReference Include=\"Onova\" />\n    <PackageReference Include=\"ThisAssembly.Project\" PrivateAssets=\"all\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <ProjectReference Include=\"..\\DiscordChatExporter.Core\\DiscordChatExporter.Core.csproj\" />\n  </ItemGroup>\n\n  <Target Name=\"PublishMacOSBundle\" AfterTargets=\"Publish\" Condition=\"$(PublishMacOSBundle)\">\n    <Exec\n      Command=\"pwsh -ExecutionPolicy Bypass -File $(ProjectDir)/Publish-MacOSBundle.ps1 -PublishDirPath $(PublishDir) -IconsFilePath $(ProjectDir)/../favicon.icns -FullVersion $(Version) -ShortVersion $(AssemblyVersion)\"\n      LogStandardErrorAsError=\"true\"\n    />\n  </Target>\n</Project>\n"
  },
  {
    "path": "DiscordChatExporter.Gui/Framework/DialogManager.cs",
    "content": "﻿using System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Linq;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing Avalonia;\nusing Avalonia.Platform.Storage;\nusing DialogHostAvalonia;\nusing DiscordChatExporter.Gui.Utils.Extensions;\n\nnamespace DiscordChatExporter.Gui.Framework;\n\npublic class DialogManager : IDisposable\n{\n    private readonly SemaphoreSlim _dialogLock = new(1, 1);\n\n    public async Task<T?> ShowDialogAsync<T>(DialogViewModelBase<T> dialog)\n    {\n        await _dialogLock.WaitAsync();\n        try\n        {\n            await DialogHost.Show(\n                dialog,\n                // It's fine to await in a void method here because it's an event handler\n                // ReSharper disable once AsyncVoidLambda\n                async (object _, DialogOpenedEventArgs args) =>\n                {\n                    await dialog.WaitForCloseAsync();\n\n                    try\n                    {\n                        args.Session.Close();\n                    }\n                    catch (InvalidOperationException)\n                    {\n                        // Dialog host is already processing a close operation\n                    }\n                }\n            );\n\n            // Yield to allow DialogHost to fully reset its state before\n            // another dialog is shown (e.g. when dialogs are shown sequentially).\n            await Task.Yield();\n\n            return dialog.DialogResult;\n        }\n        finally\n        {\n            _dialogLock.Release();\n        }\n    }\n\n    public async Task<string?> PromptSaveFilePathAsync(\n        IReadOnlyList<FilePickerFileType>? fileTypes = null,\n        string defaultFilePath = \"\"\n    )\n    {\n        var topLevel =\n            Application.Current?.ApplicationLifetime?.TryGetTopLevel()\n            ?? throw new ApplicationException(\"Could not find the top-level visual element.\");\n\n        var file = await topLevel.StorageProvider.SaveFilePickerAsync(\n            new FilePickerSaveOptions\n            {\n                FileTypeChoices = fileTypes,\n                SuggestedFileName = defaultFilePath,\n                DefaultExtension = Path.GetExtension(defaultFilePath).TrimStart('.'),\n            }\n        );\n\n        return file?.TryGetLocalPath() ?? file?.Path.ToString();\n    }\n\n    public async Task<string?> PromptDirectoryPathAsync(string defaultDirPath = \"\")\n    {\n        var topLevel =\n            Application.Current?.ApplicationLifetime?.TryGetTopLevel()\n            ?? throw new ApplicationException(\"Could not find the top-level visual element.\");\n\n        var result = await topLevel.StorageProvider.OpenFolderPickerAsync(\n            new FolderPickerOpenOptions\n            {\n                AllowMultiple = false,\n                SuggestedStartLocation = await topLevel.StorageProvider.TryGetFolderFromPathAsync(\n                    defaultDirPath\n                ),\n            }\n        );\n\n        var directory = result.FirstOrDefault();\n        if (directory is null)\n            return null;\n\n        return directory.TryGetLocalPath() ?? directory.Path.ToString();\n    }\n\n    public void Dispose() => _dialogLock.Dispose();\n}\n"
  },
  {
    "path": "DiscordChatExporter.Gui/Framework/DialogViewModelBase.cs",
    "content": "﻿using System.Threading.Tasks;\nusing CommunityToolkit.Mvvm.ComponentModel;\nusing CommunityToolkit.Mvvm.Input;\n\nnamespace DiscordChatExporter.Gui.Framework;\n\npublic abstract partial class DialogViewModelBase<T> : ViewModelBase\n{\n    private readonly TaskCompletionSource<T> _closeTcs = new(\n        TaskCreationOptions.RunContinuationsAsynchronously\n    );\n\n    [ObservableProperty]\n    public partial T? DialogResult { get; set; }\n\n    [RelayCommand]\n    protected void Close(T dialogResult)\n    {\n        DialogResult = dialogResult;\n        _closeTcs.TrySetResult(dialogResult);\n    }\n\n    public async Task<T> WaitForCloseAsync() => await _closeTcs.Task;\n}\n\npublic abstract class DialogViewModelBase : DialogViewModelBase<bool?>;\n"
  },
  {
    "path": "DiscordChatExporter.Gui/Framework/SnackbarManager.cs",
    "content": "﻿using System;\nusing Avalonia.Threading;\nusing Material.Styles.Controls;\nusing Material.Styles.Models;\n\nnamespace DiscordChatExporter.Gui.Framework;\n\npublic class SnackbarManager\n{\n    private readonly TimeSpan _defaultDuration = TimeSpan.FromSeconds(5);\n\n    public void Notify(string message, TimeSpan? duration = null) =>\n        SnackbarHost.Post(\n            new SnackbarModel(message, duration ?? _defaultDuration),\n            null,\n            DispatcherPriority.Normal\n        );\n\n    public void Notify(\n        string message,\n        string actionText,\n        Action actionHandler,\n        TimeSpan? duration = null\n    ) =>\n        SnackbarHost.Post(\n            new SnackbarModel(\n                message,\n                duration ?? _defaultDuration,\n                new SnackbarButtonModel { Text = actionText, Action = actionHandler }\n            ),\n            null,\n            DispatcherPriority.Normal\n        );\n}\n"
  },
  {
    "path": "DiscordChatExporter.Gui/Framework/ThemeVariant.cs",
    "content": "﻿namespace DiscordChatExporter.Gui.Framework;\n\npublic enum ThemeVariant\n{\n    System,\n    Light,\n    Dark,\n}\n"
  },
  {
    "path": "DiscordChatExporter.Gui/Framework/UserControl.cs",
    "content": "﻿using System;\nusing Avalonia.Controls;\n\nnamespace DiscordChatExporter.Gui.Framework;\n\npublic class UserControl<TDataContext> : UserControl\n{\n    public new TDataContext DataContext\n    {\n        get =>\n            base.DataContext is TDataContext dataContext\n                ? dataContext\n                : throw new InvalidCastException(\n                    $\"DataContext is null or not of the expected type '{typeof(TDataContext).FullName}'.\"\n                );\n        set => base.DataContext = value;\n    }\n}\n"
  },
  {
    "path": "DiscordChatExporter.Gui/Framework/ViewManager.cs",
    "content": "﻿using Avalonia.Controls;\nusing Avalonia.Controls.Templates;\nusing DiscordChatExporter.Gui.ViewModels;\nusing DiscordChatExporter.Gui.ViewModels.Components;\nusing DiscordChatExporter.Gui.ViewModels.Dialogs;\nusing DiscordChatExporter.Gui.Views;\nusing DiscordChatExporter.Gui.Views.Components;\nusing DiscordChatExporter.Gui.Views.Dialogs;\n\nnamespace DiscordChatExporter.Gui.Framework;\n\npublic partial class ViewManager\n{\n    private Control? TryCreateView(ViewModelBase viewModel) =>\n        viewModel switch\n        {\n            MainViewModel => new MainView(),\n            DashboardViewModel => new DashboardView(),\n            ExportSetupViewModel => new ExportSetupView(),\n            MessageBoxViewModel => new MessageBoxView(),\n            SettingsViewModel => new SettingsView(),\n            _ => null,\n        };\n\n    public Control? TryBindView(ViewModelBase viewModel)\n    {\n        var view = TryCreateView(viewModel);\n        if (view is null)\n            return null;\n\n        view.DataContext ??= viewModel;\n\n        return view;\n    }\n}\n\npublic partial class ViewManager : IDataTemplate\n{\n    bool IDataTemplate.Match(object? data) => data is ViewModelBase;\n\n    Control? ITemplate<object?, Control?>.Build(object? data) =>\n        data is ViewModelBase viewModel ? TryBindView(viewModel) : null;\n}\n"
  },
  {
    "path": "DiscordChatExporter.Gui/Framework/ViewModelBase.cs",
    "content": "﻿using System;\nusing CommunityToolkit.Mvvm.ComponentModel;\n\nnamespace DiscordChatExporter.Gui.Framework;\n\npublic abstract class ViewModelBase : ObservableObject, IDisposable\n{\n    ~ViewModelBase() => Dispose(false);\n\n    protected void OnAllPropertiesChanged() => OnPropertyChanged(string.Empty);\n\n    protected virtual void Dispose(bool disposing) { }\n\n    public void Dispose()\n    {\n        Dispose(true);\n        GC.SuppressFinalize(this);\n    }\n}\n"
  },
  {
    "path": "DiscordChatExporter.Gui/Framework/ViewModelManager.cs",
    "content": "﻿using System;\nusing System.Collections.Generic;\nusing DiscordChatExporter.Core.Discord.Data;\nusing DiscordChatExporter.Gui.ViewModels;\nusing DiscordChatExporter.Gui.ViewModels.Components;\nusing DiscordChatExporter.Gui.ViewModels.Dialogs;\nusing Microsoft.Extensions.DependencyInjection;\n\nnamespace DiscordChatExporter.Gui.Framework;\n\npublic class ViewModelManager(IServiceProvider services)\n{\n    public MainViewModel CreateMainViewModel() => services.GetRequiredService<MainViewModel>();\n\n    public DashboardViewModel CreateDashboardViewModel() =>\n        services.GetRequiredService<DashboardViewModel>();\n\n    public ExportSetupViewModel CreateExportSetupViewModel(\n        Guild guild,\n        IReadOnlyList<Channel> channels\n    )\n    {\n        var viewModel = services.GetRequiredService<ExportSetupViewModel>();\n\n        viewModel.Guild = guild;\n        viewModel.Channels = channels;\n\n        return viewModel;\n    }\n\n    public MessageBoxViewModel CreateMessageBoxViewModel(\n        string title,\n        string message,\n        string? okButtonText,\n        string? cancelButtonText\n    )\n    {\n        var viewModel = services.GetRequiredService<MessageBoxViewModel>();\n\n        viewModel.Title = title;\n        viewModel.Message = message;\n        viewModel.DefaultButtonText = okButtonText;\n        viewModel.CancelButtonText = cancelButtonText;\n\n        return viewModel;\n    }\n\n    public MessageBoxViewModel CreateMessageBoxViewModel(string title, string message) =>\n        CreateMessageBoxViewModel(title, message, \"CLOSE\", null);\n\n    public SettingsViewModel CreateSettingsViewModel() =>\n        services.GetRequiredService<SettingsViewModel>();\n}\n"
  },
  {
    "path": "DiscordChatExporter.Gui/Framework/Window.cs",
    "content": "﻿using System;\nusing Avalonia.Controls;\n\nnamespace DiscordChatExporter.Gui.Framework;\n\npublic class Window<TDataContext> : Window\n{\n    public new TDataContext DataContext\n    {\n        get =>\n            base.DataContext is TDataContext dataContext\n                ? dataContext\n                : throw new InvalidCastException(\n                    $\"DataContext is null or not of the expected type '{typeof(TDataContext).FullName}'.\"\n                );\n        set => base.DataContext = value;\n    }\n}\n"
  },
  {
    "path": "DiscordChatExporter.Gui/Localization/Language.cs",
    "content": "namespace DiscordChatExporter.Gui.Localization;\n\npublic enum Language\n{\n    System,\n    English,\n    Ukrainian,\n    German,\n    French,\n    Spanish,\n}\n"
  },
  {
    "path": "DiscordChatExporter.Gui/Localization/LocalizationManager.English.cs",
    "content": "using System.Collections.Generic;\n\nnamespace DiscordChatExporter.Gui.Localization;\n\npublic partial class LocalizationManager\n{\n    private static readonly IReadOnlyDictionary<string, string> EnglishLocalization =\n        new Dictionary<string, string>\n        {\n            // Dashboard\n            [nameof(PullGuildsTooltip)] = \"Pull available servers and channels (Enter)\",\n            [nameof(SettingsTooltip)] = \"Settings\",\n            [nameof(LastMessageSentTooltip)] = \"Last message sent:\",\n            [nameof(TokenWatermark)] = \"Token\",\n            // Token instructions (personal account)\n            [nameof(TokenPersonalHeader)] = \"To get the token for your personal account:\",\n            [nameof(TokenPersonalTosWarning)] =\n                \"*  Automating user accounts is technically against TOS — **use at your own risk**!\",\n            [nameof(TokenPersonalInstructions)] = \"\"\"\n                1. Open Discord in your web browser and login\n                2. Open any server or direct message channel\n                3. Press **Ctrl+Shift+I** to show developer tools\n                4. Navigate to the **Network** tab\n                5. Press **Ctrl+R** to reload\n                6. Switch between random channels to trigger network requests\n                7. Search for a request that starts with **messages**\n                8. Select the **Headers** tab on the right\n                9. Scroll down to the **Request Headers** section\n                10. Copy the value of the **authorization** header\n                \"\"\",\n            // Token instructions (bot)\n            [nameof(TokenBotHeader)] = \"To get the token for your bot:\",\n            [nameof(TokenBotInstructions)] = \"\"\"\n                The token is generated during bot creation. If you lost it, generate a new one:\n\n                1. Open Discord [developer portal](https://discord.com/developers/applications)\n                2. Open your application's settings\n                3. Navigate to the **Bot** section on the left\n                4. Under **Token** click **Reset Token**\n                5. Click **Yes, do it!** and authenticate to confirm\n                *  Integrations using the previous token will stop working until updated\n                *  Your bot needs to have the **Message Content Intent** enabled to read messages\n                \"\"\",\n            [nameof(TokenHelpText)] =\n                \"If you have questions or issues, please refer to the [documentation](https://github.com/Tyrrrz/DiscordChatExporter/tree/prime/.docs)\",\n            // Settings\n            [nameof(SettingsTitle)] = \"Settings\",\n            [nameof(ThemeLabel)] = \"Theme\",\n            [nameof(ThemeTooltip)] = \"Preferred user interface theme\",\n            [nameof(LanguageLabel)] = \"Language\",\n            [nameof(LanguageTooltip)] = \"Preferred user interface language\",\n            [nameof(AutoUpdateLabel)] = \"Auto-update\",\n            [nameof(AutoUpdateTooltip)] = \"Perform automatic updates on every launch\",\n            [nameof(PersistTokenLabel)] = \"Persist token\",\n            [nameof(PersistTokenTooltip)] = \"\"\"\n                Save the last used token to a file so that it can be persisted between sessions.\n                **Warning**: although the token is stored with encryption, it may still be recovered by an attacker who has access to your system.\n                \"\"\",\n            [nameof(RateLimitPreferenceLabel)] = \"Rate limit preference\",\n            [nameof(RateLimitPreferenceTooltip)] =\n                \"Whether to respect advisory rate limits. If disabled, only hard rate limits (i.e. 429 responses) will be respected.\",\n            [nameof(ShowThreadsLabel)] = \"Show threads\",\n            [nameof(ShowThreadsTooltip)] = \"Which types of threads to show in the channel list\",\n            [nameof(LocaleLabel)] = \"Locale\",\n            [nameof(LocaleTooltip)] = \"Locale to use when formatting dates and numbers\",\n            [nameof(NormalizeToUtcLabel)] = \"Normalize to UTC\",\n            [nameof(NormalizeToUtcTooltip)] = \"Normalize all timestamps to UTC+0\",\n            [nameof(ParallelLimitLabel)] = \"Parallel limit\",\n            [nameof(ParallelLimitTooltip)] = \"How many channels can be exported at the same time\",\n            // Export Setup\n            [nameof(ChannelsSelectedText)] = \"channels selected\",\n            [nameof(OutputPathLabel)] = \"Output path\",\n            [nameof(OutputPathTooltip)] = \"\"\"\n                Output file or directory path.\n\n                If a directory is specified, file names will be generated automatically based on the channel names and export parameters.\n\n                Directory paths must end with a slash to avoid ambiguity.\n\n                Available template tokens:\n                **%g** — server ID\n                **%G** — server name\n                **%t** — category ID\n                **%T** — category name\n                **%c** — channel ID\n                **%C** — channel name\n                **%p** — channel position\n                **%P** — category position\n                **%a** — after date\n                **%b** — before date\n                **%d** — current date\n                \"\"\",\n            [nameof(FormatLabel)] = \"Format\",\n            [nameof(FormatTooltip)] = \"Export format\",\n            [nameof(AfterDateLabel)] = \"After (date)\",\n            [nameof(AfterDateTooltip)] = \"Only include messages sent after this date\",\n            [nameof(BeforeDateLabel)] = \"Before (date)\",\n            [nameof(BeforeDateTooltip)] = \"Only include messages sent before this date\",\n            [nameof(AfterTimeLabel)] = \"After (time)\",\n            [nameof(AfterTimeTooltip)] = \"Only include messages sent after this time\",\n            [nameof(BeforeTimeLabel)] = \"Before (time)\",\n            [nameof(BeforeTimeTooltip)] = \"Only include messages sent before this time\",\n            [nameof(PartitionLimitLabel)] = \"Partition limit\",\n            [nameof(PartitionLimitTooltip)] =\n                \"Split the output into partitions, each limited to the specified number of messages (e.g. '100') or file size (e.g. '10mb')\",\n            [nameof(MessageFilterLabel)] = \"Message filter\",\n            [nameof(MessageFilterTooltip)] =\n                \"Only include messages that satisfy this filter (e.g. 'from:foo#1234' or 'has:image'). See the documentation for more info.\",\n            [nameof(ReverseMessageOrderLabel)] = \"Reverse messages\",\n            [nameof(ReverseMessageOrderTooltip)] =\n                \"Export messages in reverse chronological order (newest first)\",\n            [nameof(FormatMarkdownLabel)] = \"Format markdown\",\n            [nameof(FormatMarkdownTooltip)] =\n                \"Process markdown, mentions, and other special tokens\",\n            [nameof(DownloadAssetsLabel)] = \"Download assets\",\n            [nameof(DownloadAssetsTooltip)] =\n                \"Download assets referenced by the export (user avatars, attached files, embedded images, etc.)\",\n            [nameof(ReuseAssetsLabel)] = \"Reuse assets\",\n            [nameof(ReuseAssetsTooltip)] =\n                \"Reuse previously downloaded assets to avoid redundant requests\",\n            [nameof(AssetsDirPathLabel)] = \"Assets directory path\",\n            [nameof(AssetsDirPathTooltip)] =\n                \"Download assets to this directory. If not specified, the asset directory path will be derived from the output path.\",\n            [nameof(AdvancedOptionsTooltip)] = \"Toggle advanced options\",\n            [nameof(ExportButton)] = \"EXPORT\",\n            // Common buttons\n            [nameof(CloseButton)] = \"CLOSE\",\n            [nameof(CancelButton)] = \"CANCEL\",\n            // Dialog messages\n            [nameof(UkraineSupportTitle)] = \"Thank you for supporting Ukraine!\",\n            [nameof(UkraineSupportMessage)] = \"\"\"\n                As Russia wages a genocidal war against my country, I'm grateful to everyone who continues to stand with Ukraine in our fight for freedom.\n\n                Click LEARN MORE to find ways that you can help.\n                \"\"\",\n            [nameof(LearnMoreButton)] = \"LEARN MORE\",\n            [nameof(UnstableBuildTitle)] = \"Unstable build warning\",\n            [nameof(UnstableBuildMessage)] = \"\"\"\n                You're using a development build of {0}. These builds are not thoroughly tested and may contain bugs.\n\n                Auto-updates are disabled for development builds.\n\n                Click SEE RELEASES if you want to download a stable release instead.\n                \"\"\",\n            [nameof(SeeReleasesButton)] = \"SEE RELEASES\",\n            [nameof(UpdateDownloadingMessage)] = \"Downloading update to {0} v{1}...\",\n            [nameof(UpdateReadyMessage)] =\n                \"Update has been downloaded and will be installed when you exit\",\n            [nameof(UpdateInstallNowButton)] = \"INSTALL NOW\",\n            [nameof(UpdateFailedMessage)] = \"Failed to perform application update\",\n            [nameof(ErrorPullingGuildsTitle)] = \"Error pulling servers\",\n            [nameof(ErrorPullingChannelsTitle)] = \"Error pulling channels\",\n            [nameof(ErrorExportingTitle)] = \"Error exporting channel(s)\",\n            [nameof(SuccessfulExportMessage)] = \"Successfully exported {0} channel(s)\",\n        };\n}\n"
  },
  {
    "path": "DiscordChatExporter.Gui/Localization/LocalizationManager.French.cs",
    "content": "using System.Collections.Generic;\n\nnamespace DiscordChatExporter.Gui.Localization;\n\npublic partial class LocalizationManager\n{\n    private static readonly IReadOnlyDictionary<string, string> FrenchLocalization = new Dictionary<\n        string,\n        string\n    >\n    {\n        // Dashboard\n        [nameof(PullGuildsTooltip)] = \"Charger les serveurs et canaux disponibles (Entrée)\",\n        [nameof(SettingsTooltip)] = \"Paramètres\",\n        [nameof(LastMessageSentTooltip)] = \"Dernier message envoyé :\",\n        [nameof(TokenWatermark)] = \"Token\",\n        // Token instructions (personal account)\n        [nameof(TokenPersonalHeader)] = \"Obtenir le token pour votre compte personnel :\",\n        [nameof(TokenPersonalTosWarning)] =\n            \"*  L'automatisation des comptes est techniquement contraire aux CGU — **à vos risques et périls**!\",\n        [nameof(TokenPersonalInstructions)] = \"\"\"\n            1. Ouvrez Discord dans votre navigateur web et connectez-vous\n            2. Ouvrez n'importe quel serveur ou canal de message direct\n            3. Appuyez sur **Ctrl+Shift+I** pour afficher les outils de développement\n            4. Naviguez vers l'onglet **Network**\n            5. Appuyez sur **Ctrl+R** pour recharger\n            6. Changez de canal pour déclencher des requêtes réseau\n            7. Cherchez une requête commençant par **messages**\n            8. Sélectionnez l'onglet **Headers** à droite\n            9. Faites défiler jusqu'à la section **Request Headers**\n            10. Copiez la valeur de l'en-tête **authorization**\n            \"\"\",\n        // Token instructions (bot)\n        [nameof(TokenBotHeader)] = \"Obtenir le token pour votre bot :\",\n        [nameof(TokenBotInstructions)] = \"\"\"\n            Le token est généré lors de la création du bot. Si vous l'avez perdu, générez-en un nouveau :\n\n            1. Ouvrez Discord [portail développeur](https://discord.com/developers/applications)\n            2. Ouvrez les paramètres de votre application\n            3. Naviguez vers la section **Bot** à gauche\n            4. Sous **Token**, cliquez sur **Reset Token**\n            5. Cliquez sur **Yes, do it!** et confirmez\n            *  Les intégrations utilisant l'ancien token cesseront de fonctionner jusqu'à leur mise à jour\n            *  Votre bot doit avoir l'option **Message Content Intent** activée pour lire les messages\n            \"\"\",\n        [nameof(TokenHelpText)] =\n            \"Pour les questions ou problèmes, veuillez consulter la [documentation](https://github.com/Tyrrrz/DiscordChatExporter/tree/prime/.docs)\",\n        // Settings\n        [nameof(SettingsTitle)] = \"Paramètres\",\n        [nameof(ThemeLabel)] = \"Thème\",\n        [nameof(ThemeTooltip)] = \"Thème d'interface préféré\",\n        [nameof(LanguageLabel)] = \"Langue\",\n        [nameof(LanguageTooltip)] = \"Langue d'interface préférée\",\n        [nameof(AutoUpdateLabel)] = \"Mise à jour automatique\",\n        [nameof(AutoUpdateTooltip)] = \"Effectuer des mises à jour automatiques à chaque lancement\",\n        [nameof(PersistTokenLabel)] = \"Conserver le token\",\n        [nameof(PersistTokenTooltip)] = \"\"\"\n            Enregistrer le dernier token utilisé dans un fichier pour le conserver entre les sessions.\n            **Avertissement** : bien que le token soit stocké avec chiffrement, il peut toujours être récupéré par un attaquant ayant accès à votre système.\n            \"\"\",\n        [nameof(RateLimitPreferenceLabel)] = \"Préférence de limite de débit\",\n        [nameof(RateLimitPreferenceTooltip)] =\n            \"Indique s'il faut respecter les limites de débit recommandées. Si désactivé, seules les limites strictes (réponses 429) seront respectées.\",\n        [nameof(ShowThreadsLabel)] = \"Afficher les fils\",\n        [nameof(ShowThreadsTooltip)] = \"Quels types de fils afficher dans la liste des canaux\",\n        [nameof(LocaleLabel)] = \"Locale\",\n        [nameof(LocaleTooltip)] = \"Locale à utiliser pour le formatage des dates et des nombres\",\n        [nameof(NormalizeToUtcLabel)] = \"Normaliser en UTC\",\n        [nameof(NormalizeToUtcTooltip)] = \"Normaliser tous les horodatages en UTC+0\",\n        [nameof(ParallelLimitLabel)] = \"Limite parallèle\",\n        [nameof(ParallelLimitTooltip)] = \"Combien de canaux peuvent être exportés simultanément\",\n        // Export Setup\n        [nameof(ChannelsSelectedText)] = \"canaux sélectionnés\",\n        [nameof(OutputPathLabel)] = \"Chemin de sortie\",\n        [nameof(OutputPathTooltip)] = \"\"\"\n            Chemin du fichier ou répertoire de sortie.\n\n            Si un répertoire est spécifié, les noms de fichiers seront générés automatiquement en fonction des noms de canaux et des paramètres d'exportation.\n\n            Les chemins de répertoire doivent se terminer par un slash pour éviter toute ambiguïté.\n\n            Jetons de modèle disponibles :\n            **%g** — ID du serveur\n            **%G** — nom du serveur\n            **%t** — ID de la catégorie\n            **%T** — nom de la catégorie\n            **%c** — ID du canal\n            **%C** — nom du canal\n            **%p** — position du canal\n            **%P** — position de la catégorie\n            **%a** — date après\n            **%b** — date avant\n            **%d** — date actuelle\n            \"\"\",\n        [nameof(FormatLabel)] = \"Format\",\n        [nameof(FormatTooltip)] = \"Format d'exportation\",\n        [nameof(AfterDateLabel)] = \"Après (date)\",\n        [nameof(AfterDateTooltip)] = \"Inclure uniquement les messages envoyés après cette date\",\n        [nameof(BeforeDateLabel)] = \"Avant (date)\",\n        [nameof(BeforeDateTooltip)] = \"Inclure uniquement les messages envoyés avant cette date\",\n        [nameof(AfterTimeLabel)] = \"Après (heure)\",\n        [nameof(AfterTimeTooltip)] = \"Inclure uniquement les messages envoyés après cette heure\",\n        [nameof(BeforeTimeLabel)] = \"Avant (heure)\",\n        [nameof(BeforeTimeTooltip)] = \"Inclure uniquement les messages envoyés avant cette heure\",\n        [nameof(PartitionLimitLabel)] = \"Limite de partition\",\n        [nameof(PartitionLimitTooltip)] =\n            \"Diviser la sortie en partitions, chacune limitée au nombre de messages spécifié (ex. '100') ou à la taille de fichier (ex. '10mb')\",\n        [nameof(MessageFilterLabel)] = \"Filtre de messages\",\n        [nameof(MessageFilterTooltip)] =\n            \"Inclure uniquement les messages satisfaisant ce filtre (ex. 'from:foo#1234' ou 'has:image'). Voir la documentation pour plus d'informations.\",\n        [nameof(ReverseMessageOrderLabel)] = \"Inverser l'ordre des messages\",\n        [nameof(ReverseMessageOrderTooltip)] =\n            \"Exporter les messages en ordre chronologique inversé (les plus récents en premier)\",\n        [nameof(FormatMarkdownLabel)] = \"Formater le markdown\",\n        [nameof(FormatMarkdownTooltip)] =\n            \"Traiter le markdown, les mentions et autres tokens spéciaux\",\n        [nameof(DownloadAssetsLabel)] = \"Télécharger les ressources\",\n        [nameof(DownloadAssetsTooltip)] =\n            \"Télécharger les ressources référencées par l'export (avatars, fichiers joints, images intégrées, etc.)\",\n        [nameof(ReuseAssetsLabel)] = \"Réutiliser les ressources\",\n        [nameof(ReuseAssetsTooltip)] =\n            \"Réutiliser les ressources précédemment téléchargées pour éviter les requêtes redondantes\",\n        [nameof(AssetsDirPathLabel)] = \"Chemin du dossier des ressources\",\n        [nameof(AssetsDirPathTooltip)] =\n            \"Télécharger les ressources dans ce dossier. Si non spécifié, le chemin sera dérivé du chemin de sortie.\",\n        [nameof(AdvancedOptionsTooltip)] = \"Basculer les options avancées\",\n        [nameof(ExportButton)] = \"EXPORTER\",\n        // Common buttons\n        [nameof(CloseButton)] = \"FERMER\",\n        [nameof(CancelButton)] = \"ANNULER\",\n        // Dialog messages\n        [nameof(UkraineSupportTitle)] = \"Merci de soutenir l'Ukraine !\",\n        [nameof(UkraineSupportMessage)] = \"\"\"\n            Alors que la Russie mène une guerre génocidaire contre mon pays, je suis reconnaissant envers tous ceux qui continuent à soutenir l'Ukraine dans notre lutte pour la liberté.\n\n            Cliquez sur EN SAVOIR PLUS pour trouver des moyens d'aider.\n            \"\"\",\n        [nameof(LearnMoreButton)] = \"EN SAVOIR PLUS\",\n        [nameof(UnstableBuildTitle)] = \"Avertissement : version instable\",\n        [nameof(UnstableBuildMessage)] = \"\"\"\n            Vous utilisez une version de développement de {0}. Ces versions ne sont pas rigoureusement testées et peuvent contenir des bugs.\n\n            Les mises à jour automatiques sont désactivées pour les versions de développement.\n\n            Cliquez sur VOIR LES VERSIONS pour télécharger une version stable.\n            \"\"\",\n        [nameof(SeeReleasesButton)] = \"VOIR LES VERSIONS\",\n        [nameof(UpdateDownloadingMessage)] = \"Téléchargement de la mise à jour vers {0} v{1}...\",\n        [nameof(UpdateReadyMessage)] =\n            \"La mise à jour a été téléchargée et sera installée à la fermeture\",\n        [nameof(UpdateInstallNowButton)] = \"INSTALLER MAINTENANT\",\n        [nameof(UpdateFailedMessage)] = \"Échec de la mise à jour de l'application\",\n        [nameof(ErrorPullingGuildsTitle)] = \"Erreur lors du chargement des serveurs\",\n        [nameof(ErrorPullingChannelsTitle)] = \"Erreur lors du chargement des canaux\",\n        [nameof(ErrorExportingTitle)] = \"Erreur lors de l'exportation des canaux\",\n        [nameof(SuccessfulExportMessage)] = \"{0} canal(-aux) exporté(s) avec succès\",\n    };\n}\n"
  },
  {
    "path": "DiscordChatExporter.Gui/Localization/LocalizationManager.German.cs",
    "content": "using System.Collections.Generic;\n\nnamespace DiscordChatExporter.Gui.Localization;\n\npublic partial class LocalizationManager\n{\n    private static readonly IReadOnlyDictionary<string, string> GermanLocalization = new Dictionary<\n        string,\n        string\n    >\n    {\n        // Dashboard\n        [nameof(PullGuildsTooltip)] = \"Verfügbare Server und Kanäle laden (Enter)\",\n        [nameof(SettingsTooltip)] = \"Einstellungen\",\n        [nameof(LastMessageSentTooltip)] = \"Letzte Nachricht gesendet:\",\n        [nameof(TokenWatermark)] = \"Token\",\n        // Token instructions (personal account)\n        [nameof(TokenPersonalHeader)] = \"Token für Ihr persönliches Konto abrufen:\",\n        [nameof(TokenPersonalTosWarning)] =\n            \"*  Das Automatisieren von Benutzerkonten verstößt technisch gegen die AGB — **auf eigene Gefahr**!\",\n        [nameof(TokenPersonalInstructions)] = \"\"\"\n            1. Öffnen Sie Discord in Ihrem Webbrowser und melden Sie sich an\n            2. Öffnen Sie einen Server oder einen direkten Nachrichtenkanal\n            3. Drücken Sie **Ctrl+Shift+I**, um die Entwicklertools anzuzeigen\n            4. Navigieren Sie zum Reiter **Network**\n            5. Drücken Sie **Ctrl+R** zum Neuladen\n            6. Wechseln Sie zwischen Kanälen, um Netzwerkanfragen auszulösen\n            7. Suchen Sie nach einer Anfrage, die mit **messages** beginnt\n            8. Wählen Sie den Reiter **Headers** auf der rechten Seite\n            9. Scrollen Sie nach unten zum Abschnitt **Request Headers**\n            10. Kopieren Sie den Wert des Headers **authorization**\n            \"\"\",\n        // Token instructions (bot)\n        [nameof(TokenBotHeader)] = \"Token für Ihren Bot abrufen:\",\n        [nameof(TokenBotInstructions)] = \"\"\"\n            Der Token wird bei der Bot-Erstellung generiert. Falls er verloren gegangen ist, generieren Sie einen neuen:\n\n            1. Öffnen Sie Discord [Entwicklerportal](https://discord.com/developers/applications)\n            2. Öffnen Sie die Einstellungen Ihrer Anwendung\n            3. Navigieren Sie zum Abschnitt **Bot** auf der linken Seite\n            4. Klicken Sie unter **Token** auf **Reset Token**\n            5. Klicken Sie auf **Yes, do it!** und bestätigen Sie\n            *  Integrationen, die den alten Token verwenden, hören auf zu funktionieren, bis sie aktualisiert werden\n            *  Ihr Bot benötigt die aktivierte **Message Content Intent**, um Nachrichten zu lesen\n            \"\"\",\n        [nameof(TokenHelpText)] =\n            \"Bei Fragen oder Problemen lesen Sie die [Dokumentation](https://github.com/Tyrrrz/DiscordChatExporter/tree/prime/.docs)\",\n        // Settings\n        [nameof(SettingsTitle)] = \"Einstellungen\",\n        [nameof(ThemeLabel)] = \"Design\",\n        [nameof(ThemeTooltip)] = \"Bevorzugtes Oberflächendesign\",\n        [nameof(LanguageLabel)] = \"Sprache\",\n        [nameof(LanguageTooltip)] = \"Bevorzugte Sprache der Benutzeroberfläche\",\n        [nameof(AutoUpdateLabel)] = \"Automatische Updates\",\n        [nameof(AutoUpdateTooltip)] = \"Automatische Updates bei jedem Start durchführen\",\n        [nameof(PersistTokenLabel)] = \"Token speichern\",\n        [nameof(PersistTokenTooltip)] = \"\"\"\n            Den zuletzt verwendeten Token in einer Datei speichern, damit er zwischen Sitzungen erhalten bleibt.\n            **Warnung**: Der Token wird mit Verschlüsselung gespeichert, kann aber dennoch von einem Angreifer mit Zugriff auf Ihr System wiederhergestellt werden.\n            \"\"\",\n        [nameof(RateLimitPreferenceLabel)] = \"Ratenlimit-Einstellung\",\n        [nameof(RateLimitPreferenceTooltip)] =\n            \"Ob empfohlene Ratenlimits eingehalten werden sollen. Wenn deaktiviert, werden nur harte Ratenlimits (d. h. 429-Antworten) eingehalten.\",\n        [nameof(ShowThreadsLabel)] = \"Threads anzeigen\",\n        [nameof(ShowThreadsTooltip)] = \"Welche Thread-Typen in der Kanalliste angezeigt werden\",\n        [nameof(LocaleLabel)] = \"Gebietsschema\",\n        [nameof(LocaleTooltip)] = \"Gebietsschema für die Formatierung von Daten und Zahlen\",\n        [nameof(NormalizeToUtcLabel)] = \"Auf UTC normalisieren\",\n        [nameof(NormalizeToUtcTooltip)] = \"Alle Zeitstempel auf UTC+0 normalisieren\",\n        [nameof(ParallelLimitLabel)] = \"Paralleles Limit\",\n        [nameof(ParallelLimitTooltip)] = \"Wie viele Kanäle gleichzeitig exportiert werden können\",\n        // Export Setup\n        [nameof(ChannelsSelectedText)] = \"Kanäle ausgewählt\",\n        [nameof(OutputPathLabel)] = \"Ausgabepfad\",\n        [nameof(OutputPathTooltip)] = \"\"\"\n            Ausgabedatei- oder Verzeichnispfad.\n\n            Wenn ein Verzeichnis angegeben wird, werden Dateinamen automatisch basierend auf den Kanalnamen und Exportparametern generiert.\n\n            Verzeichnispfade müssen mit einem Schrägstrich enden, um Mehrdeutigkeiten zu vermeiden.\n\n            Verfügbare Vorlagen-Token:\n            **%g** — Server-ID\n            **%G** — Servername\n            **%t** — Kategorie-ID\n            **%T** — Kategoriename\n            **%c** — Kanal-ID\n            **%C** — Kanalname\n            **%p** — Kanalposition\n            **%P** — Kategorieposition\n            **%a** — Datum ab\n            **%b** — Datum bis\n            **%d** — aktuelles Datum\n            \"\"\",\n        [nameof(FormatLabel)] = \"Format\",\n        [nameof(FormatTooltip)] = \"Exportformat\",\n        [nameof(AfterDateLabel)] = \"Nach (Datum)\",\n        [nameof(AfterDateTooltip)] =\n            \"Nur Nachrichten einschließen, die nach diesem Datum gesendet wurden\",\n        [nameof(BeforeDateLabel)] = \"Vor (Datum)\",\n        [nameof(BeforeDateTooltip)] =\n            \"Nur Nachrichten einschließen, die vor diesem Datum gesendet wurden\",\n        [nameof(AfterTimeLabel)] = \"Nach (Uhrzeit)\",\n        [nameof(AfterTimeTooltip)] =\n            \"Nur Nachrichten einschließen, die nach dieser Uhrzeit gesendet wurden\",\n        [nameof(BeforeTimeLabel)] = \"Vor (Uhrzeit)\",\n        [nameof(BeforeTimeTooltip)] =\n            \"Nur Nachrichten einschließen, die vor dieser Uhrzeit gesendet wurden\",\n        [nameof(PartitionLimitLabel)] = \"Partitionslimit\",\n        [nameof(PartitionLimitTooltip)] =\n            \"Die Ausgabe in Partitionen aufteilen, jede begrenzt auf die angegebene Anzahl von Nachrichten (z. B. '100') oder Dateigröße (z. B. '10mb')\",\n        [nameof(MessageFilterLabel)] = \"Nachrichtenfilter\",\n        [nameof(MessageFilterTooltip)] =\n            \"Nur Nachrichten einschließen, die diesem Filter entsprechen (z. B. 'from:foo#1234' oder 'has:image'). Weitere Informationen finden Sie in der Dokumentation.\",\n        [nameof(ReverseMessageOrderLabel)] = \"Nachrichtenreihenfolge umkehren\",\n        [nameof(ReverseMessageOrderTooltip)] =\n            \"Nachrichten in umgekehrter chronologischer Reihenfolge exportieren (neueste zuerst)\",\n        [nameof(FormatMarkdownLabel)] = \"Markdown formatieren\",\n        [nameof(FormatMarkdownTooltip)] =\n            \"Markdown, Erwähnungen und andere spezielle Token verarbeiten\",\n        [nameof(DownloadAssetsLabel)] = \"Assets herunterladen\",\n        [nameof(DownloadAssetsTooltip)] =\n            \"Vom Export referenzierte Assets herunterladen (Benutzeravatare, angehängte Dateien, eingebettete Bilder usw.)\",\n        [nameof(ReuseAssetsLabel)] = \"Assets wiederverwenden\",\n        [nameof(ReuseAssetsTooltip)] =\n            \"Zuvor heruntergeladene Assets wiederverwenden, um redundante Anfragen zu vermeiden\",\n        [nameof(AssetsDirPathLabel)] = \"Asset-Verzeichnispfad\",\n        [nameof(AssetsDirPathTooltip)] =\n            \"Assets in dieses Verzeichnis herunterladen. Wenn nicht angegeben, wird der Asset-Verzeichnispfad vom Ausgabepfad abgeleitet.\",\n        [nameof(AdvancedOptionsTooltip)] = \"Erweiterte Optionen umschalten\",\n        [nameof(ExportButton)] = \"EXPORTIEREN\",\n        // Common buttons\n        [nameof(CloseButton)] = \"SCHLIESSEN\",\n        [nameof(CancelButton)] = \"ABBRECHEN\",\n        // Dialog messages\n        [nameof(UkraineSupportTitle)] = \"Danke für Ihre Unterstützung der Ukraine!\",\n        [nameof(UkraineSupportMessage)] = \"\"\"\n            Während Russland einen Vernichtungskrieg gegen mein Land führt, bin ich jedem dankbar, der weiterhin an der Seite der Ukraine in unserem Kampf für die Freiheit steht.\n\n            Klicken Sie auf MEHR ERFAHREN, um Möglichkeiten der Unterstützung zu finden.\n            \"\"\",\n        [nameof(LearnMoreButton)] = \"MEHR ERFAHREN\",\n        [nameof(UnstableBuildTitle)] = \"Warnung: Instabile Version\",\n        [nameof(UnstableBuildMessage)] = \"\"\"\n            Sie verwenden eine Entwicklungsversion von {0}. Diese Versionen wurden nicht gründlich getestet und können Fehler enthalten.\n\n            Automatische Updates sind für Entwicklungsversionen deaktiviert.\n\n            Klicken Sie auf RELEASES ANZEIGEN, wenn Sie stattdessen eine stabile Version herunterladen möchten.\n            \"\"\",\n        [nameof(SeeReleasesButton)] = \"RELEASES ANZEIGEN\",\n        [nameof(UpdateDownloadingMessage)] = \"Update auf {0} v{1} wird heruntergeladen...\",\n        [nameof(UpdateReadyMessage)] =\n            \"Update wurde heruntergeladen und wird beim Beenden installiert\",\n        [nameof(UpdateInstallNowButton)] = \"JETZT INSTALLIEREN\",\n        [nameof(UpdateFailedMessage)] = \"Anwendungsupdate konnte nicht durchgeführt werden\",\n        [nameof(ErrorPullingGuildsTitle)] = \"Fehler beim Laden der Server\",\n        [nameof(ErrorPullingChannelsTitle)] = \"Fehler beim Laden der Kanäle\",\n        [nameof(ErrorExportingTitle)] = \"Fehler beim Exportieren der Kanäle\",\n        [nameof(SuccessfulExportMessage)] = \"{0} Kanal/-äle erfolgreich exportiert\",\n    };\n}\n"
  },
  {
    "path": "DiscordChatExporter.Gui/Localization/LocalizationManager.Spanish.cs",
    "content": "using System.Collections.Generic;\n\nnamespace DiscordChatExporter.Gui.Localization;\n\npublic partial class LocalizationManager\n{\n    private static readonly IReadOnlyDictionary<string, string> SpanishLocalization =\n        new Dictionary<string, string>\n        {\n            // Dashboard\n            [nameof(PullGuildsTooltip)] = \"Cargar servidores y canales disponibles (Enter)\",\n            [nameof(SettingsTooltip)] = \"Ajustes\",\n            [nameof(LastMessageSentTooltip)] = \"Último mensaje enviado:\",\n            [nameof(TokenWatermark)] = \"Token\",\n            // Token instructions (personal account)\n            [nameof(TokenPersonalHeader)] = \"Cómo obtener el token para tu cuenta personal:\",\n            [nameof(TokenPersonalTosWarning)] =\n                \"*  Automatizar cuentas de usuario técnicamente va en contra de los ToS — **bajo tu propio riesgo**!\",\n            [nameof(TokenPersonalInstructions)] = \"\"\"\n                1. Abre Discord en tu navegador web e inicia sesión\n                2. Abre cualquier servidor o canal de mensaje directo\n                3. Presiona **Ctrl+Shift+I** para mostrar las herramientas de desarrollo\n                4. Navega a la pestaña **Network**\n                5. Presiona **Ctrl+R** para recargar\n                6. Cambia entre canales para activar solicitudes de red\n                7. Busca una solicitud que comience con **messages**\n                8. Selecciona la pestaña **Headers** a la derecha\n                9. Desplázate hasta la sección **Request Headers**\n                10. Copia el valor del encabezado **authorization**\n                \"\"\",\n            // Token instructions (bot)\n            [nameof(TokenBotHeader)] = \"Cómo obtener el token para tu bot:\",\n            [nameof(TokenBotInstructions)] = \"\"\"\n                El token se genera al crear el bot. Si lo perdiste, genera uno nuevo:\n\n                1. Abre Discord [portal de desarrolladores](https://discord.com/developers/applications)\n                2. Abre la configuración de tu aplicación\n                3. Navega a la sección **Bot** en el lado izquierdo\n                4. En **Token**, haz clic en **Reset Token**\n                5. Haz clic en **Yes, do it!** y autentica para confirmar\n                *  Las integraciones que usen el token anterior dejarán de funcionar hasta que se actualicen\n                *  Tu bot necesita tener habilitado **Message Content Intent** para leer mensajes\n                \"\"\",\n            [nameof(TokenHelpText)] =\n                \"Si tienes preguntas o problemas, consulta la [documentación](https://github.com/Tyrrrz/DiscordChatExporter/tree/prime/.docs)\",\n            // Settings\n            [nameof(SettingsTitle)] = \"Ajustes\",\n            [nameof(ThemeLabel)] = \"Tema\",\n            [nameof(ThemeTooltip)] = \"Tema de interfaz preferido\",\n            [nameof(LanguageLabel)] = \"Idioma\",\n            [nameof(LanguageTooltip)] = \"Idioma de interfaz preferido\",\n            [nameof(AutoUpdateLabel)] = \"Actualización automática\",\n            [nameof(AutoUpdateTooltip)] = \"Realizar actualizaciones automáticas en cada inicio\",\n            [nameof(PersistTokenLabel)] = \"Guardar token\",\n            [nameof(PersistTokenTooltip)] = \"\"\"\n                Guardar el último token utilizado en un archivo para conservarlo entre sesiones.\n                **Advertencia**: aunque el token se almacena con cifrado, aún puede ser recuperado por un atacante con acceso a tu sistema.\n                \"\"\",\n            [nameof(RateLimitPreferenceLabel)] = \"Preferencia de límite de velocidad\",\n            [nameof(RateLimitPreferenceTooltip)] =\n                \"Si se deben respetar los límites de velocidad recomendados. Si está desactivado, solo se respetarán los límites estrictos (respuestas 429).\",\n            [nameof(ShowThreadsLabel)] = \"Mostrar hilos\",\n            [nameof(ShowThreadsTooltip)] = \"Qué tipos de hilos mostrar en la lista de canales\",\n            [nameof(LocaleLabel)] = \"Configuración regional\",\n            [nameof(LocaleTooltip)] = \"Configuración regional para el formato de fechas y números\",\n            [nameof(NormalizeToUtcLabel)] = \"Normalizar a UTC\",\n            [nameof(NormalizeToUtcTooltip)] = \"Normalizar todas las marcas de tiempo a UTC+0\",\n            [nameof(ParallelLimitLabel)] = \"Límite paralelo\",\n            [nameof(ParallelLimitTooltip)] = \"Cuántos canales pueden exportarse al mismo tiempo\",\n            // Export Setup\n            [nameof(ChannelsSelectedText)] = \"canales seleccionados\",\n            [nameof(OutputPathLabel)] = \"Ruta de salida\",\n            [nameof(OutputPathTooltip)] = \"\"\"\n                Ruta del archivo o directorio de salida.\n\n                Si se especifica un directorio, los nombres de archivo se generarán automáticamente según los nombres de los canales y los parámetros de exportación.\n\n                Las rutas de directorio deben terminar con una barra diagonal para evitar ambigüedades.\n\n                Tokens de plantilla disponibles:\n                **%g** — ID del servidor\n                **%G** — nombre del servidor\n                **%t** — ID de categoría\n                **%T** — nombre de categoría\n                **%c** — ID del canal\n                **%C** — nombre del canal\n                **%p** — posición del canal\n                **%P** — posición de la categoría\n                **%a** — fecha desde\n                **%b** — fecha hasta\n                **%d** — fecha actual\n                \"\"\",\n            [nameof(FormatLabel)] = \"Formato\",\n            [nameof(FormatTooltip)] = \"Formato de exportación\",\n            [nameof(AfterDateLabel)] = \"Después (fecha)\",\n            [nameof(AfterDateTooltip)] = \"Solo incluir mensajes enviados después de esta fecha\",\n            [nameof(BeforeDateLabel)] = \"Antes (fecha)\",\n            [nameof(BeforeDateTooltip)] = \"Solo incluir mensajes enviados antes de esta fecha\",\n            [nameof(AfterTimeLabel)] = \"Después (hora)\",\n            [nameof(AfterTimeTooltip)] = \"Solo incluir mensajes enviados después de esta hora\",\n            [nameof(BeforeTimeLabel)] = \"Antes (hora)\",\n            [nameof(BeforeTimeTooltip)] = \"Solo incluir mensajes enviados antes de esta hora\",\n            [nameof(PartitionLimitLabel)] = \"Límite de partición\",\n            [nameof(PartitionLimitTooltip)] =\n                \"Dividir la salida en particiones, cada una limitada al número de mensajes especificado (p. ej. '100') o tamaño de archivo (p. ej. '10mb')\",\n            [nameof(MessageFilterLabel)] = \"Filtro de mensajes\",\n            [nameof(MessageFilterTooltip)] =\n                \"Solo incluir mensajes que satisfagan este filtro (p. ej. 'from:foo#1234' o 'has:image'). Consulte la documentación para más información.\",\n            [nameof(ReverseMessageOrderLabel)] = \"Invertir orden de mensajes\",\n            [nameof(ReverseMessageOrderTooltip)] =\n                \"Exportar mensajes en orden cronológico inverso (los más recientes primero)\",\n            [nameof(FormatMarkdownLabel)] = \"Formatear markdown\",\n            [nameof(FormatMarkdownTooltip)] =\n                \"Procesar markdown, menciones y otros tokens especiales\",\n            [nameof(DownloadAssetsLabel)] = \"Descargar recursos\",\n            [nameof(DownloadAssetsTooltip)] =\n                \"Descargar los recursos referenciados por la exportación (avatares, archivos adjuntos, imágenes incrustadas, etc.)\",\n            [nameof(ReuseAssetsLabel)] = \"Reutilizar recursos\",\n            [nameof(ReuseAssetsTooltip)] =\n                \"Reutilizar recursos previamente descargados para evitar solicitudes redundantes\",\n            [nameof(AssetsDirPathLabel)] = \"Ruta del directorio de recursos\",\n            [nameof(AssetsDirPathTooltip)] =\n                \"Descargar recursos en este directorio. Si no se especifica, la ruta se derivará de la ruta de salida.\",\n            [nameof(AdvancedOptionsTooltip)] = \"Alternar opciones avanzadas\",\n            [nameof(ExportButton)] = \"EXPORTAR\",\n            // Common buttons\n            [nameof(CloseButton)] = \"CERRAR\",\n            [nameof(CancelButton)] = \"CANCELAR\",\n            // Dialog messages\n            [nameof(UkraineSupportTitle)] = \"¡Gracias por apoyar a Ucrania!\",\n            [nameof(UkraineSupportMessage)] = \"\"\"\n                Mientras Rusia libra una guerra genocida contra mi país, estoy agradecido con todos los que continúan apoyando a Ucrania en nuestra lucha por la libertad.\n\n                Haga clic en MÁS INFORMACIÓN para encontrar formas de ayudar.\n                \"\"\",\n            [nameof(LearnMoreButton)] = \"MÁS INFORMACIÓN\",\n            [nameof(UnstableBuildTitle)] = \"Advertencia de versión inestable\",\n            [nameof(UnstableBuildMessage)] = \"\"\"\n                Está usando una versión de desarrollo de {0}. Estas versiones no han sido probadas exhaustivamente y pueden contener errores.\n\n                Las actualizaciones automáticas están desactivadas para las versiones de desarrollo.\n\n                Haga clic en VER VERSIONES si desea descargar una versión estable.\n                \"\"\",\n            [nameof(SeeReleasesButton)] = \"VER VERSIONES\",\n            [nameof(UpdateDownloadingMessage)] = \"Descargando actualización a {0} v{1}...\",\n            [nameof(UpdateReadyMessage)] =\n                \"La actualización se ha descargado y se instalará al salir\",\n            [nameof(UpdateInstallNowButton)] = \"INSTALAR AHORA\",\n            [nameof(UpdateFailedMessage)] = \"Error al realizar la actualización de la aplicación\",\n            [nameof(ErrorPullingGuildsTitle)] = \"Error al cargar servidores\",\n            [nameof(ErrorPullingChannelsTitle)] = \"Error al cargar canales\",\n            [nameof(ErrorExportingTitle)] = \"Error al exportar canal(es)\",\n            [nameof(SuccessfulExportMessage)] = \"{0} canal(es) exportado(s) con éxito\",\n        };\n}\n"
  },
  {
    "path": "DiscordChatExporter.Gui/Localization/LocalizationManager.Ukrainian.cs",
    "content": "using System.Collections.Generic;\n\nnamespace DiscordChatExporter.Gui.Localization;\n\npublic partial class LocalizationManager\n{\n    private static readonly IReadOnlyDictionary<string, string> UkrainianLocalization =\n        new Dictionary<string, string>\n        {\n            // Dashboard\n            [nameof(PullGuildsTooltip)] = \"Завантажити доступні сервери та канали (Enter)\",\n            [nameof(SettingsTooltip)] = \"Налаштування\",\n            [nameof(LastMessageSentTooltip)] = \"Останнє повідомлення:\",\n            [nameof(TokenWatermark)] = \"Токен\",\n            // Token instructions (personal account)\n            [nameof(TokenPersonalHeader)] = \"Як отримати токен для персонального акаунту:\",\n            [nameof(TokenPersonalTosWarning)] =\n                \"*  Автоматизація облікових записів технічно порушує Умови обслуговування — **на власний ризик**!\",\n            [nameof(TokenPersonalInstructions)] = \"\"\"\n                1. Відкрийте Discord у вашому веб-браузері та увійдіть\n                2. Відкрийте будь-який сервер або канал особистих повідомлень\n                3. Натисніть **Ctrl+Shift+I**, щоб відкрити інструменти розробника\n                4. Перейдіть на вкладку **Network**\n                5. Натисніть **Ctrl+R** для перезавантаження\n                6. Перемикайтеся між каналами, щоб викликати мережеві запити\n                7. Знайдіть запит, що починається з **messages**\n                8. Виберіть вкладку **Headers** праворуч\n                9. Прокрутіть до розділу **Request Headers**\n                10. Скопіюйте значення заголовка **authorization**\n                \"\"\",\n            // Token instructions (bot)\n            [nameof(TokenBotHeader)] = \"Як отримати токен для бота:\",\n            [nameof(TokenBotInstructions)] = \"\"\"\n                Токен генерується під час створення бота. Якщо ви його втратили, згенеруйте новий:\n\n                1. Відкрийте Discord [портал розробника](https://discord.com/developers/applications)\n                2. Відкрийте налаштування вашого застосунку\n                3. Перейдіть до розділу **Bot** ліворуч\n                4. В розділі **Token** натисніть **Reset Token**\n                5. Натисніть **Yes, do it!** та підтвердьте\n                *  Інтеграції, що використовують попередній токен, перестануть працювати\n                *  Ваш бот повинен мати включений **Message Content Intent** для читання повідомлень\n                \"\"\",\n            [nameof(TokenHelpText)] =\n                \"Якщо у вас є запитання або проблеми, зверніться до [документації](https://github.com/Tyrrrz/DiscordChatExporter/tree/prime/.docs)\",\n            // Settings\n            [nameof(SettingsTitle)] = \"Налаштування\",\n            [nameof(ThemeLabel)] = \"Тема\",\n            [nameof(ThemeTooltip)] = \"Бажана тема інтерфейсу\",\n            [nameof(LanguageLabel)] = \"Мова\",\n            [nameof(LanguageTooltip)] = \"Бажана мова інтерфейсу\",\n            [nameof(AutoUpdateLabel)] = \"Авто-оновлення\",\n            [nameof(AutoUpdateTooltip)] = \"Виконувати автоматичні оновлення при кожному запуску\",\n            [nameof(PersistTokenLabel)] = \"Зберігати токен\",\n            [nameof(PersistTokenTooltip)] = \"\"\"\n                Зберігати останній використаний токен у файлі для збереження між сеансами.\n                **Увага**: хоча токен зберігається із шифруванням, він може бути відновлений зловмисником, який має доступ до вашої системи.\n                \"\"\",\n            [nameof(RateLimitPreferenceLabel)] = \"Ліміт запитів\",\n            [nameof(RateLimitPreferenceTooltip)] =\n                \"Чи дотримуватись рекомендованих лімітів запитів. Якщо вимкнено, будуть дотримуватись лише жорсткі ліміти (тобто відповіді 429).\",\n            [nameof(ShowThreadsLabel)] = \"Показувати гілки\",\n            [nameof(ShowThreadsTooltip)] = \"Які типи гілок показувати у списку каналів\",\n            [nameof(LocaleLabel)] = \"Локаль\",\n            [nameof(LocaleTooltip)] = \"Локаль для форматування дат та чисел\",\n            [nameof(NormalizeToUtcLabel)] = \"Нормалізувати до UTC\",\n            [nameof(NormalizeToUtcTooltip)] = \"Нормалізувати всі часові мітки до UTC+0\",\n            [nameof(ParallelLimitLabel)] = \"Ліміт паралелізації\",\n            [nameof(ParallelLimitTooltip)] = \"Скільки каналів може експортуватись одночасно\",\n            // Export Setup\n            [nameof(ChannelsSelectedText)] = \"каналів вибрано\",\n            [nameof(OutputPathLabel)] = \"Шлях збереження\",\n            [nameof(OutputPathTooltip)] = \"\"\"\n                Шлях до файлу або директорії виводу.\n\n                Якщо вказано директорію, імена файлів генеруватимуться автоматично на основі назв каналів та параметрів експорту.\n\n                Шляхи до директорій повинні закінчуватись слешем для уникнення неоднозначності.\n\n                Доступні шаблонні токени:\n                **%g** — ID сервера\n                **%G** — назва сервера\n                **%t** — ID категорії\n                **%T** — назва категорії\n                **%c** — ID каналу\n                **%C** — назва каналу\n                **%p** — позиція каналу\n                **%P** — позиція категорії\n                **%a** — дата після\n                **%b** — дата до\n                **%d** — поточна дата\n                \"\"\",\n            [nameof(FormatLabel)] = \"Формат\",\n            [nameof(FormatTooltip)] = \"Формат експорту\",\n            [nameof(AfterDateLabel)] = \"Після (дата)\",\n            [nameof(AfterDateTooltip)] = \"Включати лише повідомлення, надіслані після цієї дати\",\n            [nameof(BeforeDateLabel)] = \"До (дата)\",\n            [nameof(BeforeDateTooltip)] = \"Включати лише повідомлення, надіслані до цієї дати\",\n            [nameof(AfterTimeLabel)] = \"Після (час)\",\n            [nameof(AfterTimeTooltip)] = \"Включати лише повідомлення, надіслані після цього часу\",\n            [nameof(BeforeTimeLabel)] = \"До (час)\",\n            [nameof(BeforeTimeTooltip)] = \"Включати лише повідомлення, надіслані до цього часу\",\n            [nameof(PartitionLimitLabel)] = \"Розділяти експорт\",\n            [nameof(PartitionLimitTooltip)] =\n                \"Розділити вивід на частини, кожна обмежена вказаною кількістю повідомлень (напр. '100') або розміром файлу (напр. '10mb')\",\n            [nameof(MessageFilterLabel)] = \"Фільтр повідомлень\",\n            [nameof(MessageFilterTooltip)] =\n                \"Включати лише повідомлення, що відповідають цьому фільтру (напр. 'from:foo#1234' або 'has:image'). Дивіться документацію для більш детальної інформації.\",\n            [nameof(ReverseMessageOrderLabel)] = \"Зворотній порядок повідомлень\",\n            [nameof(ReverseMessageOrderTooltip)] =\n                \"Експортувати повідомлення у зворотному хронологічному порядку (найновіші спочатку)\",\n            [nameof(FormatMarkdownLabel)] = \"Форматувати markdown\",\n            [nameof(FormatMarkdownTooltip)] =\n                \"Обробляти markdown, згадки та інші спеціальні токени\",\n            [nameof(DownloadAssetsLabel)] = \"Завантажувати ресурси\",\n            [nameof(DownloadAssetsTooltip)] =\n                \"Завантажувати ресурси, на які посилається експорт (аватари, вкладені файли, вбудовані зображення тощо)\",\n            [nameof(ReuseAssetsLabel)] = \"Повторно використовувати ресурси\",\n            [nameof(ReuseAssetsTooltip)] =\n                \"Повторно використовувати раніше завантажені ресурси, щоб уникнути зайвих запитів\",\n            [nameof(AssetsDirPathLabel)] = \"Шлях до директорії ресурсів\",\n            [nameof(AssetsDirPathTooltip)] =\n                \"Завантажувати ресурси до цієї директорії. Якщо не вказано, шлях до директорії ресурсів буде визначено з шляху збереження.\",\n            [nameof(AdvancedOptionsTooltip)] = \"Перемкнути розширені параметри\",\n            [nameof(ExportButton)] = \"ЕКСПОРТУВАТИ\",\n            // Common buttons\n            [nameof(CloseButton)] = \"ЗАКРИТИ\",\n            [nameof(CancelButton)] = \"СКАСУВАТИ\",\n            // Dialog messages\n            [nameof(UkraineSupportTitle)] = \"Дякуємо за підтримку України!\",\n            [nameof(UkraineSupportMessage)] = \"\"\"\n                Поки Росія веде геноцидну війну проти моєї країни, я вдячний кожному, хто продовжує підтримувати Україну у нашій боротьбі за свободу.\n\n                Натисніть ДІЗНАТИСЬ БІЛЬШЕ, щоб знайти способи допомогти.\n                \"\"\",\n            [nameof(LearnMoreButton)] = \"ДІЗНАТИСЬ БІЛЬШЕ\",\n            [nameof(UnstableBuildTitle)] = \"Попередження про нестабільну збірку\",\n            [nameof(UnstableBuildMessage)] = \"\"\"\n                Ви використовуєте збірку розробки {0}. Ці збірки не пройшли ретельного тестування та можуть містити помилки.\n\n                Авто-оновлення вимкнено для збірок розробки.\n\n                Натисніть ПЕРЕГЛЯНУТИ РЕЛІЗИ, щоб завантажити стабільний реліз.\n                \"\"\",\n            [nameof(SeeReleasesButton)] = \"ПЕРЕГЛЯНУТИ РЕЛІЗИ\",\n            [nameof(UpdateDownloadingMessage)] = \"Завантаження оновлення {0} v{1}...\",\n            [nameof(UpdateReadyMessage)] = \"Оновлення завантажено та буде встановлено після виходу\",\n            [nameof(UpdateInstallNowButton)] = \"ВСТАНОВИТИ ЗАРАЗ\",\n            [nameof(UpdateFailedMessage)] = \"Не вдалося виконати оновлення програми\",\n            [nameof(ErrorPullingGuildsTitle)] = \"Помилка завантаження серверів\",\n            [nameof(ErrorPullingChannelsTitle)] = \"Помилка завантаження каналів\",\n            [nameof(ErrorExportingTitle)] = \"Помилка експорту каналу(-ів)\",\n            [nameof(SuccessfulExportMessage)] = \"Успішно експортовано {0} канал(-ів)\",\n        };\n}\n"
  },
  {
    "path": "DiscordChatExporter.Gui/Localization/LocalizationManager.cs",
    "content": "using System;\nusing System.Globalization;\nusing System.Runtime.CompilerServices;\nusing CommunityToolkit.Mvvm.ComponentModel;\nusing DiscordChatExporter.Gui.Services;\nusing DiscordChatExporter.Gui.Utils;\nusing DiscordChatExporter.Gui.Utils.Extensions;\n\nnamespace DiscordChatExporter.Gui.Localization;\n\npublic partial class LocalizationManager : ObservableObject, IDisposable\n{\n    private readonly DisposableCollector _eventRoot = new();\n\n    public LocalizationManager(SettingsService settingsService)\n    {\n        _eventRoot.Add(\n            settingsService.WatchProperty(\n                o => o.Language,\n                () => Language = settingsService.Language,\n                true\n            )\n        );\n\n        _eventRoot.Add(\n            this.WatchProperty(\n                o => o.Language,\n                () =>\n                {\n                    foreach (var propertyName in EnglishLocalization.Keys)\n                        OnPropertyChanged(propertyName);\n                }\n            )\n        );\n    }\n\n    [ObservableProperty]\n    public partial Language Language { get; set; } = Language.System;\n\n    private string Get([CallerMemberName] string? key = null)\n    {\n        if (string.IsNullOrWhiteSpace(key))\n            return string.Empty;\n\n        var localization = Language switch\n        {\n            Language.System =>\n                CultureInfo.CurrentUICulture.ThreeLetterISOLanguageName.ToLowerInvariant() switch\n                {\n                    \"ukr\" => UkrainianLocalization,\n                    \"deu\" => GermanLocalization,\n                    \"fra\" => FrenchLocalization,\n                    \"spa\" => SpanishLocalization,\n                    _ => EnglishLocalization,\n                },\n            Language.Ukrainian => UkrainianLocalization,\n            Language.German => GermanLocalization,\n            Language.French => FrenchLocalization,\n            Language.Spanish => SpanishLocalization,\n            _ => EnglishLocalization,\n        };\n\n        if (\n            localization.TryGetValue(key, out var value)\n            // English is used as a fallback\n            || EnglishLocalization.TryGetValue(key, out value)\n        )\n        {\n            return value;\n        }\n\n        return $\"Missing localization for '{key}'\";\n    }\n\n    public void Dispose() => _eventRoot.Dispose();\n}\n\npublic partial class LocalizationManager\n{\n    // ---- Dashboard ----\n\n    public string PullGuildsTooltip => Get();\n    public string SettingsTooltip => Get();\n    public string LastMessageSentTooltip => Get();\n    public string TokenWatermark => Get();\n\n    // Token instructions (personal account)\n    public string TokenPersonalHeader => Get();\n    public string TokenPersonalTosWarning => Get();\n    public string TokenPersonalInstructions => Get();\n\n    // Token instructions (bot)\n    public string TokenBotHeader => Get();\n    public string TokenBotInstructions => Get();\n    public string TokenHelpText => Get();\n\n    // ---- Settings ----\n\n    public string SettingsTitle => Get();\n    public string ThemeLabel => Get();\n    public string ThemeTooltip => Get();\n    public string LanguageLabel => Get();\n    public string LanguageTooltip => Get();\n    public string AutoUpdateLabel => Get();\n    public string AutoUpdateTooltip => Get();\n    public string PersistTokenLabel => Get();\n    public string PersistTokenTooltip => Get();\n    public string RateLimitPreferenceLabel => Get();\n    public string RateLimitPreferenceTooltip => Get();\n    public string ShowThreadsLabel => Get();\n    public string ShowThreadsTooltip => Get();\n    public string LocaleLabel => Get();\n    public string LocaleTooltip => Get();\n    public string NormalizeToUtcLabel => Get();\n    public string NormalizeToUtcTooltip => Get();\n    public string ParallelLimitLabel => Get();\n    public string ParallelLimitTooltip => Get();\n\n    // ---- Export Setup ----\n\n    public string ChannelsSelectedText => Get();\n    public string OutputPathLabel => Get();\n    public string OutputPathTooltip => Get();\n    public string FormatLabel => Get();\n    public string FormatTooltip => Get();\n    public string AfterDateLabel => Get();\n    public string AfterDateTooltip => Get();\n    public string BeforeDateLabel => Get();\n    public string BeforeDateTooltip => Get();\n    public string AfterTimeLabel => Get();\n    public string AfterTimeTooltip => Get();\n    public string BeforeTimeLabel => Get();\n    public string BeforeTimeTooltip => Get();\n    public string PartitionLimitLabel => Get();\n    public string PartitionLimitTooltip => Get();\n    public string MessageFilterLabel => Get();\n    public string MessageFilterTooltip => Get();\n    public string ReverseMessageOrderLabel => Get();\n    public string ReverseMessageOrderTooltip => Get();\n    public string FormatMarkdownLabel => Get();\n    public string FormatMarkdownTooltip => Get();\n    public string DownloadAssetsLabel => Get();\n    public string DownloadAssetsTooltip => Get();\n    public string ReuseAssetsLabel => Get();\n    public string ReuseAssetsTooltip => Get();\n    public string AssetsDirPathLabel => Get();\n    public string AssetsDirPathTooltip => Get();\n    public string AdvancedOptionsTooltip => Get();\n    public string ExportButton => Get();\n\n    // ---- Common buttons ----\n\n    public string CloseButton => Get();\n    public string CancelButton => Get();\n\n    // ---- Dialog messages ----\n\n    public string UkraineSupportTitle => Get();\n    public string UkraineSupportMessage => Get();\n    public string LearnMoreButton => Get();\n    public string UnstableBuildTitle => Get();\n    public string UnstableBuildMessage => Get();\n    public string SeeReleasesButton => Get();\n    public string UpdateDownloadingMessage => Get();\n    public string UpdateReadyMessage => Get();\n    public string UpdateInstallNowButton => Get();\n    public string UpdateFailedMessage => Get();\n    public string ErrorPullingGuildsTitle => Get();\n    public string ErrorPullingChannelsTitle => Get();\n    public string ErrorExportingTitle => Get();\n    public string SuccessfulExportMessage => Get();\n}\n"
  },
  {
    "path": "DiscordChatExporter.Gui/Models/ThreadInclusionMode.cs",
    "content": "﻿namespace DiscordChatExporter.Gui.Models;\n\npublic enum ThreadInclusionMode\n{\n    None,\n    Active,\n    All,\n}\n"
  },
  {
    "path": "DiscordChatExporter.Gui/Program.cs",
    "content": "﻿using System;\nusing System.Reflection;\nusing Avalonia;\nusing DiscordChatExporter.Gui.Utils;\n\nnamespace DiscordChatExporter.Gui;\n\npublic static class Program\n{\n    private static Assembly Assembly { get; } = Assembly.GetExecutingAssembly();\n\n    public static string Name { get; } = Assembly.GetName().Name ?? \"DiscordChatExporter\";\n\n    public static Version Version { get; } = Assembly.GetName().Version ?? new Version(0, 0, 0);\n\n    public static string VersionString { get; } = Version.ToString(3);\n\n    public static bool IsDevelopmentBuild { get; } = Version.Major is <= 0 or >= 999;\n\n    public static string ProjectUrl { get; } = \"https://github.com/Tyrrrz/DiscordChatExporter\";\n\n    public static string ProjectReleasesUrl { get; } = $\"{ProjectUrl}/releases\";\n\n    public static string ProjectDocumentationUrl { get; } = ProjectUrl + \"/tree/prime/.docs\";\n\n    public static AppBuilder BuildAvaloniaApp() =>\n        AppBuilder.Configure<App>().UsePlatformDetect().LogToTrace();\n\n    [STAThread]\n    public static int Main(string[] args)\n    {\n        // Build and run the app\n        var builder = BuildAvaloniaApp();\n\n        try\n        {\n            return builder.StartWithClassicDesktopLifetime(args);\n        }\n        catch (Exception ex)\n        {\n            if (OperatingSystem.IsWindows())\n                _ = NativeMethods.Windows.MessageBox(0, ex.ToString(), \"Fatal Error\", 0x10);\n\n            throw;\n        }\n        finally\n        {\n            // Clean up after application shutdown\n            if (builder.Instance is IDisposable disposableApp)\n                disposableApp.Dispose();\n        }\n    }\n}\n"
  },
  {
    "path": "DiscordChatExporter.Gui/Publish-MacOSBundle.ps1",
    "content": "param(\n    [Parameter(Mandatory=$true)]\n    [string]$PublishDirPath,\n\n    [Parameter(Mandatory=$true)]\n    [string]$IconsFilePath,\n\n    [Parameter(Mandatory=$true)]\n    [string]$FullVersion,\n\n    [Parameter(Mandatory=$true)]\n    [string]$ShortVersion\n)\n\n$ErrorActionPreference = \"Stop\"\n\n# Setup paths\n$tempDirPath = Join-Path $PublishDirPath \"../publish-macos-app-temp\"\n$bundleName = \"DiscordChatExporter.app\"\n$bundleDirPath = Join-Path $tempDirPath $bundleName\n$contentsDirPath = Join-Path $bundleDirPath \"Contents\"\n$macosDirPath = Join-Path $contentsDirPath \"MacOS\"\n$resourcesDirPath = Join-Path $contentsDirPath \"Resources\"\n\ntry {\n    # Initialize the bundle's directory structure\n    New-Item -Path $bundleDirPath -ItemType Directory -Force\n    New-Item -Path $contentsDirPath -ItemType Directory -Force\n    New-Item -Path $macosDirPath -ItemType Directory -Force\n    New-Item -Path $resourcesDirPath -ItemType Directory -Force\n\n    # Copy icons into the .app's Resources folder\n    Copy-Item -Path $IconsFilePath -Destination (Join-Path $resourcesDirPath \"AppIcon.icns\") -Force\n\n    # Generate the Info.plist metadata file with the app information\n    $plistContent = @\"\n<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n  <dict>\n    <key>CFBundleDisplayName</key>\n    <string>DiscordChatExporter</string>\n    <key>CFBundleName</key>\n    <string>DiscordChatExporter</string>\n    <key>CFBundleExecutable</key>\n    <string>DiscordChatExporter</string>\n    <key>NSHumanReadableCopyright</key>\n    <string>© Oleksii Holub</string>\n    <key>CFBundleIdentifier</key>\n    <string>me.Tyrrrz.DiscordChatExporter</string>\n    <key>CFBundleSpokenName</key>\n    <string>Discord Chat Exporter</string>\n    <key>CFBundleIconFile</key>\n    <string>AppIcon</string>\n    <key>CFBundleIconName</key>\n    <string>AppIcon</string>\n    <key>CFBundleVersion</key>\n    <string>$FullVersion</string>\n    <key>CFBundleShortVersionString</key>\n    <string>$ShortVersion</string>\n    <key>NSHighResolutionCapable</key>\n    <true />\n    <key>CFBundlePackageType</key>\n    <string>APPL</string>\n  </dict>\n</plist>\n\"@\n\n    Set-Content -Path (Join-Path $contentsDirPath \"Info.plist\") -Value $plistContent\n\n    # Delete the previous bundle if it exists\n    if (Test-Path (Join-Path $PublishDirPath $bundleName)) {\n        Remove-Item -Path (Join-Path $PublishDirPath $bundleName) -Recurse -Force\n    }\n\n    # Move all files from the publish directory into the MacOS directory\n    Get-ChildItem -Path $PublishDirPath | ForEach-Object {\n        Move-Item -Path $_.FullName -Destination $macosDirPath -Force\n    }\n\n    # Move the final bundle into the publish directory for upload\n    Move-Item -Path $bundleDirPath -Destination $PublishDirPath -Force\n}\nfinally {\n    # Clean up the temporary directory\n    Remove-Item -Path $tempDirPath -Recurse -Force\n}"
  },
  {
    "path": "DiscordChatExporter.Gui/Services/SettingsService.TokenEncryptionConverter.cs",
    "content": "using System;\nusing System.Security.Cryptography;\nusing System.Text;\nusing System.Text.Json;\nusing System.Text.Json.Serialization;\nusing DiscordChatExporter.Gui.Utils.Extensions;\n\nnamespace DiscordChatExporter.Gui.Services;\n\npublic partial class SettingsService\n{\n    private class TokenEncryptionConverter : JsonConverter<string?>\n    {\n        private const string Prefix = \"enc:\";\n\n        private static readonly Lazy<byte[]> Key = new(() =>\n            Rfc2898DeriveBytes.Pbkdf2(\n                Encoding.UTF8.GetBytes(Environment.TryGetMachineId() ?? string.Empty),\n                Encoding.UTF8.GetBytes(ThisAssembly.Project.EncryptionSalt),\n                600_000,\n                HashAlgorithmName.SHA256,\n                16\n            )\n        );\n\n        public override string? Read(\n            ref Utf8JsonReader reader,\n            Type typeToConvert,\n            JsonSerializerOptions options\n        )\n        {\n            var value = reader.GetString();\n\n            // No prefix means the token is stored as plain text, which was the case for older\n            // versions of the application. Load it as is and encrypt it on next save.\n            if (\n                string.IsNullOrWhiteSpace(value)\n                || !value.StartsWith(Prefix, StringComparison.Ordinal)\n            )\n            {\n                return value;\n            }\n\n            try\n            {\n                var encryptedData = Convert.FromHexString(value[Prefix.Length..]);\n                var tokenData = new byte[encryptedData.AsSpan(28).Length];\n\n                // Layout: nonce (12 bytes) | tag (16 bytes) | cipher\n                using var aes = new AesGcm(Key.Value, 16);\n                aes.Decrypt(\n                    encryptedData.AsSpan(0, 12),\n                    encryptedData.AsSpan(28),\n                    encryptedData.AsSpan(12, 16),\n                    tokenData\n                );\n\n                return Encoding.UTF8.GetString(tokenData);\n            }\n            catch (Exception ex)\n                when (ex\n                        is FormatException\n                            or CryptographicException\n                            or ArgumentException\n                            or IndexOutOfRangeException\n                )\n            {\n                return null;\n            }\n        }\n\n        public override void Write(\n            Utf8JsonWriter writer,\n            string? value,\n            JsonSerializerOptions options\n        )\n        {\n            if (string.IsNullOrWhiteSpace(value))\n            {\n                writer.WriteNullValue();\n                return;\n            }\n\n            var tokenData = Encoding.UTF8.GetBytes(value);\n            var encryptedData = new byte[28 + tokenData.Length];\n\n            // Nonce\n            RandomNumberGenerator.Fill(encryptedData.AsSpan(0, 12));\n\n            // Layout: nonce (12 bytes) | tag (16 bytes) | cipher\n            using var aes = new AesGcm(Key.Value, 16);\n            aes.Encrypt(\n                encryptedData.AsSpan(0, 12),\n                tokenData,\n                encryptedData.AsSpan(28),\n                encryptedData.AsSpan(12, 16)\n            );\n\n            writer.WriteStringValue(Prefix + Convert.ToHexStringLower(encryptedData));\n        }\n    }\n}\n"
  },
  {
    "path": "DiscordChatExporter.Gui/Services/SettingsService.cs",
    "content": "﻿using System.Text.Json.Serialization;\nusing Cogwheel;\nusing CommunityToolkit.Mvvm.ComponentModel;\nusing DiscordChatExporter.Core.Discord;\nusing DiscordChatExporter.Core.Exporting;\nusing DiscordChatExporter.Gui.Framework;\nusing DiscordChatExporter.Gui.Localization;\nusing DiscordChatExporter.Gui.Models;\n\nnamespace DiscordChatExporter.Gui.Services;\n\n[ObservableObject]\npublic partial class SettingsService()\n    : SettingsBase(StartOptions.Current.SettingsPath, SerializerContext.Default)\n{\n    [ObservableProperty]\n    public partial bool IsUkraineSupportMessageEnabled { get; set; } = true;\n\n    [ObservableProperty]\n    public partial ThemeVariant Theme { get; set; }\n\n    [ObservableProperty]\n    public partial Language Language { get; set; }\n\n    [ObservableProperty]\n    public partial bool IsAutoUpdateEnabled { get; set; } = true;\n\n    [ObservableProperty]\n    public partial bool IsTokenPersisted { get; set; } = true;\n\n    [ObservableProperty]\n    public partial RateLimitPreference RateLimitPreference { get; set; } =\n        RateLimitPreference.RespectAll;\n\n    [ObservableProperty]\n    public partial ThreadInclusionMode ThreadInclusionMode { get; set; }\n\n    [ObservableProperty]\n    public partial string? Locale { get; set; }\n\n    [ObservableProperty]\n    public partial bool IsUtcNormalizationEnabled { get; set; }\n\n    [ObservableProperty]\n    public partial int ParallelLimit { get; set; } = 1;\n\n    [ObservableProperty]\n    [JsonConverter(typeof(TokenEncryptionConverter))]\n    public partial string? LastToken { get; set; }\n\n    [ObservableProperty]\n    public partial ExportFormat LastExportFormat { get; set; } = ExportFormat.HtmlDark;\n\n    [ObservableProperty]\n    public partial string? LastPartitionLimitValue { get; set; }\n\n    [ObservableProperty]\n    public partial string? LastMessageFilterValue { get; set; }\n\n    [ObservableProperty]\n    public partial bool LastIsReverseMessageOrder { get; set; }\n\n    [ObservableProperty]\n    public partial bool LastShouldFormatMarkdown { get; set; } = true;\n\n    [ObservableProperty]\n    public partial bool LastShouldDownloadAssets { get; set; }\n\n    [ObservableProperty]\n    public partial bool LastShouldReuseAssets { get; set; }\n\n    [ObservableProperty]\n    public partial string? LastAssetsDirPath { get; set; }\n\n    public override void Save()\n    {\n        // Clear the token if it's not supposed to be persisted\n        var lastToken = LastToken;\n        if (!IsTokenPersisted)\n            LastToken = null;\n\n        base.Save();\n\n        LastToken = lastToken;\n    }\n}\n\npublic partial class SettingsService\n{\n    [JsonSerializable(typeof(SettingsService))]\n    private partial class SerializerContext : JsonSerializerContext;\n}\n"
  },
  {
    "path": "DiscordChatExporter.Gui/Services/UpdateService.cs",
    "content": "﻿using System;\nusing System.Runtime.InteropServices;\nusing System.Threading.Tasks;\nusing Onova;\nusing Onova.Exceptions;\nusing Onova.Services;\n\nnamespace DiscordChatExporter.Gui.Services;\n\npublic class UpdateService(SettingsService settingsService) : IDisposable\n{\n    private readonly IUpdateManager? _updateManager = OperatingSystem.IsWindows()\n        ? new UpdateManager(\n            new GithubPackageResolver(\n                \"Tyrrrz\",\n                \"DiscordChatExporter\",\n                // Examples:\n                // DiscordChatExporter.win-arm64.zip\n                // DiscordChatExporter.win-x64.zip\n                // DiscordChatExporter.linux-x64.zip\n                $\"DiscordChatExporter.{RuntimeInformation.RuntimeIdentifier}.zip\"\n            ),\n            new ZipPackageExtractor()\n        )\n        : null;\n\n    private Version? _updateVersion;\n    private bool _updatePrepared;\n    private bool _updaterLaunched;\n\n    public async ValueTask<Version?> CheckForUpdatesAsync()\n    {\n        if (_updateManager is null)\n            return null;\n\n        if (!settingsService.IsAutoUpdateEnabled)\n            return null;\n\n        var check = await _updateManager.CheckForUpdatesAsync();\n        return check.CanUpdate ? check.LastVersion : null;\n    }\n\n    public async ValueTask PrepareUpdateAsync(Version version)\n    {\n        if (_updateManager is null)\n            return;\n\n        if (!settingsService.IsAutoUpdateEnabled)\n            return;\n\n        try\n        {\n            await _updateManager.PrepareUpdateAsync(_updateVersion = version);\n            _updatePrepared = true;\n        }\n        catch (UpdaterAlreadyLaunchedException)\n        {\n            // Ignore race conditions\n        }\n        catch (LockFileNotAcquiredException)\n        {\n            // Ignore race conditions\n        }\n    }\n\n    public void FinalizeUpdate(bool needRestart)\n    {\n        if (_updateManager is null)\n            return;\n\n        if (!settingsService.IsAutoUpdateEnabled)\n            return;\n\n        if (_updateVersion is null || !_updatePrepared || _updaterLaunched)\n            return;\n\n        try\n        {\n            _updateManager.LaunchUpdater(_updateVersion, needRestart);\n            _updaterLaunched = true;\n        }\n        catch (UpdaterAlreadyLaunchedException)\n        {\n            // Ignore race conditions\n        }\n        catch (LockFileNotAcquiredException)\n        {\n            // Ignore race conditions\n        }\n    }\n\n    public void Dispose() => _updateManager?.Dispose();\n}\n"
  },
  {
    "path": "DiscordChatExporter.Gui/StartOptions.cs",
    "content": "using System;\nusing System.IO;\n\nnamespace DiscordChatExporter.Gui;\n\npublic partial class StartOptions\n{\n    public required string SettingsPath { get; init; }\n}\n\npublic partial class StartOptions\n{\n    public static StartOptions Current { get; } =\n        new()\n        {\n            SettingsPath =\n                Environment.GetEnvironmentVariable(\"DISCORDCHATEXPORTER_SETTINGS_PATH\") is { } path\n                && !string.IsNullOrWhiteSpace(path)\n                    ? Path.EndsInDirectorySeparator(path) || Directory.Exists(path)\n                        ? Path.Combine(path, \"Settings.dat\")\n                        : path\n                    : Path.Combine(AppContext.BaseDirectory, \"Settings.dat\"),\n        };\n}\n"
  },
  {
    "path": "DiscordChatExporter.Gui/Utils/Disposable.cs",
    "content": "﻿using System;\n\nnamespace DiscordChatExporter.Gui.Utils;\n\ninternal class Disposable(Action dispose) : IDisposable\n{\n    public static IDisposable Create(Action dispose) => new Disposable(dispose);\n\n    public void Dispose() => dispose();\n}\n"
  },
  {
    "path": "DiscordChatExporter.Gui/Utils/DisposableCollector.cs",
    "content": "﻿using System;\nusing System.Collections.Generic;\nusing DiscordChatExporter.Gui.Utils.Extensions;\n\nnamespace DiscordChatExporter.Gui.Utils;\n\ninternal class DisposableCollector : IDisposable\n{\n    private readonly object _lock = new();\n    private readonly List<IDisposable> _items = [];\n\n    public void Add(IDisposable item)\n    {\n        lock (_lock)\n        {\n            _items.Add(item);\n        }\n    }\n\n    public void Dispose()\n    {\n        lock (_lock)\n        {\n            _items.DisposeAll();\n            _items.Clear();\n        }\n    }\n}\n"
  },
  {
    "path": "DiscordChatExporter.Gui/Utils/Extensions/AvaloniaExtensions.cs",
    "content": "﻿using Avalonia.Controls;\nusing Avalonia.Controls.ApplicationLifetimes;\nusing Avalonia.VisualTree;\n\nnamespace DiscordChatExporter.Gui.Utils.Extensions;\n\ninternal static class AvaloniaExtensions\n{\n    extension(IApplicationLifetime lifetime)\n    {\n        public Window? TryGetMainWindow() =>\n            lifetime is IClassicDesktopStyleApplicationLifetime desktopLifetime\n                ? desktopLifetime.MainWindow\n                : null;\n\n        public TopLevel? TryGetTopLevel() =>\n            lifetime.TryGetMainWindow()\n            ?? (lifetime as ISingleViewApplicationLifetime)?.MainView?.GetVisualRoot() as TopLevel;\n\n        public bool TryShutdown(int exitCode = 0)\n        {\n            if (lifetime is IClassicDesktopStyleApplicationLifetime desktopLifetime)\n            {\n                return desktopLifetime.TryShutdown(exitCode);\n            }\n\n            if (lifetime is IControlledApplicationLifetime controlledLifetime)\n            {\n                controlledLifetime.Shutdown(exitCode);\n                return true;\n            }\n\n            return false;\n        }\n    }\n}\n"
  },
  {
    "path": "DiscordChatExporter.Gui/Utils/Extensions/DisposableExtensions.cs",
    "content": "﻿using System;\nusing System.Collections.Generic;\nusing System.Linq;\n\nnamespace DiscordChatExporter.Gui.Utils.Extensions;\n\ninternal static class DisposableExtensions\n{\n    extension(IEnumerable<IDisposable> disposables)\n    {\n        public void DisposeAll()\n        {\n            var exceptions = default(List<Exception>);\n\n            foreach (var disposable in disposables)\n            {\n                try\n                {\n                    disposable.Dispose();\n                }\n                catch (Exception ex)\n                {\n                    (exceptions ??= []).Add(ex);\n                }\n            }\n\n            if (exceptions?.Any() == true)\n                throw new AggregateException(exceptions);\n        }\n    }\n}\n"
  },
  {
    "path": "DiscordChatExporter.Gui/Utils/Extensions/EnvironmentExtensions.cs",
    "content": "using System;\nusing System.IO;\n\nnamespace DiscordChatExporter.Gui.Utils.Extensions;\n\ninternal static class EnvironmentExtensions\n{\n    extension(Environment)\n    {\n        public static string? TryGetMachineId()\n        {\n            // Windows: stable GUID written during OS installation\n            if (OperatingSystem.IsWindows())\n            {\n                try\n                {\n                    using var regKey = Microsoft.Win32.Registry.LocalMachine.OpenSubKey(\n                        @\"SOFTWARE\\Microsoft\\Cryptography\"\n                    );\n                    if (\n                        regKey?.GetValue(\"MachineGuid\") is string guid\n                        && !string.IsNullOrWhiteSpace(guid)\n                    )\n                        return guid;\n                }\n                catch { }\n            }\n            else\n            {\n                // Unix: /etc/machine-id (set once by systemd at first boot)\n                foreach (var path in new[] { \"/etc/machine-id\", \"/var/lib/dbus/machine-id\" })\n                {\n                    try\n                    {\n                        var id = File.ReadAllText(path).Trim();\n                        if (!string.IsNullOrWhiteSpace(id))\n                            return id;\n                    }\n                    catch { }\n                }\n            }\n\n            // Last-resort fallback\n            try\n            {\n                return Environment.MachineName;\n            }\n            catch\n            {\n                return null;\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "DiscordChatExporter.Gui/Utils/Extensions/MarkdigExtensions.cs",
    "content": "using System.Linq;\nusing Markdig.Syntax.Inlines;\n\nnamespace DiscordChatExporter.Gui.Utils.Extensions;\n\ninternal static class MarkdigExtensions\n{\n    extension(Inline inline)\n    {\n        public string GetInnerText() =>\n            inline switch\n            {\n                LiteralInline literal => literal.Content.ToString(),\n                ContainerInline container => string.Concat(container.Select(c => c.GetInnerText())),\n                _ => string.Empty,\n            };\n    }\n}\n"
  },
  {
    "path": "DiscordChatExporter.Gui/Utils/Extensions/NotifyPropertyChangedExtensions.cs",
    "content": "﻿using System;\nusing System.ComponentModel;\nusing System.Linq.Expressions;\nusing System.Reflection;\n\nnamespace DiscordChatExporter.Gui.Utils.Extensions;\n\ninternal static class NotifyPropertyChangedExtensions\n{\n    extension<TOwner>(TOwner owner)\n        where TOwner : INotifyPropertyChanged\n    {\n        public IDisposable WatchProperty<TProperty>(\n            Expression<Func<TOwner, TProperty>> propertyExpression,\n            Action callback,\n            bool watchInitialValue = false\n        )\n        {\n            var memberExpression = propertyExpression.Body as MemberExpression;\n            if (memberExpression?.Member is not PropertyInfo property)\n                throw new ArgumentException(\"Provided expression must reference a property.\");\n\n            void OnPropertyChanged(object? sender, PropertyChangedEventArgs args)\n            {\n                if (\n                    string.IsNullOrWhiteSpace(args.PropertyName)\n                    || string.Equals(args.PropertyName, property.Name, StringComparison.Ordinal)\n                )\n                {\n                    callback();\n                }\n            }\n\n            owner.PropertyChanged += OnPropertyChanged;\n\n            if (watchInitialValue)\n                callback();\n\n            return Disposable.Create(() => owner.PropertyChanged -= OnPropertyChanged);\n        }\n\n        public IDisposable WatchAllProperties(Action callback, bool watchInitialValues = false)\n        {\n            void OnPropertyChanged(object? sender, PropertyChangedEventArgs args) => callback();\n            owner.PropertyChanged += OnPropertyChanged;\n\n            if (watchInitialValues)\n                callback();\n\n            return Disposable.Create(() => owner.PropertyChanged -= OnPropertyChanged);\n        }\n    }\n}\n"
  },
  {
    "path": "DiscordChatExporter.Gui/Utils/Extensions/ProcessExtensions.cs",
    "content": "﻿using System.Diagnostics;\n\nnamespace DiscordChatExporter.Gui.Utils.Extensions;\n\ninternal static class ProcessExtensions\n{\n    extension(Process)\n    {\n        public static void StartShellExecute(string path)\n        {\n            using var process = new Process();\n            process.StartInfo = new ProcessStartInfo(path) { UseShellExecute = true };\n\n            process.Start();\n        }\n    }\n}\n"
  },
  {
    "path": "DiscordChatExporter.Gui/Utils/Internationalization.cs",
    "content": "﻿using System.Globalization;\n\nnamespace DiscordChatExporter.Gui.Utils;\n\ninternal static class Internationalization\n{\n    public static bool Is24Hours =>\n        string.IsNullOrWhiteSpace(CultureInfo.CurrentCulture.DateTimeFormat.AMDesignator)\n        && string.IsNullOrWhiteSpace(CultureInfo.CurrentCulture.DateTimeFormat.PMDesignator);\n\n    public static string AvaloniaClockIdentifier => Is24Hours ? \"24HourClock\" : \"12HourClock\";\n}\n"
  },
  {
    "path": "DiscordChatExporter.Gui/Utils/NativeMethods.cs",
    "content": "﻿using System.Runtime.InteropServices;\n\nnamespace DiscordChatExporter.Gui.Utils;\n\ninternal static class NativeMethods\n{\n    public static class Windows\n    {\n        [DllImport(\"user32.dll\", SetLastError = true)]\n        public static extern int MessageBox(nint hWnd, string text, string caption, uint type);\n    }\n}\n"
  },
  {
    "path": "DiscordChatExporter.Gui/ViewModels/Components/DashboardViewModel.cs",
    "content": "﻿using System;\nusing System.Collections.Generic;\nusing System.Collections.ObjectModel;\nusing System.Diagnostics;\nusing System.Linq;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing CommunityToolkit.Mvvm.ComponentModel;\nusing CommunityToolkit.Mvvm.Input;\nusing DiscordChatExporter.Core.Discord;\nusing DiscordChatExporter.Core.Discord.Data;\nusing DiscordChatExporter.Core.Exceptions;\nusing DiscordChatExporter.Core.Exporting;\nusing DiscordChatExporter.Core.Utils.Extensions;\nusing DiscordChatExporter.Gui.Framework;\nusing DiscordChatExporter.Gui.Localization;\nusing DiscordChatExporter.Gui.Models;\nusing DiscordChatExporter.Gui.Services;\nusing DiscordChatExporter.Gui.Utils;\nusing DiscordChatExporter.Gui.Utils.Extensions;\nusing Gress;\nusing Gress.Completable;\n\nnamespace DiscordChatExporter.Gui.ViewModels.Components;\n\npublic partial class DashboardViewModel : ViewModelBase\n{\n    private readonly ViewModelManager _viewModelManager;\n    private readonly SnackbarManager _snackbarManager;\n    private readonly DialogManager _dialogManager;\n    private readonly SettingsService _settingsService;\n\n    private readonly DisposableCollector _eventRoot = new();\n    private readonly AutoResetProgressMuxer _progressMuxer;\n\n    private DiscordClient? _discord;\n\n    public DashboardViewModel(\n        ViewModelManager viewModelManager,\n        DialogManager dialogManager,\n        SnackbarManager snackbarManager,\n        SettingsService settingsService,\n        LocalizationManager localizationManager\n    )\n    {\n        _viewModelManager = viewModelManager;\n        _dialogManager = dialogManager;\n        _snackbarManager = snackbarManager;\n        _settingsService = settingsService;\n        LocalizationManager = localizationManager;\n\n        _progressMuxer = Progress.CreateMuxer().WithAutoReset();\n\n        _eventRoot.Add(\n            Progress.WatchProperty(\n                o => o.Current,\n                () => OnPropertyChanged(nameof(IsProgressIndeterminate))\n            )\n        );\n\n        _eventRoot.Add(\n            SelectedChannels.WatchProperty(\n                o => o.Count,\n                () => ExportCommand.NotifyCanExecuteChanged()\n            )\n        );\n    }\n\n    [ObservableProperty]\n    [NotifyPropertyChangedFor(nameof(IsProgressIndeterminate))]\n    [NotifyCanExecuteChangedFor(nameof(PullGuildsCommand))]\n    [NotifyCanExecuteChangedFor(nameof(PullChannelsCommand))]\n    [NotifyCanExecuteChangedFor(nameof(ExportCommand))]\n    public partial bool IsBusy { get; set; }\n\n    public LocalizationManager LocalizationManager { get; }\n\n    public ProgressContainer<Percentage> Progress { get; } = new();\n\n    public bool IsProgressIndeterminate => IsBusy && Progress.Current.Fraction is <= 0 or >= 1;\n\n    [ObservableProperty]\n    [NotifyCanExecuteChangedFor(nameof(PullGuildsCommand))]\n    public partial string? Token { get; set; }\n\n    [ObservableProperty]\n    public partial IReadOnlyList<Guild>? AvailableGuilds { get; set; }\n\n    [ObservableProperty]\n    [NotifyCanExecuteChangedFor(nameof(PullChannelsCommand))]\n    [NotifyCanExecuteChangedFor(nameof(ExportCommand))]\n    public partial Guild? SelectedGuild { get; set; }\n\n    [ObservableProperty]\n    public partial IReadOnlyList<ChannelConnection>? AvailableChannels { get; set; }\n\n    public ObservableCollection<ChannelConnection> SelectedChannels { get; } = [];\n\n    [RelayCommand]\n    private void Initialize()\n    {\n        if (!string.IsNullOrWhiteSpace(_settingsService.LastToken))\n            Token = _settingsService.LastToken;\n    }\n\n    [RelayCommand]\n    private async Task ShowSettingsAsync() =>\n        await _dialogManager.ShowDialogAsync(_viewModelManager.CreateSettingsViewModel());\n\n    private bool CanPullGuilds() => !IsBusy && !string.IsNullOrWhiteSpace(Token);\n\n    [RelayCommand(CanExecute = nameof(CanPullGuilds))]\n    private async Task PullGuildsAsync()\n    {\n        IsBusy = true;\n        var progress = _progressMuxer.CreateInput();\n\n        try\n        {\n            var token = Token?.Trim('\"', ' ');\n            if (string.IsNullOrWhiteSpace(token))\n                return;\n\n            AvailableGuilds = null;\n            SelectedGuild = null;\n            AvailableChannels = null;\n            SelectedChannels.Clear();\n\n            _discord = new DiscordClient(token, _settingsService.RateLimitPreference);\n            _settingsService.LastToken = token;\n\n            var guilds = await _discord.GetUserGuildsAsync();\n\n            AvailableGuilds = guilds;\n            SelectedGuild = guilds.FirstOrDefault();\n\n            await PullChannelsAsync();\n        }\n        catch (DiscordChatExporterException ex) when (!ex.IsFatal)\n        {\n            _snackbarManager.Notify(ex.Message.TrimEnd('.'));\n        }\n        catch (Exception ex)\n        {\n            var dialog = _viewModelManager.CreateMessageBoxViewModel(\n                LocalizationManager.ErrorPullingGuildsTitle,\n                ex.ToString()\n            );\n\n            await _dialogManager.ShowDialogAsync(dialog);\n        }\n        finally\n        {\n            progress.ReportCompletion();\n            IsBusy = false;\n        }\n    }\n\n    private bool CanPullChannels() => !IsBusy && _discord is not null && SelectedGuild is not null;\n\n    [RelayCommand(CanExecute = nameof(CanPullChannels))]\n    private async Task PullChannelsAsync()\n    {\n        IsBusy = true;\n        var progress = _progressMuxer.CreateInput();\n\n        try\n        {\n            if (_discord is null || SelectedGuild is null)\n                return;\n\n            AvailableChannels = null;\n            SelectedChannels.Clear();\n\n            var channels = new List<Channel>();\n\n            // Regular channels\n            await foreach (var channel in _discord.GetGuildChannelsAsync(SelectedGuild.Id))\n                channels.Add(channel);\n\n            // Threads\n            if (_settingsService.ThreadInclusionMode != ThreadInclusionMode.None)\n            {\n                await foreach (\n                    var thread in _discord.GetGuildThreadsAsync(\n                        SelectedGuild.Id,\n                        _settingsService.ThreadInclusionMode == ThreadInclusionMode.All\n                    )\n                )\n                {\n                    channels.Add(thread);\n                }\n            }\n\n            // Build a hierarchy of channels\n            var channelTree = ChannelConnection.BuildTree(\n                channels\n                    .OrderByDescending(c => c.IsDirect ? c.LastMessageId : null)\n                    .ThenBy(c => c.Position)\n                    .ToArray()\n            );\n\n            AvailableChannels = channelTree;\n            SelectedChannels.Clear();\n        }\n        catch (DiscordChatExporterException ex) when (!ex.IsFatal)\n        {\n            _snackbarManager.Notify(ex.Message.TrimEnd('.'));\n        }\n        catch (Exception ex)\n        {\n            var dialog = _viewModelManager.CreateMessageBoxViewModel(\n                LocalizationManager.ErrorPullingChannelsTitle,\n                ex.ToString()\n            );\n\n            await _dialogManager.ShowDialogAsync(dialog);\n        }\n        finally\n        {\n            progress.ReportCompletion();\n            IsBusy = false;\n        }\n    }\n\n    private bool CanExport() =>\n        !IsBusy && _discord is not null && SelectedGuild is not null && SelectedChannels.Any();\n\n    [RelayCommand(CanExecute = nameof(CanExport))]\n    private async Task ExportAsync()\n    {\n        IsBusy = true;\n\n        try\n        {\n            if (_discord is null || SelectedGuild is null || !SelectedChannels.Any())\n                return;\n\n            var dialog = _viewModelManager.CreateExportSetupViewModel(\n                SelectedGuild,\n                SelectedChannels.Select(c => c.Channel).ToArray()\n            );\n\n            if (await _dialogManager.ShowDialogAsync(dialog) != true)\n                return;\n\n            var exporter = new ChannelExporter(_discord);\n\n            var channelProgressPairs = dialog\n                .Channels!.Select(c => new { Channel = c, Progress = _progressMuxer.CreateInput() })\n                .ToArray();\n\n            var successfulExportCount = 0;\n\n            await Parallel.ForEachAsync(\n                channelProgressPairs,\n                new ParallelOptions\n                {\n                    MaxDegreeOfParallelism = Math.Max(1, _settingsService.ParallelLimit),\n                },\n                async (pair, cancellationToken) =>\n                {\n                    var channel = pair.Channel;\n                    var progress = pair.Progress;\n\n                    try\n                    {\n                        var request = new ExportRequest(\n                            dialog.Guild!,\n                            channel,\n                            dialog.OutputPath!,\n                            dialog.AssetsDirPath,\n                            dialog.SelectedFormat,\n                            dialog.After?.Pipe(Snowflake.FromDate),\n                            dialog.Before?.Pipe(Snowflake.FromDate),\n                            dialog.PartitionLimit,\n                            dialog.MessageFilter,\n                            dialog.IsReverseMessageOrder,\n                            dialog.ShouldFormatMarkdown,\n                            dialog.ShouldDownloadAssets,\n                            dialog.ShouldReuseAssets,\n                            _settingsService.Locale,\n                            _settingsService.IsUtcNormalizationEnabled\n                        );\n\n                        await exporter.ExportChannelAsync(request, progress, cancellationToken);\n\n                        Interlocked.Increment(ref successfulExportCount);\n                    }\n                    catch (ChannelEmptyException ex)\n                    {\n                        _snackbarManager.Notify(ex.Message.TrimEnd('.'));\n                    }\n                    catch (DiscordChatExporterException ex) when (!ex.IsFatal)\n                    {\n                        _snackbarManager.Notify(ex.Message.TrimEnd('.'));\n                    }\n                    finally\n                    {\n                        progress.ReportCompletion();\n                    }\n                }\n            );\n\n            // Notify of the overall completion\n            if (successfulExportCount > 0)\n            {\n                _snackbarManager.Notify(\n                    string.Format(\n                        LocalizationManager.SuccessfulExportMessage,\n                        successfulExportCount\n                    )\n                );\n            }\n        }\n        catch (Exception ex)\n        {\n            var dialog = _viewModelManager.CreateMessageBoxViewModel(\n                LocalizationManager.ErrorExportingTitle,\n                ex.ToString()\n            );\n\n            await _dialogManager.ShowDialogAsync(dialog);\n        }\n        finally\n        {\n            IsBusy = false;\n        }\n    }\n\n    protected override void Dispose(bool disposing)\n    {\n        if (disposing)\n        {\n            _eventRoot.Dispose();\n        }\n\n        base.Dispose(disposing);\n    }\n}\n"
  },
  {
    "path": "DiscordChatExporter.Gui/ViewModels/Dialogs/ExportSetupViewModel.cs",
    "content": "﻿using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Threading.Tasks;\nusing Avalonia.Platform.Storage;\nusing CommunityToolkit.Mvvm.ComponentModel;\nusing CommunityToolkit.Mvvm.Input;\nusing DiscordChatExporter.Core.Discord;\nusing DiscordChatExporter.Core.Discord.Data;\nusing DiscordChatExporter.Core.Exporting;\nusing DiscordChatExporter.Core.Exporting.Filtering;\nusing DiscordChatExporter.Core.Exporting.Partitioning;\nusing DiscordChatExporter.Core.Utils.Extensions;\nusing DiscordChatExporter.Gui.Framework;\nusing DiscordChatExporter.Gui.Localization;\nusing DiscordChatExporter.Gui.Services;\n\nnamespace DiscordChatExporter.Gui.ViewModels.Dialogs;\n\npublic partial class ExportSetupViewModel(\n    DialogManager dialogManager,\n    SettingsService settingsService,\n    LocalizationManager localizationManager\n) : DialogViewModelBase\n{\n    public LocalizationManager LocalizationManager { get; } = localizationManager;\n\n    [ObservableProperty]\n    public partial Guild? Guild { get; set; }\n\n    [ObservableProperty]\n    [NotifyPropertyChangedFor(nameof(IsSingleChannel))]\n    public partial IReadOnlyList<Channel>? Channels { get; set; }\n\n    [ObservableProperty]\n    public partial string? OutputPath { get; set; }\n\n    [ObservableProperty]\n    public partial ExportFormat SelectedFormat { get; set; }\n\n    [ObservableProperty]\n    [NotifyPropertyChangedFor(nameof(IsAfterDateSet))]\n    [NotifyPropertyChangedFor(nameof(After))]\n    public partial DateTimeOffset? AfterDate { get; set; }\n\n    [ObservableProperty]\n    public partial TimeSpan? AfterTime { get; set; }\n\n    [ObservableProperty]\n    [NotifyPropertyChangedFor(nameof(IsBeforeDateSet))]\n    [NotifyPropertyChangedFor(nameof(Before))]\n    public partial DateTimeOffset? BeforeDate { get; set; }\n\n    [ObservableProperty]\n    public partial TimeSpan? BeforeTime { get; set; }\n\n    [ObservableProperty]\n    [NotifyPropertyChangedFor(nameof(PartitionLimit))]\n    public partial string? PartitionLimitValue { get; set; }\n\n    [ObservableProperty]\n    [NotifyPropertyChangedFor(nameof(MessageFilter))]\n    public partial string? MessageFilterValue { get; set; }\n\n    [ObservableProperty]\n    public partial bool IsReverseMessageOrder { get; set; }\n\n    [ObservableProperty]\n    public partial bool ShouldFormatMarkdown { get; set; }\n\n    [ObservableProperty]\n    public partial bool ShouldDownloadAssets { get; set; }\n\n    [ObservableProperty]\n    public partial bool ShouldReuseAssets { get; set; }\n\n    [ObservableProperty]\n    public partial string? AssetsDirPath { get; set; }\n\n    [ObservableProperty]\n    public partial bool IsAdvancedSectionDisplayed { get; set; }\n\n    public bool IsSingleChannel => Channels?.Count == 1;\n\n    public IReadOnlyList<ExportFormat> AvailableFormats { get; } = Enum.GetValues<ExportFormat>();\n\n    public bool IsAfterDateSet => AfterDate is not null;\n\n    public DateTimeOffset? After => AfterDate?.Add(AfterTime ?? TimeSpan.Zero);\n\n    public bool IsBeforeDateSet => BeforeDate is not null;\n\n    public DateTimeOffset? Before => BeforeDate?.Add(BeforeTime ?? TimeSpan.Zero);\n\n    public PartitionLimit PartitionLimit =>\n        !string.IsNullOrWhiteSpace(PartitionLimitValue)\n            ? PartitionLimit.Parse(PartitionLimitValue)\n            : PartitionLimit.Null;\n\n    public MessageFilter MessageFilter =>\n        !string.IsNullOrWhiteSpace(MessageFilterValue)\n            ? MessageFilter.Parse(MessageFilterValue)\n            : MessageFilter.Null;\n\n    [RelayCommand]\n    private void Initialize()\n    {\n        // Persist preferences\n        SelectedFormat = settingsService.LastExportFormat;\n        PartitionLimitValue = settingsService.LastPartitionLimitValue;\n        MessageFilterValue = settingsService.LastMessageFilterValue;\n        IsReverseMessageOrder = settingsService.LastIsReverseMessageOrder;\n        ShouldFormatMarkdown = settingsService.LastShouldFormatMarkdown;\n        ShouldDownloadAssets = settingsService.LastShouldDownloadAssets;\n        ShouldReuseAssets = settingsService.LastShouldReuseAssets;\n        AssetsDirPath = settingsService.LastAssetsDirPath;\n\n        // Show the \"advanced options\" section by default if any\n        // of the advanced options are set to non-default values.\n        IsAdvancedSectionDisplayed =\n            After is not null\n            || Before is not null\n            || !string.IsNullOrWhiteSpace(PartitionLimitValue)\n            || !string.IsNullOrWhiteSpace(MessageFilterValue)\n            || ShouldDownloadAssets\n            || ShouldReuseAssets\n            || !string.IsNullOrWhiteSpace(AssetsDirPath)\n            || IsReverseMessageOrder;\n    }\n\n    [RelayCommand]\n    private async Task ShowOutputPathPromptAsync()\n    {\n        if (IsSingleChannel)\n        {\n            var defaultFileName = ExportRequest.GetDefaultOutputFileName(\n                Guild!,\n                Channels!.Single(),\n                SelectedFormat,\n                After?.Pipe(Snowflake.FromDate),\n                Before?.Pipe(Snowflake.FromDate)\n            );\n\n            var extension = SelectedFormat.GetFileExtension();\n\n            var path = await dialogManager.PromptSaveFilePathAsync(\n                [\n                    new FilePickerFileType($\"{extension.ToUpperInvariant()} file\")\n                    {\n                        Patterns = [$\"*.{extension}\"],\n                    },\n                ],\n                defaultFileName\n            );\n\n            if (!string.IsNullOrWhiteSpace(path))\n                OutputPath = path;\n        }\n        else\n        {\n            var path = await dialogManager.PromptDirectoryPathAsync();\n            if (!string.IsNullOrWhiteSpace(path))\n                OutputPath = path;\n        }\n    }\n\n    [RelayCommand]\n    private async Task ShowAssetsDirPathPromptAsync()\n    {\n        var path = await dialogManager.PromptDirectoryPathAsync();\n        if (!string.IsNullOrWhiteSpace(path))\n            AssetsDirPath = path;\n    }\n\n    [RelayCommand]\n    private async Task ConfirmAsync()\n    {\n        // Prompt the output path if it hasn't been set yet\n        if (string.IsNullOrWhiteSpace(OutputPath))\n        {\n            await ShowOutputPathPromptAsync();\n\n            // If the output path is still not set, cancel the export\n            if (string.IsNullOrWhiteSpace(OutputPath))\n                return;\n        }\n\n        // Persist preferences\n        settingsService.LastExportFormat = SelectedFormat;\n        settingsService.LastPartitionLimitValue = PartitionLimitValue;\n        settingsService.LastMessageFilterValue = MessageFilterValue;\n        settingsService.LastIsReverseMessageOrder = IsReverseMessageOrder;\n        settingsService.LastShouldFormatMarkdown = ShouldFormatMarkdown;\n        settingsService.LastShouldDownloadAssets = ShouldDownloadAssets;\n        settingsService.LastShouldReuseAssets = ShouldReuseAssets;\n        settingsService.LastAssetsDirPath = AssetsDirPath;\n\n        Close(true);\n    }\n}\n"
  },
  {
    "path": "DiscordChatExporter.Gui/ViewModels/Dialogs/MessageBoxViewModel.cs",
    "content": "﻿using CommunityToolkit.Mvvm.ComponentModel;\nusing DiscordChatExporter.Gui.Framework;\n\nnamespace DiscordChatExporter.Gui.ViewModels.Dialogs;\n\npublic partial class MessageBoxViewModel : DialogViewModelBase\n{\n    [ObservableProperty]\n    public partial string? Title { get; set; } = \"Title\";\n\n    [ObservableProperty]\n    public partial string? Message { get; set; } = \"Message\";\n\n    [ObservableProperty]\n    [NotifyPropertyChangedFor(nameof(IsDefaultButtonVisible))]\n    [NotifyPropertyChangedFor(nameof(ButtonsCount))]\n    public partial string? DefaultButtonText { get; set; } = \"OK\";\n\n    [ObservableProperty]\n    [NotifyPropertyChangedFor(nameof(IsCancelButtonVisible))]\n    [NotifyPropertyChangedFor(nameof(ButtonsCount))]\n    public partial string? CancelButtonText { get; set; } = \"Cancel\";\n\n    public bool IsDefaultButtonVisible => !string.IsNullOrWhiteSpace(DefaultButtonText);\n\n    public bool IsCancelButtonVisible => !string.IsNullOrWhiteSpace(CancelButtonText);\n\n    public int ButtonsCount => (IsDefaultButtonVisible ? 1 : 0) + (IsCancelButtonVisible ? 1 : 0);\n}\n"
  },
  {
    "path": "DiscordChatExporter.Gui/ViewModels/Dialogs/SettingsViewModel.cs",
    "content": "﻿using System;\nusing System.Collections.Generic;\nusing DiscordChatExporter.Core.Discord;\nusing DiscordChatExporter.Core.Utils.Extensions;\nusing DiscordChatExporter.Gui.Framework;\nusing DiscordChatExporter.Gui.Localization;\nusing DiscordChatExporter.Gui.Models;\nusing DiscordChatExporter.Gui.Services;\nusing DiscordChatExporter.Gui.Utils;\nusing DiscordChatExporter.Gui.Utils.Extensions;\n\nnamespace DiscordChatExporter.Gui.ViewModels.Dialogs;\n\npublic class SettingsViewModel : DialogViewModelBase\n{\n    private readonly SettingsService _settingsService;\n\n    private readonly DisposableCollector _eventRoot = new();\n\n    public SettingsViewModel(\n        SettingsService settingsService,\n        LocalizationManager localizationManager\n    )\n    {\n        _settingsService = settingsService;\n        LocalizationManager = localizationManager;\n\n        _eventRoot.Add(_settingsService.WatchAllProperties(OnAllPropertiesChanged));\n    }\n\n    public LocalizationManager LocalizationManager { get; }\n\n    public IReadOnlyList<ThemeVariant> AvailableThemes { get; } = Enum.GetValues<ThemeVariant>();\n\n    public ThemeVariant Theme\n    {\n        get => _settingsService.Theme;\n        set => _settingsService.Theme = value;\n    }\n\n    public IReadOnlyList<Language> AvailableLanguages { get; } = Enum.GetValues<Language>();\n\n    public Language Language\n    {\n        get => _settingsService.Language;\n        set => _settingsService.Language = value;\n    }\n\n    public bool IsAutoUpdateEnabled\n    {\n        get => _settingsService.IsAutoUpdateEnabled;\n        set => _settingsService.IsAutoUpdateEnabled = value;\n    }\n\n    public bool IsTokenPersisted\n    {\n        get => _settingsService.IsTokenPersisted;\n        set => _settingsService.IsTokenPersisted = value;\n    }\n\n    public IReadOnlyList<RateLimitPreference> AvailableRateLimitPreferences { get; } =\n        Enum.GetValues<RateLimitPreference>();\n\n    public RateLimitPreference RateLimitPreference\n    {\n        get => _settingsService.RateLimitPreference;\n        set => _settingsService.RateLimitPreference = value;\n    }\n\n    public IReadOnlyList<ThreadInclusionMode> AvailableThreadInclusionModes { get; } =\n        Enum.GetValues<ThreadInclusionMode>();\n\n    public ThreadInclusionMode ThreadInclusionMode\n    {\n        get => _settingsService.ThreadInclusionMode;\n        set => _settingsService.ThreadInclusionMode = value;\n    }\n\n    // These items have to be non-nullable because Avalonia ComboBox doesn't allow a null value to be selected\n    public IReadOnlyList<string> AvailableLocales { get; } =\n    [\n        // Current locale (maps to null downstream)\n        \"\",\n        // Locales supported by the Discord app\n        \"da-DK\",\n        \"de-DE\",\n        \"en-GB\",\n        \"en-US\",\n        \"es-ES\",\n        \"fr-FR\",\n        \"hr-HR\",\n        \"it-IT\",\n        \"lt-LT\",\n        \"hu-HU\",\n        \"nl-NL\",\n        \"no-NO\",\n        \"pl-PL\",\n        \"pt-BR\",\n        \"ro-RO\",\n        \"fi-FI\",\n        \"sv-SE\",\n        \"vi-VN\",\n        \"tr-TR\",\n        \"cs-CZ\",\n        \"el-GR\",\n        \"bg-BG\",\n        \"ru-RU\",\n        \"uk-UA\",\n        \"th-TH\",\n        \"zh-CN\",\n        \"ja-JP\",\n        \"zh-TW\",\n        \"ko-KR\",\n    ];\n\n    // This has to be non-nullable because Avalonia ComboBox doesn't allow a null value to be selected\n    public string Locale\n    {\n        get => _settingsService.Locale ?? \"\";\n        // Important to reduce empty strings to nulls, because empty strings don't correspond to valid cultures\n        set => _settingsService.Locale = value.NullIfWhiteSpace();\n    }\n\n    public bool IsUtcNormalizationEnabled\n    {\n        get => _settingsService.IsUtcNormalizationEnabled;\n        set => _settingsService.IsUtcNormalizationEnabled = value;\n    }\n\n    public int ParallelLimit\n    {\n        get => _settingsService.ParallelLimit;\n        set => _settingsService.ParallelLimit = Math.Clamp(value, 1, 10);\n    }\n\n    protected override void Dispose(bool disposing)\n    {\n        if (disposing)\n        {\n            _eventRoot.Dispose();\n        }\n\n        base.Dispose(disposing);\n    }\n}\n"
  },
  {
    "path": "DiscordChatExporter.Gui/ViewModels/MainViewModel.cs",
    "content": "﻿using System;\nusing System.Diagnostics;\nusing System.Threading.Tasks;\nusing Avalonia;\nusing CommunityToolkit.Mvvm.Input;\nusing DiscordChatExporter.Gui.Framework;\nusing DiscordChatExporter.Gui.Localization;\nusing DiscordChatExporter.Gui.Services;\nusing DiscordChatExporter.Gui.Utils.Extensions;\nusing DiscordChatExporter.Gui.ViewModels.Components;\n\nnamespace DiscordChatExporter.Gui.ViewModels;\n\npublic partial class MainViewModel(\n    ViewModelManager viewModelManager,\n    DialogManager dialogManager,\n    SnackbarManager snackbarManager,\n    SettingsService settingsService,\n    UpdateService updateService,\n    LocalizationManager localizationManager\n) : ViewModelBase\n{\n    public string Title { get; } = $\"{Program.Name} v{Program.VersionString}\";\n\n    public DashboardViewModel Dashboard { get; } = viewModelManager.CreateDashboardViewModel();\n\n    private async Task ShowUkraineSupportMessageAsync()\n    {\n        if (!settingsService.IsUkraineSupportMessageEnabled)\n            return;\n\n        var dialog = viewModelManager.CreateMessageBoxViewModel(\n            localizationManager.UkraineSupportTitle,\n            localizationManager.UkraineSupportMessage,\n            localizationManager.LearnMoreButton,\n            localizationManager.CloseButton\n        );\n\n        // Disable this message in the future\n        settingsService.IsUkraineSupportMessageEnabled = false;\n        settingsService.Save();\n\n        if (await dialogManager.ShowDialogAsync(dialog) == true)\n            Process.StartShellExecute(\"https://tyrrrz.me/ukraine?source=discordchatexporter\");\n    }\n\n    private async Task ShowDevelopmentBuildMessageAsync()\n    {\n        if (!Program.IsDevelopmentBuild)\n            return;\n\n        // If debugging, the user is likely a developer\n        if (Debugger.IsAttached)\n            return;\n\n        var dialog = viewModelManager.CreateMessageBoxViewModel(\n            localizationManager.UnstableBuildTitle,\n            string.Format(localizationManager.UnstableBuildMessage, Program.Name),\n            localizationManager.SeeReleasesButton,\n            localizationManager.CloseButton\n        );\n\n        if (await dialogManager.ShowDialogAsync(dialog) == true)\n            Process.StartShellExecute(Program.ProjectReleasesUrl);\n    }\n\n    private async Task CheckForUpdatesAsync()\n    {\n        try\n        {\n            var updateVersion = await updateService.CheckForUpdatesAsync();\n            if (updateVersion is null)\n                return;\n\n            snackbarManager.Notify(\n                string.Format(\n                    localizationManager.UpdateDownloadingMessage,\n                    Program.Name,\n                    updateVersion\n                )\n            );\n            await updateService.PrepareUpdateAsync(updateVersion);\n\n            snackbarManager.Notify(\n                localizationManager.UpdateReadyMessage,\n                localizationManager.UpdateInstallNowButton,\n                () =>\n                {\n                    updateService.FinalizeUpdate(true);\n\n                    if (Application.Current?.ApplicationLifetime?.TryShutdown(2) != true)\n                        Environment.Exit(2);\n                }\n            );\n        }\n        catch\n        {\n            // Failure to update shouldn't crash the application\n            snackbarManager.Notify(localizationManager.UpdateFailedMessage);\n        }\n    }\n\n    [RelayCommand]\n    private async Task InitializeAsync()\n    {\n        await ShowUkraineSupportMessageAsync();\n        await ShowDevelopmentBuildMessageAsync();\n        await CheckForUpdatesAsync();\n    }\n\n    protected override void Dispose(bool disposing)\n    {\n        if (disposing)\n        {\n            // Save settings\n            settingsService.Save();\n\n            // Finalize pending updates\n            updateService.FinalizeUpdate(false);\n        }\n\n        base.Dispose(disposing);\n    }\n}\n"
  },
  {
    "path": "DiscordChatExporter.Gui/Views/Components/DashboardView.axaml",
    "content": "﻿<UserControl\n    x:Class=\"DiscordChatExporter.Gui.Views.Components.DashboardView\"\n    xmlns=\"https://github.com/avaloniaui\"\n    xmlns:x=\"http://schemas.microsoft.com/winfx/2006/xaml\"\n    xmlns:asyncImageLoader=\"clr-namespace:AsyncImageLoader;assembly=AsyncImageLoader.Avalonia\"\n    xmlns:components=\"clr-namespace:DiscordChatExporter.Gui.ViewModels.Components\"\n    xmlns:converters=\"clr-namespace:DiscordChatExporter.Gui.Converters\"\n    xmlns:materialIcons=\"clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia\"\n    xmlns:materialStyles=\"clr-namespace:Material.Styles.Controls;assembly=Material.Styles\"\n    x:Name=\"UserControl\"\n    x:DataType=\"components:DashboardViewModel\"\n    Loaded=\"UserControl_OnLoaded\">\n    <DockPanel>\n        <!--  Header  -->\n        <StackPanel\n            Background=\"{DynamicResource MaterialDarkBackgroundBrush}\"\n            DockPanel.Dock=\"Top\"\n            Orientation=\"Vertical\">\n            <Grid Margin=\"12,12,8,12\" ColumnDefinitions=\"*,Auto\">\n                <materialStyles:Card Grid.Column=\"0\">\n                    <!--  Token  -->\n                    <TextBox\n                        x:Name=\"TokenValueTextBox\"\n                        FontSize=\"16\"\n                        PasswordChar=\"*\"\n                        RevealPassword=\"{Binding $self.IsFocused}\"\n                        Text=\"{Binding Token}\"\n                        Theme=\"{DynamicResource SoloTextBox}\"\n                        Watermark=\"{Binding LocalizationManager.TokenWatermark}\">\n                        <TextBox.InnerLeftContent>\n                            <materialIcons:MaterialIcon\n                                Grid.Column=\"0\"\n                                Width=\"24\"\n                                Height=\"24\"\n                                Margin=\"4,0,8,0\"\n                                Foreground=\"{DynamicResource PrimaryHueMidBrush}\"\n                                Kind=\"Key\" />\n                        </TextBox.InnerLeftContent>\n                        <TextBox.InnerRightContent>\n                            <Button\n                                Grid.Column=\"2\"\n                                Margin=\"8,0,0,0\"\n                                Padding=\"4\"\n                                Command=\"{Binding PullGuildsCommand}\"\n                                IsDefault=\"True\"\n                                Theme=\"{DynamicResource MaterialFlatButton}\"\n                                ToolTip.Tip=\"{Binding LocalizationManager.PullGuildsTooltip}\">\n                                <materialIcons:MaterialIcon\n                                    Width=\"24\"\n                                    Height=\"24\"\n                                    Kind=\"ArrowRight\" />\n                            </Button>\n                        </TextBox.InnerRightContent>\n                    </TextBox>\n                </materialStyles:Card>\n\n                <!--  Settings button  -->\n                <Button\n                    Grid.Column=\"1\"\n                    Margin=\"8,0,0,0\"\n                    Padding=\"8\"\n                    VerticalAlignment=\"Center\"\n                    Command=\"{Binding ShowSettingsCommand}\"\n                    Foreground=\"{DynamicResource MaterialDarkForegroundBrush}\"\n                    Theme=\"{DynamicResource MaterialFlatButton}\"\n                    ToolTip.Tip=\"{Binding LocalizationManager.SettingsTooltip}\">\n                    <materialIcons:MaterialIcon\n                        Width=\"24\"\n                        Height=\"24\"\n                        Kind=\"Settings\" />\n                </Button>\n            </Grid>\n\n            <!--  Progress  -->\n            <ProgressBar\n                Height=\"2\"\n                Background=\"Transparent\"\n                IsIndeterminate=\"{Binding IsProgressIndeterminate}\"\n                Value=\"{Binding Progress.Current.Fraction, Mode=OneWay}\" />\n        </StackPanel>\n\n        <!--  Body  -->\n        <Panel\n            Background=\"{DynamicResource MaterialCardBackgroundBrush}\"\n            DockPanel.Dock=\"Bottom\"\n            IsEnabled=\"{Binding !IsBusy}\">\n            <Panel.Styles>\n                <Style Selector=\"Panel\">\n                    <Style Selector=\"^:disabled\">\n                        <Setter Property=\"Opacity\" Value=\"0.5\" />\n                    </Style>\n                </Style>\n            </Panel.Styles>\n            <!--  Guilds and channels  -->\n            <Grid ColumnDefinitions=\"Auto,*\" IsVisible=\"{Binding !!AvailableGuilds.Count}\">\n                <!--  Guilds  -->\n                <Border\n                    Grid.Column=\"0\"\n                    BorderBrush=\"{DynamicResource MaterialDividerBrush}\"\n                    BorderThickness=\"0,0,1,0\">\n                    <ListBox\n                        x:Name=\"AvailableGuildsListBox\"\n                        ItemsSource=\"{Binding AvailableGuilds}\"\n                        ScrollViewer.VerticalScrollBarVisibility=\"Hidden\"\n                        SelectedItem=\"{Binding SelectedGuild}\"\n                        SelectionChanged=\"AvailableGuildsListBox_OnSelectionChanged\"\n                        SelectionMode=\"Single\">\n                        <ListBox.Styles>\n                            <Style Selector=\"ListBox\">\n                                <Style Selector=\"^ ListBoxItem\">\n                                    <Setter Property=\"Padding\" Value=\"0\" />\n                                    <Setter Property=\"Cursor\" Value=\"Hand\" />\n                                </Style>\n                            </Style>\n                        </ListBox.Styles>\n                        <ListBox.ItemTemplate>\n                            <DataTemplate>\n                                <Panel Background=\"Transparent\" ToolTip.Tip=\"{Binding Name}\">\n                                    <!--  Guild icon placeholder  -->\n                                    <Ellipse\n                                        Width=\"48\"\n                                        Height=\"48\"\n                                        Margin=\"12\"\n                                        Fill=\"{DynamicResource MaterialDividerBrush}\" />\n\n                                    <!--  Guild icon  -->\n                                    <Ellipse\n                                        Width=\"48\"\n                                        Height=\"48\"\n                                        Margin=\"12\">\n                                        <Ellipse.Fill>\n                                            <ImageBrush asyncImageLoader:ImageBrushLoader.Source=\"{Binding IconUrl}\" />\n                                        </Ellipse.Fill>\n                                    </Ellipse>\n                                </Panel>\n                            </DataTemplate>\n                        </ListBox.ItemTemplate>\n                    </ListBox>\n                </Border>\n\n                <!--  Channels  -->\n                <Border Grid.Column=\"1\">\n                    <TreeView\n                        x:Name=\"AvailableChannelsTreeView\"\n                        ItemsSource=\"{Binding AvailableChannels}\"\n                        SelectedItems=\"{Binding SelectedChannels}\"\n                        SelectionChanged=\"AvailableChannelsTreeView_OnSelectionChanged\"\n                        SelectionMode=\"Multiple\"\n                        TextSearch.Text=\"Name\">\n                        <TreeView.Styles>\n                            <Style Selector=\"TreeView\">\n                                <Style Selector=\"^ TreeViewItem\">\n                                    <Setter Property=\"Padding\" Value=\"0\" />\n                                    <Setter Property=\"Cursor\" Value=\"Hand\" />\n                                </Style>\n                            </Style>\n                        </TreeView.Styles>\n                        <TreeView.ItemTemplate>\n                            <TreeDataTemplate ItemsSource=\"{Binding Children}\">\n                                <Grid\n                                    Background=\"Transparent\"\n                                    Classes.category=\"{Binding Channel.IsCategory}\"\n                                    ColumnDefinitions=\"Auto,*,Auto\"\n                                    DoubleTapped=\"ChannelGrid_OnDoubleTapped\">\n                                    <Grid.Styles>\n                                        <Style Selector=\"Grid\">\n                                            <Style Selector=\"^:not(.category)\">\n                                                <Setter Property=\"ToolTip.Tip\">\n                                                    <Template>\n                                                        <TextBlock>\n                                                            <Run Text=\"{Binding #UserControl.DataContext.LocalizationManager.LastMessageSentTooltip}\" />\n                                                            <Run FontWeight=\"SemiBold\" Text=\"{Binding Channel.LastMessageId, Converter={x:Static converters:SnowflakeToTimestampStringConverter.Instance}, TargetNullValue=never, Mode=OneWay}\" />\n                                                        </TextBlock>\n                                                    </Template>\n                                                </Setter>\n                                            </Style>\n                                        </Style>\n                                    </Grid.Styles>\n\n                                    <!--  Channel icon  -->\n                                    <materialIcons:MaterialIcon\n                                        Grid.Column=\"0\"\n                                        Margin=\"0,0,4,0\"\n                                        Classes.voice=\"{Binding Channel.IsVoice}\"\n                                        IsVisible=\"{Binding !Channel.IsCategory}\">\n                                        <materialIcons:MaterialIcon.Styles>\n                                            <Style Selector=\"materialIcons|MaterialIcon\">\n                                                <Setter Property=\"Kind\" Value=\"Pound\" />\n\n                                                <Style Selector=\"^.voice\">\n                                                    <Setter Property=\"Kind\" Value=\"VolumeHigh\" />\n                                                </Style>\n                                            </Style>\n                                        </materialIcons:MaterialIcon.Styles>\n                                    </materialIcons:MaterialIcon>\n\n                                    <!--  Channel name  -->\n                                    <TextBlock\n                                        Grid.Column=\"1\"\n                                        Margin=\"0,12\"\n                                        FontSize=\"14\"\n                                        Text=\"{Binding Channel.Name, Mode=OneWay}\" />\n\n                                    <!--  Checkmark  -->\n                                    <materialIcons:MaterialIcon\n                                        Grid.Column=\"2\"\n                                        Width=\"24\"\n                                        Height=\"24\"\n                                        Margin=\"16,0\"\n                                        IsVisible=\"{Binding $parent[TreeViewItem].IsSelected}\"\n                                        Kind=\"Check\" />\n                                </Grid>\n                            </TreeDataTemplate>\n                        </TreeView.ItemTemplate>\n                    </TreeView>\n                </Border>\n            </Grid>\n\n            <!--  Placeholder / usage instructions  -->\n            <Panel IsVisible=\"{Binding !AvailableGuilds.Count}\">\n                <ScrollViewer HorizontalScrollBarVisibility=\"Disabled\" VerticalScrollBarVisibility=\"Auto\">\n                    <StackPanel Margin=\"32,16\" Spacing=\"0\">\n                        <!--  User token  -->\n                        <TextBlock>\n                            <InlineUIContainer>\n                                <materialIcons:MaterialIcon\n                                    Width=\"18\"\n                                    Height=\"18\"\n                                    Foreground=\"{DynamicResource PrimaryHueMidBrush}\"\n                                    Kind=\"Account\" />\n                            </InlineUIContainer>\n                            <Run Text=\"\" />\n                            <Run\n                                FontSize=\"16\"\n                                FontWeight=\"SemiBold\"\n                                Text=\"{Binding LocalizationManager.TokenPersonalHeader}\" />\n                        </TextBlock>\n\n                        <TextBlock\n                            FontSize=\"14\"\n                            FontWeight=\"Light\"\n                            Inlines=\"{Binding LocalizationManager.TokenPersonalTosWarning, Converter={x:Static converters:MarkdownToInlinesConverter.Instance}}\"\n                            LineHeight=\"23\"\n                            TextWrapping=\"Wrap\" />\n\n                        <TextBlock\n                            FontSize=\"14\"\n                            FontWeight=\"Light\"\n                            Inlines=\"{Binding LocalizationManager.TokenPersonalInstructions, Converter={x:Static converters:MarkdownToInlinesConverter.Instance}}\"\n                            LineHeight=\"23\"\n                            TextWrapping=\"Wrap\" />\n\n                        <!--  Bot token  -->\n                        <TextBlock Margin=\"0,12,0,0\">\n                            <InlineUIContainer>\n                                <materialIcons:MaterialIcon\n                                    Width=\"18\"\n                                    Height=\"18\"\n                                    Foreground=\"{DynamicResource PrimaryHueMidBrush}\"\n                                    Kind=\"Robot\" />\n                            </InlineUIContainer>\n                            <Run Text=\"\" />\n                            <Run\n                                FontSize=\"16\"\n                                FontWeight=\"SemiBold\"\n                                Text=\"{Binding LocalizationManager.TokenBotHeader}\" />\n                        </TextBlock>\n\n                        <TextBlock\n                            FontSize=\"14\"\n                            FontWeight=\"Light\"\n                            Inlines=\"{Binding LocalizationManager.TokenBotInstructions, Converter={x:Static converters:MarkdownToInlinesConverter.Instance}}\"\n                            LineHeight=\"23\"\n                            TextWrapping=\"Wrap\" />\n\n                        <TextBlock\n                            Margin=\"0,12,0,0\"\n                            FontSize=\"14\"\n                            FontWeight=\"Light\"\n                            Inlines=\"{Binding LocalizationManager.TokenHelpText, Converter={x:Static converters:MarkdownToInlinesConverter.Instance}}\"\n                            LineHeight=\"23\"\n                            TextWrapping=\"Wrap\" />\n                    </StackPanel>\n                </ScrollViewer>\n            </Panel>\n\n            <!--  Export button  -->\n            <Button\n                Width=\"56\"\n                Height=\"56\"\n                Margin=\"32,24\"\n                Padding=\"0\"\n                HorizontalAlignment=\"Right\"\n                VerticalAlignment=\"Bottom\"\n                Background=\"{DynamicResource MaterialSecondaryMidBrush}\"\n                Command=\"{Binding ExportCommand}\"\n                Foreground=\"{DynamicResource MaterialSecondaryMidForegroundBrush}\"\n                IsVisible=\"{Binding $self.IsEffectivelyEnabled}\"\n                Theme=\"{DynamicResource MaterialIconButton}\">\n                <materialIcons:MaterialIcon\n                    Width=\"32\"\n                    Height=\"32\"\n                    Kind=\"Download\" />\n            </Button>\n        </Panel>\n    </DockPanel>\n</UserControl>"
  },
  {
    "path": "DiscordChatExporter.Gui/Views/Components/DashboardView.axaml.cs",
    "content": "﻿using System.Linq;\nusing Avalonia.Controls;\nusing Avalonia.Input;\nusing Avalonia.Interactivity;\nusing DiscordChatExporter.Core.Discord.Data;\nusing DiscordChatExporter.Gui.Framework;\nusing DiscordChatExporter.Gui.ViewModels.Components;\n\nnamespace DiscordChatExporter.Gui.Views.Components;\n\npublic partial class DashboardView : UserControl<DashboardViewModel>\n{\n    public DashboardView() => InitializeComponent();\n\n    private void UserControl_OnLoaded(object? sender, RoutedEventArgs args)\n    {\n        DataContext.InitializeCommand.Execute(null);\n        TokenValueTextBox.Focus();\n    }\n\n    private void AvailableGuildsListBox_OnSelectionChanged(\n        object? sender,\n        SelectionChangedEventArgs args\n    ) => DataContext.PullChannelsCommand.Execute(null);\n\n    private void AvailableChannelsTreeView_OnSelectionChanged(\n        object? sender,\n        SelectionChangedEventArgs args\n    )\n    {\n        // Hack: unselect categories because they cannot be exported\n        foreach (\n            var item in args.AddedItems.OfType<ChannelConnection>().Where(x => x.Channel.IsCategory)\n        )\n        {\n            if (AvailableChannelsTreeView.TreeContainerFromItem(item) is TreeViewItem container)\n                container.IsSelected = false;\n        }\n    }\n\n    private void ChannelGrid_OnDoubleTapped(object? sender, TappedEventArgs args)\n    {\n        if (DataContext.SelectedChannels.Count != 1)\n            return;\n\n        DataContext.ExportCommand.Execute(null);\n    }\n}\n"
  },
  {
    "path": "DiscordChatExporter.Gui/Views/Controls/HyperLink.axaml",
    "content": "﻿<UserControl\n    x:Class=\"DiscordChatExporter.Gui.Views.Controls.HyperLink\"\n    xmlns=\"https://github.com/avaloniaui\"\n    xmlns:x=\"http://schemas.microsoft.com/winfx/2006/xaml\"\n    xmlns:controls=\"clr-namespace:DiscordChatExporter.Gui.Views.Controls\">\n    <TextBlock\n        x:Name=\"TextBlock\"\n        Cursor=\"Hand\"\n        Foreground=\"{DynamicResource MaterialSecondaryDarkBrush}\"\n        PointerReleased=\"TextBlock_OnPointerReleased\"\n        Text=\"{Binding $parent[controls:HyperLink].Text, Mode=OneWay}\">\n        <TextBlock.Styles>\n            <Style Selector=\"TextBlock\">\n                <Style Selector=\"^:pointerover\">\n                    <Setter Property=\"TextDecorations\" Value=\"Underline\" />\n                </Style>\n            </Style>\n        </TextBlock.Styles>\n    </TextBlock>\n</UserControl>"
  },
  {
    "path": "DiscordChatExporter.Gui/Views/Controls/HyperLink.axaml.cs",
    "content": "﻿using System.Diagnostics;\nusing System.Windows.Input;\nusing Avalonia;\nusing Avalonia.Controls;\nusing Avalonia.Input;\nusing DiscordChatExporter.Gui.Utils.Extensions;\n\nnamespace DiscordChatExporter.Gui.Views.Controls;\n\npublic partial class HyperLink : UserControl\n{\n    public static readonly StyledProperty<string?> TextProperty =\n        TextBlock.TextProperty.AddOwner<HyperLink>();\n\n    public static readonly StyledProperty<ICommand?> CommandProperty =\n        Button.CommandProperty.AddOwner<HyperLink>();\n\n    public static readonly StyledProperty<object?> CommandParameterProperty =\n        Button.CommandParameterProperty.AddOwner<HyperLink>();\n\n    // If Url is set and Command is not set, clicking will open this URL in the default browser.\n    public static readonly StyledProperty<string?> UrlProperty = AvaloniaProperty.Register<\n        HyperLink,\n        string?\n    >(nameof(Url));\n\n    public HyperLink() => InitializeComponent();\n\n    public string? Text\n    {\n        get => GetValue(TextProperty);\n        set => SetValue(TextProperty, value);\n    }\n\n    public ICommand? Command\n    {\n        get => GetValue(CommandProperty);\n        set => SetValue(CommandProperty, value);\n    }\n\n    public object? CommandParameter\n    {\n        get => GetValue(CommandParameterProperty);\n        set => SetValue(CommandParameterProperty, value);\n    }\n\n    public string? Url\n    {\n        get => GetValue(UrlProperty);\n        set => SetValue(UrlProperty, value);\n    }\n\n    private void TextBlock_OnPointerReleased(object? sender, PointerReleasedEventArgs args)\n    {\n        if (Command is not null)\n        {\n            if (Command.CanExecute(CommandParameter))\n                Command.Execute(CommandParameter);\n        }\n        else if (!string.IsNullOrWhiteSpace(Url))\n        {\n            Process.StartShellExecute(Url);\n        }\n    }\n}\n"
  },
  {
    "path": "DiscordChatExporter.Gui/Views/Dialogs/ExportSetupView.axaml",
    "content": "﻿<UserControl\n    x:Class=\"DiscordChatExporter.Gui.Views.Dialogs.ExportSetupView\"\n    xmlns=\"https://github.com/avaloniaui\"\n    xmlns:x=\"http://schemas.microsoft.com/winfx/2006/xaml\"\n    xmlns:asyncImageLoader=\"clr-namespace:AsyncImageLoader;assembly=AsyncImageLoader.Avalonia\"\n    xmlns:converters=\"clr-namespace:DiscordChatExporter.Gui.Converters\"\n    xmlns:dialogs=\"clr-namespace:DiscordChatExporter.Gui.ViewModels.Dialogs\"\n    xmlns:materialAssists=\"clr-namespace:Material.Styles.Assists;assembly=Material.Styles\"\n    xmlns:materialIcons=\"clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia\"\n    xmlns:utils=\"clr-namespace:DiscordChatExporter.Gui.Utils\"\n    x:Name=\"UserControl\"\n    Width=\"380\"\n    x:DataType=\"dialogs:ExportSetupViewModel\"\n    Loaded=\"UserControl_OnLoaded\">\n    <Grid RowDefinitions=\"Auto,*,Auto\">\n        <!--  Guild/channel info  -->\n        <Grid\n            Grid.Row=\"0\"\n            Margin=\"16\"\n            ColumnDefinitions=\"Auto,*\">\n            <!--  Guild icon  -->\n            <Ellipse\n                Grid.Column=\"0\"\n                Width=\"32\"\n                Height=\"32\">\n                <Ellipse.Fill>\n                    <ImageBrush asyncImageLoader:ImageBrushLoader.Source=\"{Binding Guild.IconUrl}\" />\n                </Ellipse.Fill>\n            </Ellipse>\n\n            <!--  Channel count (for multiple channels)  -->\n            <TextBlock\n                Grid.Column=\"1\"\n                Margin=\"8,0,0,0\"\n                VerticalAlignment=\"Center\"\n                FontSize=\"19\"\n                FontWeight=\"Light\"\n                IsVisible=\"{Binding !IsSingleChannel}\"\n                TextTrimming=\"CharacterEllipsis\">\n                <Run Text=\"{Binding Channels.Count, FallbackValue=0, Mode=OneWay}\" />\n                <Run Text=\"{Binding LocalizationManager.ChannelsSelectedText}\" />\n            </TextBlock>\n\n            <!--  Category and channel name (for single channel)  -->\n            <TextBlock\n                Grid.Column=\"1\"\n                Margin=\"8,0,0,0\"\n                VerticalAlignment=\"Center\"\n                FontSize=\"19\"\n                FontWeight=\"Light\"\n                IsVisible=\"{Binding IsSingleChannel}\"\n                TextTrimming=\"CharacterEllipsis\"\n                ToolTip.Tip=\"{Binding Channels[0], Converter={x:Static converters:ChannelToHierarchicalNameStringConverter.Instance}}\">\n                <Run Text=\"{Binding Channels[0].Parent.Name, StringFormat='{}{0} / ', FallbackValue='', TargetNullValue='', Mode=OneWay}\" /><Run FontWeight=\"SemiBold\" Text=\"{Binding Channels[0].Name, Mode=OneWay}\" />\n            </TextBlock>\n        </Grid>\n\n        <Border\n            Grid.Row=\"1\"\n            Padding=\"0,8\"\n            BorderBrush=\"{DynamicResource MaterialDividerBrush}\"\n            BorderThickness=\"0,1\">\n            <ScrollViewer HorizontalScrollBarVisibility=\"Disabled\" VerticalScrollBarVisibility=\"Auto\">\n                <StackPanel Orientation=\"Vertical\">\n                    <!--  Output path  -->\n                    <TextBox\n                        Margin=\"16,8\"\n                        materialAssists:TextFieldAssist.Label=\"{Binding LocalizationManager.OutputPathLabel}\"\n                        Text=\"{Binding OutputPath}\"\n                        Theme=\"{DynamicResource FilledTextBox}\">\n                        <ToolTip.Tip>\n                            <TextBlock\n                                MaxWidth=\"400\"\n                                Inlines=\"{Binding LocalizationManager.OutputPathTooltip, Converter={x:Static converters:MarkdownToInlinesConverter.Instance}}\"\n                                TextWrapping=\"Wrap\" />\n                        </ToolTip.Tip>\n                        <TextBox.InnerRightContent>\n                            <Button\n                                Margin=\"8,8,8,6\"\n                                Padding=\"8\"\n                                VerticalAlignment=\"Center\"\n                                Command=\"{Binding ShowOutputPathPromptCommand}\"\n                                Theme=\"{DynamicResource MaterialFlatButton}\">\n                                <materialIcons:MaterialIcon\n                                    Width=\"20\"\n                                    Height=\"20\"\n                                    Kind=\"FolderOpen\" />\n                            </Button>\n                        </TextBox.InnerRightContent>\n                    </TextBox>\n\n                    <!--  Format  -->\n                    <ComboBox\n                        Margin=\"16,8\"\n                        materialAssists:ComboBoxAssist.Label=\"{Binding LocalizationManager.FormatLabel}\"\n                        ItemsSource=\"{Binding AvailableFormats}\"\n                        SelectedItem=\"{Binding SelectedFormat}\"\n                        Theme=\"{DynamicResource MaterialFilledComboBox}\"\n                        ToolTip.Tip=\"{Binding LocalizationManager.FormatTooltip}\">\n                        <ComboBox.ItemTemplate>\n                            <DataTemplate>\n                                <TextBlock Text=\"{Binding Converter={x:Static converters:ExportFormatToStringConverter.Instance}}\" />\n                            </DataTemplate>\n                        </ComboBox.ItemTemplate>\n                    </ComboBox>\n\n                    <!--  Advanced section  -->\n                    <StackPanel IsVisible=\"{Binding IsAdvancedSectionDisplayed}\" Orientation=\"Vertical\">\n                        <!--  Date limits  -->\n                        <Grid ColumnDefinitions=\"*,*\" RowDefinitions=\"*,*\">\n                            <DatePicker\n                                Grid.Row=\"0\"\n                                Grid.Column=\"0\"\n                                Margin=\"16,8,8,8\"\n                                materialAssists:TextFieldAssist.Label=\"{Binding LocalizationManager.AfterDateLabel}\"\n                                SelectedDate=\"{Binding AfterDate}\"\n                                ToolTip.Tip=\"{Binding LocalizationManager.AfterDateTooltip}\">\n                                <DatePicker.Styles>\n                                    <Style Selector=\"DatePicker\">\n                                        <Style Selector=\"^ /template/ TextBox#DisplayTextBox\">\n                                            <Setter Property=\"Theme\" Value=\"{DynamicResource FilledTextBox}\" />\n                                        </Style>\n                                    </Style>\n                                </DatePicker.Styles>\n                            </DatePicker>\n                            <DatePicker\n                                Grid.Row=\"0\"\n                                Grid.Column=\"1\"\n                                Margin=\"8,8,16,8\"\n                                materialAssists:TextFieldAssist.Label=\"{Binding LocalizationManager.BeforeDateLabel}\"\n                                SelectedDate=\"{Binding BeforeDate}\"\n                                ToolTip.Tip=\"{Binding LocalizationManager.BeforeDateTooltip}\">\n                                <DatePicker.Styles>\n                                    <Style Selector=\"DatePicker\">\n                                        <Style Selector=\"^ /template/ TextBox#DisplayTextBox\">\n                                            <Setter Property=\"Theme\" Value=\"{DynamicResource FilledTextBox}\" />\n                                        </Style>\n                                    </Style>\n                                </DatePicker.Styles>\n                            </DatePicker>\n\n                            <!--  Time limits  -->\n                            <TimePicker\n                                Grid.Row=\"1\"\n                                Grid.Column=\"0\"\n                                Margin=\"16,8,8,8\"\n                                materialAssists:TextFieldAssist.Label=\"{Binding LocalizationManager.AfterTimeLabel}\"\n                                ClockIdentifier=\"{x:Static utils:Internationalization.AvaloniaClockIdentifier}\"\n                                IsEnabled=\"{Binding IsAfterDateSet}\"\n                                SelectedTime=\"{Binding AfterTime}\"\n                                ToolTip.Tip=\"{Binding LocalizationManager.AfterTimeTooltip}\">\n                                <TimePicker.Styles>\n                                    <Style Selector=\"TimePicker\">\n                                        <Style Selector=\"^ /template/ TextBox#PART_DisplayTextBox\">\n                                            <Setter Property=\"Theme\" Value=\"{DynamicResource FilledTextBox}\" />\n                                        </Style>\n                                    </Style>\n                                </TimePicker.Styles>\n                            </TimePicker>\n                            <TimePicker\n                                Grid.Row=\"1\"\n                                Grid.Column=\"1\"\n                                Margin=\"8,8,16,8\"\n                                materialAssists:TextFieldAssist.Label=\"{Binding LocalizationManager.BeforeTimeLabel}\"\n                                ClockIdentifier=\"{x:Static utils:Internationalization.AvaloniaClockIdentifier}\"\n                                IsEnabled=\"{Binding IsBeforeDateSet}\"\n                                SelectedTime=\"{Binding BeforeTime}\"\n                                ToolTip.Tip=\"{Binding LocalizationManager.BeforeTimeTooltip}\">\n                                <TimePicker.Styles>\n                                    <Style Selector=\"TimePicker\">\n                                        <Style Selector=\"^ /template/ TextBox#PART_DisplayTextBox\">\n                                            <Setter Property=\"Theme\" Value=\"{DynamicResource FilledTextBox}\" />\n                                        </Style>\n                                    </Style>\n                                </TimePicker.Styles>\n                            </TimePicker>\n                        </Grid>\n\n                        <!--  Partitioning  -->\n                        <TextBox\n                            Margin=\"16,8\"\n                            materialAssists:TextFieldAssist.Label=\"{Binding LocalizationManager.PartitionLimitLabel}\"\n                            Text=\"{Binding PartitionLimitValue}\"\n                            Theme=\"{DynamicResource FilledTextBox}\"\n                            ToolTip.Tip=\"{Binding LocalizationManager.PartitionLimitTooltip}\" />\n\n                        <!--  Filtering  -->\n                        <TextBox\n                            Margin=\"16,8\"\n                            materialAssists:TextFieldAssist.Label=\"{Binding LocalizationManager.MessageFilterLabel}\"\n                            Text=\"{Binding MessageFilterValue}\"\n                            Theme=\"{DynamicResource FilledTextBox}\"\n                            ToolTip.Tip=\"{Binding LocalizationManager.MessageFilterTooltip}\" />\n\n                        <!--  Reverse message order  -->\n                        <DockPanel\n                            Margin=\"16,8\"\n                            LastChildFill=\"False\"\n                            ToolTip.Tip=\"{Binding LocalizationManager.ReverseMessageOrderTooltip}\">\n                            <TextBlock DockPanel.Dock=\"Left\" Text=\"{Binding LocalizationManager.ReverseMessageOrderLabel}\" />\n                            <ToggleSwitch DockPanel.Dock=\"Right\" IsChecked=\"{Binding IsReverseMessageOrder}\" />\n                        </DockPanel>\n\n                        <!--  Markdown formatting  -->\n                        <DockPanel\n                            Margin=\"16,8\"\n                            LastChildFill=\"False\"\n                            ToolTip.Tip=\"{Binding LocalizationManager.FormatMarkdownTooltip}\">\n                            <TextBlock DockPanel.Dock=\"Left\" Text=\"{Binding LocalizationManager.FormatMarkdownLabel}\" />\n                            <ToggleSwitch DockPanel.Dock=\"Right\" IsChecked=\"{Binding ShouldFormatMarkdown}\" />\n                        </DockPanel>\n\n                        <!--  Download assets  -->\n                        <DockPanel\n                            Margin=\"16,8\"\n                            LastChildFill=\"False\"\n                            ToolTip.Tip=\"{Binding LocalizationManager.DownloadAssetsTooltip}\">\n                            <TextBlock DockPanel.Dock=\"Left\" Text=\"{Binding LocalizationManager.DownloadAssetsLabel}\" />\n                            <ToggleSwitch DockPanel.Dock=\"Right\" IsChecked=\"{Binding ShouldDownloadAssets}\" />\n                        </DockPanel>\n\n                        <!--  Reuse assets  -->\n                        <DockPanel\n                            Margin=\"16,8\"\n                            IsEnabled=\"{Binding ShouldDownloadAssets}\"\n                            LastChildFill=\"False\"\n                            ToolTip.Tip=\"{Binding LocalizationManager.ReuseAssetsTooltip}\">\n                            <TextBlock DockPanel.Dock=\"Left\" Text=\"{Binding LocalizationManager.ReuseAssetsLabel}\" />\n                            <ToggleSwitch DockPanel.Dock=\"Right\" IsChecked=\"{Binding ShouldReuseAssets}\" />\n                        </DockPanel>\n\n                        <!--  Assets path  -->\n                        <TextBox\n                            Margin=\"16,8\"\n                            materialAssists:TextFieldAssist.Label=\"{Binding LocalizationManager.AssetsDirPathLabel}\"\n                            IsEnabled=\"{Binding ShouldDownloadAssets}\"\n                            Text=\"{Binding AssetsDirPath}\"\n                            Theme=\"{DynamicResource FilledTextBox}\"\n                            ToolTip.Tip=\"{Binding LocalizationManager.AssetsDirPathTooltip}\">\n                            <TextBox.InnerRightContent>\n                                <Button\n                                    Margin=\"8,8,8,6\"\n                                    Padding=\"8\"\n                                    VerticalAlignment=\"Center\"\n                                    Command=\"{Binding ShowAssetsDirPathPromptCommand}\"\n                                    Theme=\"{DynamicResource MaterialFlatButton}\">\n                                    <materialIcons:MaterialIcon\n                                        Width=\"20\"\n                                        Height=\"20\"\n                                        Kind=\"FolderOpen\" />\n                                </Button>\n                            </TextBox.InnerRightContent>\n                        </TextBox>\n                    </StackPanel>\n                </StackPanel>\n            </ScrollViewer>\n        </Border>\n\n        <!--  Buttons  -->\n        <Grid\n            Grid.Row=\"2\"\n            Margin=\"16\"\n            ColumnDefinitions=\"Auto,*,Auto,Auto\">\n            <ToggleButton\n                Grid.Column=\"0\"\n                IsChecked=\"{Binding IsAdvancedSectionDisplayed}\"\n                Theme=\"{DynamicResource MaterialOutlineButton}\"\n                ToolTip.Tip=\"{Binding LocalizationManager.AdvancedOptionsTooltip}\">\n                <Button.Styles>\n                    <Style Selector=\"ToggleButton\">\n                        <Setter Property=\"Content\" Value=\"MORE\" />\n\n                        <Style Selector=\"^:checked\">\n                            <Setter Property=\"Content\" Value=\"LESS\" />\n                        </Style>\n                    </Style>\n                </Button.Styles>\n            </ToggleButton>\n\n            <Button\n                Grid.Column=\"2\"\n                Command=\"{Binding ConfirmCommand}\"\n                Content=\"{Binding LocalizationManager.ExportButton}\"\n                IsDefault=\"True\"\n                Theme=\"{DynamicResource MaterialOutlineButton}\" />\n            <Button\n                Grid.Column=\"3\"\n                Margin=\"16,0,0,0\"\n                Command=\"{Binding CloseCommand}\"\n                Content=\"{Binding LocalizationManager.CancelButton}\"\n                IsCancel=\"True\"\n                Theme=\"{DynamicResource MaterialOutlineButton}\" />\n        </Grid>\n    </Grid>\n</UserControl>"
  },
  {
    "path": "DiscordChatExporter.Gui/Views/Dialogs/ExportSetupView.axaml.cs",
    "content": "using Avalonia.Interactivity;\nusing DiscordChatExporter.Gui.Framework;\nusing DiscordChatExporter.Gui.ViewModels.Dialogs;\n\nnamespace DiscordChatExporter.Gui.Views.Dialogs;\n\npublic partial class ExportSetupView : UserControl<ExportSetupViewModel>\n{\n    public ExportSetupView() => InitializeComponent();\n\n    private void UserControl_OnLoaded(object? sender, RoutedEventArgs args) =>\n        DataContext.InitializeCommand.Execute(null);\n}\n"
  },
  {
    "path": "DiscordChatExporter.Gui/Views/Dialogs/MessageBoxView.axaml",
    "content": "﻿<UserControl\n    x:Class=\"DiscordChatExporter.Gui.Views.Dialogs.MessageBoxView\"\n    xmlns=\"https://github.com/avaloniaui\"\n    xmlns:x=\"http://schemas.microsoft.com/winfx/2006/xaml\"\n    xmlns:dialogs=\"clr-namespace:DiscordChatExporter.Gui.ViewModels.Dialogs\"\n    xmlns:system=\"clr-namespace:System;assembly=System.Runtime\"\n    Width=\"500\"\n    x:DataType=\"dialogs:MessageBoxViewModel\">\n    <Grid RowDefinitions=\"Auto,*,Auto\">\n        <!--  Title  -->\n        <TextBlock\n            Grid.Row=\"0\"\n            Margin=\"16\"\n            FontSize=\"19\"\n            FontWeight=\"Light\"\n            Text=\"{Binding Title}\"\n            TextTrimming=\"CharacterEllipsis\"\n            ToolTip.Tip=\"{Binding Title}\" />\n\n        <!--  Message  -->\n        <Border\n            Grid.Row=\"1\"\n            Padding=\"0,8\"\n            BorderBrush=\"{DynamicResource MaterialDividerBrush}\"\n            BorderThickness=\"0,1\">\n            <ScrollViewer HorizontalScrollBarVisibility=\"Disabled\" VerticalScrollBarVisibility=\"Auto\">\n                <TextBlock\n                    Margin=\"16,8\"\n                    Text=\"{Binding Message}\"\n                    TextWrapping=\"Wrap\" />\n            </ScrollViewer>\n        </Border>\n\n        <UniformGrid\n            Grid.Row=\"2\"\n            Margin=\"8\"\n            Columns=\"{Binding ButtonsCount}\">\n            <!--  OK  -->\n            <Button\n                Margin=\"8\"\n                HorizontalContentAlignment=\"Stretch\"\n                Command=\"{Binding CloseCommand}\"\n                IsDefault=\"True\"\n                IsVisible=\"{Binding IsDefaultButtonVisible}\"\n                Theme=\"{DynamicResource MaterialOutlineButton}\"\n                ToolTip.Tip=\"{Binding DefaultButtonText}\">\n                <Button.CommandParameter>\n                    <system:Boolean>True</system:Boolean>\n                </Button.CommandParameter>\n                <TextBlock\n                    Text=\"{Binding DefaultButtonText}\"\n                    TextAlignment=\"Center\"\n                    TextTrimming=\"CharacterEllipsis\" />\n            </Button>\n\n            <!--  Cancel  -->\n            <Button\n                Margin=\"8\"\n                HorizontalContentAlignment=\"Stretch\"\n                Command=\"{Binding CloseCommand}\"\n                IsCancel=\"True\"\n                IsVisible=\"{Binding IsCancelButtonVisible}\"\n                Theme=\"{DynamicResource MaterialOutlineButton}\"\n                ToolTip.Tip=\"{Binding CancelButtonText}\">\n                <TextBlock\n                    Text=\"{Binding CancelButtonText}\"\n                    TextAlignment=\"Center\"\n                    TextTrimming=\"CharacterEllipsis\" />\n            </Button>\n        </UniformGrid>\n    </Grid>\n</UserControl>"
  },
  {
    "path": "DiscordChatExporter.Gui/Views/Dialogs/MessageBoxView.axaml.cs",
    "content": "﻿using DiscordChatExporter.Gui.Framework;\nusing DiscordChatExporter.Gui.ViewModels.Dialogs;\n\nnamespace DiscordChatExporter.Gui.Views.Dialogs;\n\npublic partial class MessageBoxView : UserControl<MessageBoxViewModel>\n{\n    public MessageBoxView() => InitializeComponent();\n}\n"
  },
  {
    "path": "DiscordChatExporter.Gui/Views/Dialogs/SettingsView.axaml",
    "content": "﻿<UserControl\n    x:Class=\"DiscordChatExporter.Gui.Views.Dialogs.SettingsView\"\n    xmlns=\"https://github.com/avaloniaui\"\n    xmlns:x=\"http://schemas.microsoft.com/winfx/2006/xaml\"\n    xmlns:converters=\"clr-namespace:DiscordChatExporter.Gui.Converters\"\n    xmlns:dialogs=\"clr-namespace:DiscordChatExporter.Gui.ViewModels.Dialogs\"\n    Width=\"380\"\n    x:DataType=\"dialogs:SettingsViewModel\">\n    <Grid RowDefinitions=\"Auto,*,Auto\">\n        <TextBlock\n            Grid.Row=\"0\"\n            Margin=\"16\"\n            FontSize=\"19\"\n            FontWeight=\"Light\"\n            Text=\"{Binding LocalizationManager.SettingsTitle}\" />\n\n        <Border\n            Grid.Row=\"1\"\n            Padding=\"0,8\"\n            BorderBrush=\"{DynamicResource MaterialDividerBrush}\"\n            BorderThickness=\"0,1\">\n            <ScrollViewer HorizontalScrollBarVisibility=\"Disabled\" VerticalScrollBarVisibility=\"Auto\">\n                <StackPanel Orientation=\"Vertical\">\n                    <!--  Theme  -->\n                    <DockPanel\n                        Margin=\"16,8\"\n                        LastChildFill=\"False\"\n                        ToolTip.Tip=\"{Binding LocalizationManager.ThemeTooltip}\">\n                        <TextBlock DockPanel.Dock=\"Left\" Text=\"{Binding LocalizationManager.ThemeLabel}\" />\n                        <ComboBox\n                            Width=\"150\"\n                            DockPanel.Dock=\"Right\"\n                            ItemsSource=\"{Binding AvailableThemes}\"\n                            SelectedItem=\"{Binding Theme}\" />\n                    </DockPanel>\n\n                    <!--  Language  -->\n                    <DockPanel\n                        Margin=\"16,8\"\n                        LastChildFill=\"False\"\n                        ToolTip.Tip=\"{Binding LocalizationManager.LanguageTooltip}\">\n                        <TextBlock DockPanel.Dock=\"Left\" Text=\"{Binding LocalizationManager.LanguageLabel}\" />\n                        <ComboBox\n                            Width=\"150\"\n                            DockPanel.Dock=\"Right\"\n                            ItemsSource=\"{Binding AvailableLanguages}\"\n                            SelectedItem=\"{Binding Language}\" />\n                    </DockPanel>\n\n                    <!--  Auto-updates  -->\n                    <DockPanel\n                        Margin=\"16,8\"\n                        IsVisible=\"{OnPlatform False,\n                                               Windows=True}\"\n                        LastChildFill=\"False\"\n                        ToolTip.Tip=\"{Binding LocalizationManager.AutoUpdateTooltip}\">\n                        <TextBlock DockPanel.Dock=\"Left\" Text=\"{Binding LocalizationManager.AutoUpdateLabel}\" />\n                        <ToggleSwitch DockPanel.Dock=\"Right\" IsChecked=\"{Binding IsAutoUpdateEnabled}\" />\n                    </DockPanel>\n\n                    <!--  Persist token  -->\n                    <DockPanel Margin=\"16,8\" LastChildFill=\"False\">\n                        <ToolTip.Tip>\n                            <TextBlock\n                                MaxWidth=\"400\"\n                                Inlines=\"{Binding LocalizationManager.PersistTokenTooltip, Converter={x:Static converters:MarkdownToInlinesConverter.Instance}}\"\n                                TextWrapping=\"Wrap\" />\n                        </ToolTip.Tip>\n                        <TextBlock DockPanel.Dock=\"Left\" Text=\"{Binding LocalizationManager.PersistTokenLabel}\" />\n                        <ToggleSwitch DockPanel.Dock=\"Right\" IsChecked=\"{Binding IsTokenPersisted}\" />\n                    </DockPanel>\n\n                    <!--  Rate limit preference  -->\n                    <DockPanel\n                        Margin=\"16,8\"\n                        LastChildFill=\"False\"\n                        ToolTip.Tip=\"{Binding LocalizationManager.RateLimitPreferenceTooltip}\">\n                        <TextBlock DockPanel.Dock=\"Left\" Text=\"{Binding LocalizationManager.RateLimitPreferenceLabel}\" />\n                        <ComboBox\n                            Width=\"150\"\n                            DockPanel.Dock=\"Right\"\n                            ItemsSource=\"{Binding AvailableRateLimitPreferences}\"\n                            SelectedItem=\"{Binding RateLimitPreference}\">\n                            <ComboBox.ItemTemplate>\n                                <DataTemplate>\n                                    <TextBlock Text=\"{Binding Converter={x:Static converters:RateLimitPreferenceToStringConverter.Instance}}\" />\n                                </DataTemplate>\n                            </ComboBox.ItemTemplate>\n                        </ComboBox>\n                    </DockPanel>\n\n                    <!--  Thread inclusion mode  -->\n                    <DockPanel\n                        Margin=\"16,8\"\n                        LastChildFill=\"False\"\n                        ToolTip.Tip=\"{Binding LocalizationManager.ShowThreadsTooltip}\">\n                        <TextBlock DockPanel.Dock=\"Left\" Text=\"{Binding LocalizationManager.ShowThreadsLabel}\" />\n                        <ComboBox\n                            Width=\"150\"\n                            DockPanel.Dock=\"Right\"\n                            ItemsSource=\"{Binding AvailableThreadInclusionModes}\"\n                            SelectedItem=\"{Binding ThreadInclusionMode}\" />\n                    </DockPanel>\n\n                    <!--  Locale  -->\n                    <DockPanel\n                        Margin=\"16,8\"\n                        LastChildFill=\"False\"\n                        ToolTip.Tip=\"{Binding LocalizationManager.LocaleTooltip}\">\n                        <TextBlock DockPanel.Dock=\"Left\" Text=\"{Binding LocalizationManager.LocaleLabel}\" />\n                        <ComboBox\n                            Width=\"150\"\n                            DockPanel.Dock=\"Right\"\n                            ItemsSource=\"{Binding AvailableLocales}\"\n                            SelectedItem=\"{Binding Locale}\">\n                            <ComboBox.ItemTemplate>\n                                <DataTemplate>\n                                    <TextBlock Text=\"{Binding Converter={x:Static converters:LocaleToDisplayNameStringConverter.Instance}}\" />\n                                </DataTemplate>\n                            </ComboBox.ItemTemplate>\n                        </ComboBox>\n                    </DockPanel>\n\n                    <!--  UTC normalization  -->\n                    <DockPanel\n                        Margin=\"16,8\"\n                        LastChildFill=\"False\"\n                        ToolTip.Tip=\"{Binding LocalizationManager.NormalizeToUtcTooltip}\">\n                        <TextBlock DockPanel.Dock=\"Left\" Text=\"{Binding LocalizationManager.NormalizeToUtcLabel}\" />\n                        <ToggleSwitch DockPanel.Dock=\"Right\" IsChecked=\"{Binding IsUtcNormalizationEnabled}\" />\n                    </DockPanel>\n\n                    <!--  Parallel limit  -->\n                    <DockPanel\n                        Margin=\"16,8\"\n                        LastChildFill=\"False\"\n                        ToolTip.Tip=\"{Binding LocalizationManager.ParallelLimitTooltip}\">\n                        <TextBlock DockPanel.Dock=\"Left\" Text=\"{Binding LocalizationManager.ParallelLimitLabel}\" />\n                        <StackPanel DockPanel.Dock=\"Right\" Orientation=\"Horizontal\">\n                            <TextBlock Margin=\"10,0\" Text=\"{Binding ParallelLimit}\" />\n                            <Slider\n                                Width=\"150\"\n                                IsSnapToTickEnabled=\"True\"\n                                Maximum=\"10\"\n                                Minimum=\"1\"\n                                TickFrequency=\"1\"\n                                Value=\"{Binding ParallelLimit}\" />\n                        </StackPanel>\n                    </DockPanel>\n                </StackPanel>\n            </ScrollViewer>\n        </Border>\n\n        <!--  Close button  -->\n        <Button\n            Grid.Row=\"2\"\n            Margin=\"16\"\n            HorizontalAlignment=\"Stretch\"\n            Command=\"{Binding CloseCommand}\"\n            Content=\"{Binding LocalizationManager.CloseButton}\"\n            IsCancel=\"True\"\n            IsDefault=\"True\"\n            Theme=\"{DynamicResource MaterialOutlineButton}\" />\n    </Grid>\n</UserControl>"
  },
  {
    "path": "DiscordChatExporter.Gui/Views/Dialogs/SettingsView.axaml.cs",
    "content": "﻿using DiscordChatExporter.Gui.Framework;\nusing DiscordChatExporter.Gui.ViewModels.Dialogs;\n\nnamespace DiscordChatExporter.Gui.Views.Dialogs;\n\npublic partial class SettingsView : UserControl<SettingsViewModel>\n{\n    public SettingsView() => InitializeComponent();\n}\n"
  },
  {
    "path": "DiscordChatExporter.Gui/Views/MainView.axaml",
    "content": "﻿<Window\n    x:Class=\"DiscordChatExporter.Gui.Views.MainView\"\n    xmlns=\"https://github.com/avaloniaui\"\n    xmlns:x=\"http://schemas.microsoft.com/winfx/2006/xaml\"\n    xmlns:dialogHostAvalonia=\"clr-namespace:DialogHostAvalonia;assembly=DialogHost.Avalonia\"\n    xmlns:materialStyles=\"clr-namespace:Material.Styles.Controls;assembly=Material.Styles\"\n    xmlns:viewModels=\"clr-namespace:DiscordChatExporter.Gui.ViewModels\"\n    Title=\"{Binding Title}\"\n    Width=\"625\"\n    Height=\"665\"\n    MinWidth=\"600\"\n    MinHeight=\"400\"\n    x:DataType=\"viewModels:MainViewModel\"\n    Icon=\"/favicon.ico\"\n    RenderOptions.BitmapInterpolationMode=\"HighQuality\"\n    WindowStartupLocation=\"CenterScreen\">\n    <dialogHostAvalonia:DialogHost\n        x:Name=\"DialogHost\"\n        CloseOnClickAway=\"False\"\n        Loaded=\"DialogHost_OnLoaded\">\n        <materialStyles:SnackbarHost HostName=\"Root\" SnackbarMaxCounts=\"3\">\n            <ContentControl Content=\"{Binding Dashboard}\" />\n        </materialStyles:SnackbarHost>\n    </dialogHostAvalonia:DialogHost>\n</Window>"
  },
  {
    "path": "DiscordChatExporter.Gui/Views/MainView.axaml.cs",
    "content": "﻿using Avalonia.Interactivity;\nusing DiscordChatExporter.Gui.Framework;\nusing DiscordChatExporter.Gui.ViewModels;\n\nnamespace DiscordChatExporter.Gui.Views;\n\npublic partial class MainView : Window<MainViewModel>\n{\n    public MainView() => InitializeComponent();\n\n    private void DialogHost_OnLoaded(object? sender, RoutedEventArgs args) =>\n        DataContext.InitializeCommand.Execute(null);\n}\n"
  },
  {
    "path": "DiscordChatExporter.sln",
    "content": "﻿\nMicrosoft Visual Studio Solution File, Format Version 12.00\n# Visual Studio Version 16\nVisualStudioVersion = 16.0.28729.10\nMinimumVisualStudioVersion = 10.0.40219.1\nProject(\"{2150E333-8FDC-42A3-9474-1A3956D46DE8}\") = \"Misc\", \"Misc\", \"{EA305DD5-1F98-415D-B6C4-65053A58F914}\"\n\tProjectSection(SolutionItems) = preProject\n\t\tLicense.txt = License.txt\n\t\tReadme.md = Readme.md\n\t\tDirectory.Build.props = Directory.Build.props\n\t\tDirectory.Packages.props = Directory.Packages.props\n\t\tDiscordChatExporter.Cli.dockerfile = DiscordChatExporter.Cli.dockerfile\n\t\tglobal.json = global.json\n\t\tNuGet.config = NuGet.config\n\tEndProjectSection\nEndProject\nProject(\"{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}\") = \"DiscordChatExporter.Gui\", \"DiscordChatExporter.Gui\\DiscordChatExporter.Gui.csproj\", \"{732A67AF-93DE-49DF-B10F-FD74710B7863}\"\nEndProject\nProject(\"{9A19103F-16F7-4668-BE54-9A1E7A4F7556}\") = \"DiscordChatExporter.Cli\", \"DiscordChatExporter.Cli\\DiscordChatExporter.Cli.csproj\", \"{D08624B6-3081-4BCB-91F8-E9832FACC6CE}\"\nEndProject\nProject(\"{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}\") = \"DiscordChatExporter.Core\", \"DiscordChatExporter.Core\\DiscordChatExporter.Core.csproj\", \"{E19980B9-2B84-4257-A517-540FF1E3FCDD}\"\nEndProject\nProject(\"{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}\") = \"DiscordChatExporter.Cli.Tests\", \"DiscordChatExporter.Cli.Tests\\DiscordChatExporter.Cli.Tests.csproj\", \"{C5064B8B-692E-4515-BA55-A9BE392EE540}\"\nEndProject\nGlobal\n\tGlobalSection(SolutionConfigurationPlatforms) = preSolution\n\t\tDebug|Any CPU = Debug|Any CPU\n\t\tRelease|Any CPU = Release|Any CPU\n\tEndGlobalSection\n\tGlobalSection(ProjectConfigurationPlatforms) = postSolution\n\t\t{732A67AF-93DE-49DF-B10F-FD74710B7863}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{732A67AF-93DE-49DF-B10F-FD74710B7863}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{732A67AF-93DE-49DF-B10F-FD74710B7863}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{732A67AF-93DE-49DF-B10F-FD74710B7863}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{D08624B6-3081-4BCB-91F8-E9832FACC6CE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{D08624B6-3081-4BCB-91F8-E9832FACC6CE}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{D08624B6-3081-4BCB-91F8-E9832FACC6CE}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{D08624B6-3081-4BCB-91F8-E9832FACC6CE}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{E19980B9-2B84-4257-A517-540FF1E3FCDD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{E19980B9-2B84-4257-A517-540FF1E3FCDD}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{E19980B9-2B84-4257-A517-540FF1E3FCDD}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{E19980B9-2B84-4257-A517-540FF1E3FCDD}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{C5064B8B-692E-4515-BA55-A9BE392EE540}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{C5064B8B-692E-4515-BA55-A9BE392EE540}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{C5064B8B-692E-4515-BA55-A9BE392EE540}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{C5064B8B-692E-4515-BA55-A9BE392EE540}.Release|Any CPU.Build.0 = Release|Any CPU\n\tEndGlobalSection\n\tGlobalSection(SolutionProperties) = preSolution\n\t\tHideSolutionNode = FALSE\n\tEndGlobalSection\n\tGlobalSection(ExtensibilityGlobals) = postSolution\n\t\tSolutionGuid = {51587D08-01E1-4511-AC57-A417D1A9162F}\n\tEndGlobalSection\nEndGlobal\n"
  },
  {
    "path": "License.txt",
    "content": "MIT License\n\nCopyright (c) 2017-2026 Oleksii Holub\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE."
  },
  {
    "path": "NuGet.config",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<configuration>\n  <packageSources>\n    <clear />\n    <add key=\"nuget.org\" value=\"https://api.nuget.org/v3/index.json\" protocolVersion=\"3\" />\n  </packageSources>\n  <config>\n    <add key=\"defaultPushSource\" value=\"https://api.nuget.org/v3/index.json\" />\n  </config>\n</configuration>\n"
  },
  {
    "path": "Readme.md",
    "content": "# DiscordChatExporter\n\n[![Status](https://img.shields.io/badge/status-maintenance-ffd700.svg)](https://github.com/Tyrrrz/.github/blob/prime/docs/project-status.md)\n[![Made in Ukraine](https://img.shields.io/badge/made_in-ukraine-ffd700.svg?labelColor=0057b7)](https://tyrrrz.me/ukraine)\n[![Build](https://img.shields.io/github/actions/workflow/status/Tyrrrz/DiscordChatExporter/main.yml?branch=prime)](https://github.com/Tyrrrz/DiscordChatExporter/actions)\n[![Coverage](https://img.shields.io/codecov/c/github/Tyrrrz/DiscordChatExporter/prime)](https://codecov.io/gh/Tyrrrz/DiscordChatExporter)\n[![Release](https://img.shields.io/github/release/Tyrrrz/DiscordChatExporter.svg)](https://github.com/Tyrrrz/DiscordChatExporter/releases)\n[![Downloads](https://img.shields.io/github/downloads/Tyrrrz/DiscordChatExporter/total.svg)](https://github.com/Tyrrrz/DiscordChatExporter/releases)\n[![Pulls](https://img.shields.io/docker/pulls/tyrrrz/discordchatexporter)](https://hub.docker.com/r/tyrrrz/discordchatexporter)\n[![Discord](https://img.shields.io/discord/869237470565392384?label=discord)](https://discord.gg/2SUWKFnHSm)\n[![Fuck Russia](https://img.shields.io/badge/fuck-russia-e4181c.svg?labelColor=000000)](https://twitter.com/tyrrrz/status/1495972128977571848)\n\n<table>\n    <tr>\n        <td width=\"99999\" align=\"center\">Development of this project is entirely funded by the community. <b><a href=\"https://tyrrrz.me/donate\">Consider donating to support!</a></b></td>\n    </tr>\n</table>\n\n<p align=\"center\">\n    <img src=\"favicon.png\" alt=\"Icon\" />\n</p>\n\n**DiscordChatExporter** is an application that can be used to export message history from any [Discord](https://discord.com) channel to a file.\nIt works with direct messages, group messages, and server channels, and supports Discord's dialect of markdown as well as most other rich media features.\n\n> ❔ If you have questions or issues, **please refer to the [docs](.docs)**.\n\n> 💬 If you want to chat, **join my [Discord server](https://discord.gg/2SUWKFnHSm)**.\n\n## Terms of use<sup>[[?]](https://github.com/Tyrrrz/.github/blob/prime/docs/why-so-political.md)</sup>\n\nBy using this project or its source code, for any purpose and in any shape or form, you grant your **implicit agreement** to all the following statements:\n\n- You **condemn Russia and its military aggression against Ukraine**\n- You **recognize that Russia is an occupant that unlawfully invaded a sovereign state**\n- You **support Ukraine's territorial integrity, including its claims over temporarily occupied territories of Crimea and Donbas**\n- You **reject false narratives perpetuated by Russian state propaganda**\n\nTo learn more about the war and how you can help, [click here](https://tyrrrz.me/ukraine). Glory to Ukraine! 🇺🇦\n\n## Download\n\n- **Graphical user interface** (desktop app):\n  - 🟢 **[Stable release](https://github.com/Tyrrrz/DiscordChatExporter/releases/latest)**: look for `DiscordChatExporter.*.zip`\n  - 🟠 [CI build](https://github.com/Tyrrrz/DiscordChatExporter/actions/workflows/main.yml): look for `DiscordChatExporter.*.zip`\n- **Command-line interface** (terminal app):\n  - 🟢 **[Stable release](https://github.com/Tyrrrz/DiscordChatExporter/releases/latest)**: look for `DiscordChatExporter.Cli.*.zip`\n  - 🟠 [CI build](https://github.com/Tyrrrz/DiscordChatExporter/actions/workflows/main.yml): look for `DiscordChatExporter.Cli.*.zip`\n  - 🐋 [Docker](https://hub.docker.com/r/tyrrrz/discordchatexporter): `docker pull tyrrrz/discordchatexporter`\n  - 📦 [AUR](https://aur.archlinux.org/packages/discord-chat-exporter-cli): `discord-chat-exporter-cli`\n  - 📦 [Nix](https://search.nixos.org/packages?query=discordchatexporter-cli): `discordchatexporter-cli`\n\n> [!IMPORTANT]\n> To launch the GUI version of the app on MacOS, you need to first remove the downloaded file from quarantine.\n> You can do that by running the following command in the terminal: `xattr -rd com.apple.quarantine DiscordChatExporter.app`.\n\n> [!NOTE]\n> If you're unsure which build is right for your system, consult with [this page](https://useragent.cc) to determine your OS and CPU architecture.\n\n> [!NOTE]\n> AUR and Nix packages linked above are maintained by the community.\n> If you have any issues with them, please contact the corresponding maintainers.\n\n## Features\n\n- Cross-platform graphical and command-line interfaces\n- Authentication via either a user or a bot token\n- Multiple output formats: HTML (dark/light), TXT, CSV, JSON\n- Support for markdown, attachments, embeds, emoji, and other rich media features\n- File partitioning, date ranges, message filtering, and other export options\n- Self-contained exports that can be viewed offline\n\n## Screenshots\n\n![channel list](.assets/list.png)\n![rendered output](.assets/output.png)\n\n## See also\n\n- [**Chat Analytics**](https://github.com/mlomb/chat-analytics) — solution for analyzing chat patterns of Discord users, using exports produced by **DiscordChatExporter**.\n- [**DiscordChatExporter-frontend**](https://github.com/slatinsky/DiscordChatExporter-frontend) — convenient viewer for exports produced by **DiscordChatExporter**.\n"
  },
  {
    "path": "docker-entrypoint.sh",
    "content": "#!/usr/bin/env sh\n\n# If we are root, ensure the files in /out are writable\n# by the dce user and restart the process as the dce user\nif [ \"$(id -u)\" = '0' ]; then\n  chown -R dce:dce /out\n  exec su-exec dce \"$0\" \"$@\"\nfi\n\nexec /opt/app/DiscordChatExporter.Cli \"$@\"\n"
  },
  {
    "path": "global.json",
    "content": "{\n  \"sdk\": {\n    \"version\": \"10.0.100\",\n    \"rollForward\": \"latestFeature\"\n  }\n}"
  }
]