From 58802752d98951d6328acf9cd965bf908c34ee5a Mon Sep 17 00:00:00 2001 From: 0x4248 <60709927+0x4248@users.noreply.github.com> Date: Thu, 15 Aug 2024 10:52:05 +0100 Subject: [PATCH 01/84] event: make dangerous command warning more clear Signed-off-by: 0x4248 <60709927+0x4248@users.noreply.github.com> --- tux/handlers/event.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tux/handlers/event.py b/tux/handlers/event.py index 3a7f415..4522043 100644 --- a/tux/handlers/event.py +++ b/tux/handlers/event.py @@ -26,7 +26,7 @@ class EventHandler(commands.Cog): if is_harmful(stripped_content): await message.reply( - "-# ⚠️ **This command is likely harmful. By running it, all directory contents will be deleted. There is no undo. Ensure you fully understand the consequences before proceeding. If you have received this message in error, please disregard it.**", + "⚠️ **This command is likely harmful.**\n-# By running it, **all directory contents will be deleted. There is no undo.** Ensure you fully understand the consequences before proceeding. If you have received this message in error, please disregard it.", ) @commands.Cog.listener() From feef3a3e2e8da4d018649a47f43f1fe6de1a2378 Mon Sep 17 00:00:00 2001 From: 0x4248 <60709927+0x4248@users.noreply.github.com> Date: Thu, 15 Aug 2024 11:55:35 +0100 Subject: [PATCH 02/84] Add rm link and dd filter Signed-off-by: 0x4248 <60709927+0x4248@users.noreply.github.com> --- tux/handlers/event.py | 14 ++++++++++---- tux/utils/functions.py | 22 +++++++++++++++++++--- 2 files changed, 29 insertions(+), 7 deletions(-) diff --git a/tux/handlers/event.py b/tux/handlers/event.py index 4522043..054c9be 100644 --- a/tux/handlers/event.py +++ b/tux/handlers/event.py @@ -2,7 +2,7 @@ import discord from discord.ext import commands from tux.database.controllers import DatabaseController -from tux.utils.functions import is_harmful, strip_formatting +from tux.utils.functions import is_harmful, get_harmful_command_type, strip_formatting class EventHandler(commands.Cog): @@ -25,9 +25,15 @@ class EventHandler(commands.Cog): stripped_content = strip_formatting(message.content) if is_harmful(stripped_content): - await message.reply( - "⚠️ **This command is likely harmful.**\n-# By running it, **all directory contents will be deleted. There is no undo.** Ensure you fully understand the consequences before proceeding. If you have received this message in error, please disregard it.", - ) + bad_command_type: str = get_harmful_command_type(stripped_content) + if bad_command_type == "rm": + await message.reply( + "⚠️ **This command is likely harmful.**\n-# By running it, **all directory contents will be deleted. There is no undo.** Ensure you fully understand the consequences before proceeding. If you have received this message in error, please disregard it. [Learn more]()" + ) + elif bad_command_type == "dd": + await message.reply( + "⚠️ **This command is likely harmful.**\n-# By running it, **all data on the specified disk will be erased. There is no undo.** Ensure you fully understand the consequences before proceeding. If you have received this message in error, please disregard it." + ) @commands.Cog.listener() async def on_message_edit(self, before: discord.Message, after: discord.Message) -> None: diff --git a/tux/utils/functions.py b/tux/utils/functions.py index 2f32761..3998e23 100644 --- a/tux/utils/functions.py +++ b/tux/utils/functions.py @@ -5,13 +5,29 @@ from typing import Any import discord harmful_command_pattern = r"(?:sudo\s+|doas\s+|run0\s+)?rm\s+(-[frR]*|--force|--recursive|--no-preserve-root|\s+)*([/\∕~]\s*|\*|/bin|/boot|/etc|/lib|/proc|/root|/sbin|/sys|/tmp|/usr|/var|/var/log|/network.|/system)(\s+--no-preserve-root|\s+\*)*|:\(\)\{ :|:& \};:" # noqa: RUF001 - +harmful_dd_command_pattern = r"dd\s+if=\/dev\/(zero|random|urandom)\s+of=\/dev\/.*da.*" def is_harmful(command: str) -> bool: first_test: bool = re.search(harmful_command_pattern, command, re.IGNORECASE) is not None - second_test: bool = re.search(r"rm.{0,5}[rfRF]", command, re.IGNORECASE) is not None - return first_test and second_test + second_test: bool = re.search(r"rm.{0,5}[rfRF]", command, re.IGNORECASE) is not None + third_test: bool = re.search(r"X\s*=\s*/\s*&&\s*(sudo\s*)?rm\s*-\s*rf", command, re.IGNORECASE) is not None + ret: bool = first_test and second_test or third_test + if not ret: + # Check for a harmful dd command + ret = re.search(harmful_dd_command_pattern, command, re.IGNORECASE) is not None + return ret +def get_harmful_command_type(command: str) -> str: + bad_command_type = "" + first_test: bool = re.search(harmful_command_pattern, command, re.IGNORECASE) is not None + second_test: bool = re.search(r"rm.{0,5}[rfRF]", command, re.IGNORECASE) is not None + third_test: bool = re.search(r"X\s*=\s*/\s*&&\s*(sudo\s*)?rm\s*-\s*rf", command, re.IGNORECASE) is not None + if first_test and second_test or third_test: + bad_command_type = "rm" + else: + if re.search(harmful_dd_command_pattern, command, re.IGNORECASE) is not None: + bad_command_type = "dd" + return bad_command_type def strip_formatting(content: str) -> str: # Remove triple backtick blocks considering any spaces and platform-specific newlines From 9425137c59f8409d806f3bc9c60e36de54c85a9d Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 16 Aug 2024 08:55:55 +0000 Subject: [PATCH 03/84] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- tux/handlers/event.py | 6 +++--- tux/utils/functions.py | 7 +++++-- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/tux/handlers/event.py b/tux/handlers/event.py index 054c9be..1b511f5 100644 --- a/tux/handlers/event.py +++ b/tux/handlers/event.py @@ -2,7 +2,7 @@ import discord from discord.ext import commands from tux.database.controllers import DatabaseController -from tux.utils.functions import is_harmful, get_harmful_command_type, strip_formatting +from tux.utils.functions import get_harmful_command_type, is_harmful, strip_formatting class EventHandler(commands.Cog): @@ -28,11 +28,11 @@ class EventHandler(commands.Cog): bad_command_type: str = get_harmful_command_type(stripped_content) if bad_command_type == "rm": await message.reply( - "⚠️ **This command is likely harmful.**\n-# By running it, **all directory contents will be deleted. There is no undo.** Ensure you fully understand the consequences before proceeding. If you have received this message in error, please disregard it. [Learn more]()" + "⚠️ **This command is likely harmful.**\n-# By running it, **all directory contents will be deleted. There is no undo.** Ensure you fully understand the consequences before proceeding. If you have received this message in error, please disregard it. [Learn more]()", ) elif bad_command_type == "dd": await message.reply( - "⚠️ **This command is likely harmful.**\n-# By running it, **all data on the specified disk will be erased. There is no undo.** Ensure you fully understand the consequences before proceeding. If you have received this message in error, please disregard it." + "⚠️ **This command is likely harmful.**\n-# By running it, **all data on the specified disk will be erased. There is no undo.** Ensure you fully understand the consequences before proceeding. If you have received this message in error, please disregard it.", ) @commands.Cog.listener() diff --git a/tux/utils/functions.py b/tux/utils/functions.py index 3998e23..8e71c5e 100644 --- a/tux/utils/functions.py +++ b/tux/utils/functions.py @@ -7,9 +7,10 @@ import discord harmful_command_pattern = r"(?:sudo\s+|doas\s+|run0\s+)?rm\s+(-[frR]*|--force|--recursive|--no-preserve-root|\s+)*([/\∕~]\s*|\*|/bin|/boot|/etc|/lib|/proc|/root|/sbin|/sys|/tmp|/usr|/var|/var/log|/network.|/system)(\s+--no-preserve-root|\s+\*)*|:\(\)\{ :|:& \};:" # noqa: RUF001 harmful_dd_command_pattern = r"dd\s+if=\/dev\/(zero|random|urandom)\s+of=\/dev\/.*da.*" + def is_harmful(command: str) -> bool: first_test: bool = re.search(harmful_command_pattern, command, re.IGNORECASE) is not None - second_test: bool = re.search(r"rm.{0,5}[rfRF]", command, re.IGNORECASE) is not None + second_test: bool = re.search(r"rm.{0,5}[rfRF]", command, re.IGNORECASE) is not None third_test: bool = re.search(r"X\s*=\s*/\s*&&\s*(sudo\s*)?rm\s*-\s*rf", command, re.IGNORECASE) is not None ret: bool = first_test and second_test or third_test if not ret: @@ -17,10 +18,11 @@ def is_harmful(command: str) -> bool: ret = re.search(harmful_dd_command_pattern, command, re.IGNORECASE) is not None return ret + def get_harmful_command_type(command: str) -> str: bad_command_type = "" first_test: bool = re.search(harmful_command_pattern, command, re.IGNORECASE) is not None - second_test: bool = re.search(r"rm.{0,5}[rfRF]", command, re.IGNORECASE) is not None + second_test: bool = re.search(r"rm.{0,5}[rfRF]", command, re.IGNORECASE) is not None third_test: bool = re.search(r"X\s*=\s*/\s*&&\s*(sudo\s*)?rm\s*-\s*rf", command, re.IGNORECASE) is not None if first_test and second_test or third_test: bad_command_type = "rm" @@ -29,6 +31,7 @@ def get_harmful_command_type(command: str) -> str: bad_command_type = "dd" return bad_command_type + def strip_formatting(content: str) -> str: # Remove triple backtick blocks considering any spaces and platform-specific newlines content = re.sub(r"`/```(.*)```/", "", content, flags=re.DOTALL) From c2239be65ea0cd57bdce87620da98f3c6b13a278 Mon Sep 17 00:00:00 2001 From: "0x4248 (Blix)" <60709927+0x4248@users.noreply.github.com> Date: Fri, 16 Aug 2024 10:41:42 +0100 Subject: [PATCH 04/84] add message if bad_command_type is not set Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> --- tux/handlers/event.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tux/handlers/event.py b/tux/handlers/event.py index 1b511f5..5fb0d79 100644 --- a/tux/handlers/event.py +++ b/tux/handlers/event.py @@ -30,6 +30,13 @@ class EventHandler(commands.Cog): await message.reply( "⚠️ **This command is likely harmful.**\n-# By running it, **all directory contents will be deleted. There is no undo.** Ensure you fully understand the consequences before proceeding. If you have received this message in error, please disregard it. [Learn more]()", ) + else: + await message.reply( + f"⚠️ **This command may be harmful.** Please ensure you understand its effects before proceeding. If you received this message in error, please disregard it.", + ) + await message.reply( + "⚠️ **This command is likely harmful.**\n-# By running it, **all directory contents will be deleted. There is no undo.** Ensure you fully understand the consequences before proceeding. If you have received this message in error, please disregard it. [Learn more]()", + ) elif bad_command_type == "dd": await message.reply( "⚠️ **This command is likely harmful.**\n-# By running it, **all data on the specified disk will be erased. There is no undo.** Ensure you fully understand the consequences before proceeding. If you have received this message in error, please disregard it.", From 1bf1642b5b73dbca6e310f9675f0aaaea2bee1a7 Mon Sep 17 00:00:00 2001 From: kzndotsh Date: Sat, 17 Aug 2024 07:26:20 +0000 Subject: [PATCH 05/84] docs(development.md): add detailed explanation about Cogs in the Cogs Primer section to provide better understanding for developers --- docs/development.md | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/docs/development.md b/docs/development.md index 9f60c8e..fdabf22 100644 --- a/docs/development.md +++ b/docs/development.md @@ -53,11 +53,13 @@ The project is structured as follows: - `help.py`: The help command class definition. ### Configuration + - `.env.example`: The example environment file containing the environment variables required for the bot. - `config/`: The config directory containing the configuration files for the bot. - `settings.json`: The settings file containing the bot settings and configuration. ### Documentation + - `docs/`: The documentation directory containing the project documentation. - `CONTRIBUTING.md`: The contributing guidelines for the project. - `README.md`: The project README file containing the project overview and installation instructions. @@ -66,6 +68,7 @@ The project is structured as follows: - `LICENSE.md`: The license file containing the project license information. ### Development + - `pyproject.toml`: The Poetry configuration file containing the project metadata and dependencies for the bot. - `Dockerfile`: The Dockerfile containing the container configuration for the bot. - `docker-compose.yml`: The Docker Compose file containing the container environment configuration for the bot. @@ -73,13 +76,33 @@ The project is structured as follows: - `.gitignore`: The Git ignore file containing the files and directories to be ignored by Git. ### CI/CD + - `.pre-commit-config.yaml`: The pre-commit configuration file containing the pre-commit hooks for the bot. - `.github/workflows/`: The GitHub Actions directory containing the CI/CD workflows for the bot. - `renovate.json`: The Renovate configuration file containing the dependency update settings for the bot. ## Cogs Primer -TODO: Add cogs primer +There comes a point in your bot’s development when you want to organize a collection of commands, listeners, and some state into one class. Cogs allow you to do just that. + +It should be noted that cogs are typically used alongside with Extensions. An extension at its core is a python file with an entry point called setup. This setup function must be a Python coroutine. It takes a single parameter of the Bot that loads the extension. + +With regards to Tux, we typically define one cog per extension. Furthermore, we have a `CogLoader` class that loads our cogs (technically, extensions) from the `cogs` directory and registers them with the bot at startup. + +### Cog Essentials + +- Each cog is a Python class that subclasses commands.Cog. +- Every regular command or "prefix" is marked with the `@commands.command()` decorator. +- Every app or "slash" command is marked with the `@app_commands.command()` decorator. +- Every hybrid command is marked with the `@commands.hybrid_command()` decorator. +- Every listener is marked with the `@commands.Cog.listener()` decorator. + +tl;dr - Extensions are imported "modules", cogs are classes that are subclasses of `commands.Cog`. + +Referance: + +- [discord.py - Cogs](https://discordpy.readthedocs.io/en/stable/ext/commands/cogs.html) +- [discord.py - Extensions](https://discordpy.readthedocs.io/en/stable/ext/commands/extensions.html) ## Database Primer From 8fcebd00be67cb755c5628ae2e078c31f3a3b702 Mon Sep 17 00:00:00 2001 From: kzndotsh Date: Sat, 17 Aug 2024 08:27:13 +0000 Subject: [PATCH 06/84] docs(CODE_OF_CONDUCT.md, CONTRIBUTING.md): wrap email and URLs in angle brackets for better markdown rendering refactor(CONTRIBUTING.md): improve document structure and readability by adding indentation and spacing style(CONTRIBUTING.md): add newline at end of file to adhere to POSIX standards feat: add .markdownlint.yaml for consistent markdown formatting This commit adds a .markdownlint.yaml configuration file to enforce consistent markdown formatting across the project. This will help maintain readability and uniformity in all markdown files. docs: improve readability and clarity of project documentation - README.md: Simplify title and subtitle formatting, add warning about bot readiness, clarify installation steps, and improve overall readability. - cli.md: Improve readability by adding line breaks. - commands.md: Remove unnecessary line break. - development.md: Simplify introduction, refer to README for installation instructions, and improve readability. - permissions.md: Improve readability by adding line breaks and clarifying permission levels. style(services.md): improve readability by adding a line break between two sentences docs(services.md): add newline at end of file to adhere to POSIX standards --- .github/CODE_OF_CONDUCT.md | 8 +- .github/CONTRIBUTING.md | 37 +++--- .markdownlint.yaml | 266 +++++++++++++++++++++++++++++++++++++ README.md | 78 +++++++---- docs/cli.md | 8 +- docs/commands.md | 3 +- docs/development.md | 20 +-- docs/permissions.md | 17 ++- docs/services.md | 6 +- 9 files changed, 379 insertions(+), 64 deletions(-) create mode 100644 .markdownlint.yaml diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md index ec5353e..6362c88 100644 --- a/.github/CODE_OF_CONDUCT.md +++ b/.github/CODE_OF_CONDUCT.md @@ -60,7 +60,7 @@ representative at an online or offline event. Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at -allthingslinux@proton.me. +. All complaints will be reviewed and investigated promptly and fairly. All community leaders are obligated to respect the privacy and security of the @@ -116,7 +116,7 @@ the community. This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0, available at -https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. +. Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity). @@ -124,5 +124,5 @@ enforcement ladder](https://github.com/mozilla/diversity). [homepage]: https://www.contributor-covenant.org For answers to common questions about this code of conduct, see the FAQ at -https://www.contributor-covenant.org/faq. Translations are available at -https://www.contributor-covenant.org/translations. +. Translations are available at +. diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 59b9983..d512657 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -1,38 +1,43 @@ # Contributing to Tux 🐧 ## Topics -- [Contributing Flow](#contributing-flow) -- [Issues](#issues) -- [Branch Naming Conventions](#branch-naming-conventions) + +- [Contributing to Tux 🐧](#contributing-to-tux-) + - [Topics](#topics) + - [Contributing Flow](#contributing-flow) + - [Issues](#issues) + - [Branch Naming Conventions](#branch-naming-conventions) ## Contributing Flow + 1. See [Issues](#issues) topic. 2. Fork the project. 3. Create a new branch (please, see [Branch Naming Conventions](#branch-naming-conventions) topic if you don't know our conventions). - 4. After done with modifications, time to commit and push. Example: -``` -git add tux/help.py -git commit -m "feat(tux): add help command" -m "Help command description" -git push origin feat/add-help-command -``` + ```bash + git add tux/help.py + git commit -m "feat(tux): add help command" -m "Help command description" + git push origin feat/add-help-command + ``` 5. Send a Pull Request (PR) with the modifications, referencing the `main` branch. 6. Your contribution will be reviewed by the maintainers. - After merge: + - Delete the branch used to commit: -``` + +```bash git checkout main git push origin --delete feat/add-help-command git branch -D feat/add-help-command ``` - Update your fork: -``` + +```bash git remote add upstream https://github.com/allthingslinux/tux.git git fetch upstream git rebase upstream/main @@ -40,10 +45,12 @@ git push -f origin main ``` ## Issues -Before submitting a large PR, please open an [issue](https://github.com/allthingslinux/tux/issues/new) so we can discuss the idea. -## Branch Naming Conventions +Before submitting a large PR, please open an [issue](https://github.com/allthingslinux/tux/issues/new) so we can discuss the idea. + +## Branch Naming Conventions + - Documentation: `git checkout -b docs/contributing` - Modifications: `git checkout -b chore/update-dependencies` - Features: `git checkout -b feat/add-help-command` -- Fixing: `git checkout -b fix/help-command` \ No newline at end of file +- Fixing: `git checkout -b fix/help-command` diff --git a/.markdownlint.yaml b/.markdownlint.yaml new file mode 100644 index 0000000..3419b50 --- /dev/null +++ b/.markdownlint.yaml @@ -0,0 +1,266 @@ +# Example markdownlint configuration with all properties set to their default value + +# Default state for all rules +default: true + +# Path to configuration file to extend +extends: null + +# MD001/heading-increment : Heading levels should only increment by one level at a time : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md001.md +MD001: true + +# MD003/heading-style : Heading style : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md003.md +MD003: + # Heading style + style: "consistent" + +# MD004/ul-style : Unordered list style : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md004.md +MD004: + # List style + style: "consistent" + +# MD005/list-indent : Inconsistent indentation for list items at the same level : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md005.md +MD005: true + +# MD007/ul-indent : Unordered list indentation : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md007.md +MD007: + # Spaces for indent + indent: 2 + # Whether to indent the first level of the list + start_indented: false + # Spaces for first level indent (when start_indented is set) + start_indent: 2 + +# MD009/no-trailing-spaces : Trailing spaces : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md009.md +MD009: + # Spaces for line break + br_spaces: 2 + # Allow spaces for empty lines in list items + list_item_empty_lines: false + # Include unnecessary breaks + strict: false + +# MD010/no-hard-tabs : Hard tabs : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md010.md +MD010: + # Include code blocks + code_blocks: true + # Fenced code languages to ignore + ignore_code_languages: [] + # Number of spaces for each hard tab + spaces_per_tab: 1 + +# MD011/no-reversed-links : Reversed link syntax : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md011.md +MD011: true + +# MD012/no-multiple-blanks : Multiple consecutive blank lines : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md012.md +MD012: + # Consecutive blank lines + maximum: 1 + +# MD013/line-length : Line length : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md013.md +MD013: + # Number of characters + line_length: 200 + # Number of characters for headings + heading_line_length: 80 + # Number of characters for code blocks + code_block_line_length: 80 + # Include code blocks + code_blocks: true + # Include tables + tables: true + # Include headings + headings: true + # Strict length checking + strict: false + # Stern length checking + stern: false + +# MD014/commands-show-output : Dollar signs used before commands without showing output : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md014.md +MD014: true + +# MD018/no-missing-space-atx : No space after hash on atx style heading : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md018.md +MD018: true + +# MD019/no-multiple-space-atx : Multiple spaces after hash on atx style heading : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md019.md +MD019: true + +# MD020/no-missing-space-closed-atx : No space inside hashes on closed atx style heading : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md020.md +MD020: true + +# MD021/no-multiple-space-closed-atx : Multiple spaces inside hashes on closed atx style heading : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md021.md +MD021: true + +# MD022/blanks-around-headings : Headings should be surrounded by blank lines : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md022.md +MD022: + # Blank lines above heading + lines_above: 1 + # Blank lines below heading + lines_below: 1 + +# MD023/heading-start-left : Headings must start at the beginning of the line : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md023.md +MD023: true + +# MD024/no-duplicate-heading : Multiple headings with the same content : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md024.md +MD024: + # Only check sibling headings + siblings_only: false + +# MD025/single-title/single-h1 : Multiple top-level headings in the same document : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md025.md +MD025: + # Heading level + level: 1 + # RegExp for matching title in front matter + front_matter_title: "^\\s*title\\s*[:=]" + +# MD026/no-trailing-punctuation : Trailing punctuation in heading : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md026.md +MD026: + # Punctuation characters + punctuation: ".,;:!。,;:!" + +# MD027/no-multiple-space-blockquote : Multiple spaces after blockquote symbol : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md027.md +MD027: true + +# MD028/no-blanks-blockquote : Blank line inside blockquote : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md028.md +MD028: false + +# MD029/ol-prefix : Ordered list item prefix : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md029.md +MD029: + # List style + style: "one_or_ordered" + +# MD030/list-marker-space : Spaces after list markers : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md030.md +MD030: + # Spaces for single-line unordered list items + ul_single: 1 + # Spaces for single-line ordered list items + ol_single: 1 + # Spaces for multi-line unordered list items + ul_multi: 1 + # Spaces for multi-line ordered list items + ol_multi: 1 + +# MD031/blanks-around-fences : Fenced code blocks should be surrounded by blank lines : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md031.md +MD031: + # Include list items + list_items: true + +# MD032/blanks-around-lists : Lists should be surrounded by blank lines : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md032.md +MD032: true + +# MD033/no-inline-html : Inline HTML : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md033.md +MD033: false + +# MD034/no-bare-urls : Bare URL used : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md034.md +MD034: true + +# MD035/hr-style : Horizontal rule style : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md035.md +MD035: + # Horizontal rule style + style: "consistent" + +# MD036/no-emphasis-as-heading : Emphasis used instead of a heading : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md036.md +MD036: + # Punctuation characters + punctuation: ".,;:!?。,;:!?" + +# MD037/no-space-in-emphasis : Spaces inside emphasis markers : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md037.md +MD037: true + +# MD038/no-space-in-code : Spaces inside code span elements : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md038.md +MD038: true + +# MD039/no-space-in-links : Spaces inside link text : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md039.md +MD039: true + +# MD040/fenced-code-language : Fenced code blocks should have a language specified : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md040.md +MD040: + # List of languages + allowed_languages: [] + # Require language only + language_only: false + +# MD041/first-line-heading/first-line-h1 : First line in a file should be a top-level heading : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md041.md +MD041: + # Heading level + level: 1 + # RegExp for matching title in front matter + front_matter_title: "^\\s*title\\s*[:=]" + +# MD042/no-empty-links : No empty links : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md042.md +MD042: true + +# MD043/required-headings : Required heading structure : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md043.md +MD043: false + +# MD044/proper-names : Proper names should have the correct capitalization : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md044.md +MD044: + # List of proper names + names: [] + # Include code blocks + code_blocks: true + # Include HTML elements + html_elements: true + +# MD045/no-alt-text : Images should have alternate text (alt text) : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md045.md +MD045: true + +# MD046/code-block-style : Code block style : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md046.md +MD046: + # Block style + style: "consistent" + +# MD047/single-trailing-newline : Files should end with a single newline character : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md047.md +MD047: true + +# MD048/code-fence-style : Code fence style : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md048.md +MD048: + # Code fence style + style: "consistent" + +# MD049/emphasis-style : Emphasis style : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md049.md +MD049: + # Emphasis style + style: "consistent" + +# MD050/strong-style : Strong style : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md050.md +MD050: + # Strong style + style: "consistent" + +# MD051/link-fragments : Link fragments should be valid : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md051.md +MD051: true + +# MD052/reference-links-images : Reference links and images should use a label that is defined : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md052.md +MD052: + # Include shortcut syntax + shortcut_syntax: false + +# MD053/link-image-reference-definitions : Link and image reference definitions should be needed : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md053.md +MD053: + # Ignored definitions + ignored_definitions: + - "//" + +# MD054/link-image-style : Link and image style : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md054.md +MD054: + # Allow autolinks + autolink: true + # Allow inline links and images + inline: true + # Allow full reference links and images + full: true + # Allow collapsed reference links and images + collapsed: true + # Allow shortcut reference links and images + shortcut: true + # Allow URLs as inline links + url_inline: true + +# MD055/table-pipe-style : Table pipe style : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md055.md +MD055: + # Table pipe style + style: "consistent" + +# MD056/table-column-count : Table column count : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md056.md +MD056: true diff --git a/README.md b/README.md index 1f796b5..9e3287e 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,6 @@ -
- -

Tux

-

A Discord bot for the All Things Linux Discord server

-
+# Tux + +## A Discord bot for the All Things Linux Discord server

@@ -12,21 +10,23 @@ Repo size Issues - - License Discord

-# NOTE: This bot (without plenty of tweaking) is not ready for multi-server use, we recommend against using it until it is more complete +> [!WARNING] +**This bot (without plenty of tweaking) is not ready for production use, we recommend against using it until it is more complete.** ## About -Tux is a Discord bot for the All Things Linux Discord server. It is designed to provide a variety of features to the server, including moderation, support, utility, and various fun commands. The bot is written in Python using the discord.py library. +Tux is an all in one Discord bot for the All Things Linux Discord server. +It is designed to provide a variety of features to the server, including moderation, support, utility, and various fun commands. ## Tech Stack + +- Python 3.12 alongside the Discord.py library - Poetry for dependency management - Docker and Docker Compose for development and deployment - Strict typing with Pyright and type hints @@ -38,6 +38,7 @@ Tux is a Discord bot for the All Things Linux Discord server. It is designed to - Exception handling with Sentry ## Bot Features + - Asynchronous codebase - Hybrid command system with both slash commands and traditional commands - Cog loading system with hot reloading @@ -51,71 +52,100 @@ Tux is a Discord bot for the All Things Linux Discord server. It is designed to ## Installation ### Prerequisites + - Python 3.12 - [Poetry](https://python-poetry.org/docs/) -- Optional: [Docker](https://docs.docker.com/get-docker/) if you want to run the bot in a container. -- Optional: [Docker Compose](https://docs.docker.com/compose/install/) if you want to define the container environment in a `docker-compose.yml` file. -- Optional: [Just](https://github.com/casey/just/) if you want to use the Justfile for easy CLI commands. +- [Supabase](https://supabase.io/) +- Optional: [Docker](https://docs.docker.com/get-docker/) +- Optional: [Docker Compose](https://docs.docker.com/compose/install/) +- Optional: [Just](https://github.com/casey/just/) + +### Steps to Install + +Assuming you have the prerequisites installed, follow these steps to get started with the development of the project: + +Further detailed instructions can be found in the [development guide](docs/development.md). -### Steps 1. Clone the repository - + ```bash git clone https://github.com/allthingslinux/tux && cd tux ``` -2. Install the dependencies +2. Install the project's dependencies + ```bash poetry install ``` 3. Activate the virtual environment + ```bash poetry shell ``` 4. Install the pre-commit hooks + ```bash pre-commit install ``` 5. Generate the prisma client + ```bash prisma generate ``` -6. Copy the `.env.example` file to `.env` and fill in the required values + Currently, you will need to have a Supabase database set up and the URL set in the `DATABASE_URL` environment variable. + + In the future, we will provide a way to use a local database. We can provide a dev database on request. + +6. Copy the `.env.example` file to `.env` and fill in the required values. + ```bash cp .env.example .env ``` -7. Copy the `config/settings.json.example` file to `config/settings.json` and fill in the required values + You'll need to fill in your Discord bot token here, as well as the Sentry DSN if you want to use Sentry for error tracking. + + We offer dev tokens on request in our Discord server. + +7. Copy the `config/settings.json.example` file to `config/settings.json` and fill in the required values. + ```bash cp config/settings.json.example config/settings.json ``` -8. Run the bot + Be sure to add your Discord user ID to the `BOT_OWNER` key in the settings file. + + You can also add your custom prefix here. + +8. Start the bot! + ```bash poetry run python tux/main.py ``` 9. Run the sync command in the server to sync the slash command tree. - ``` + + ```bash {prefix}dev sync ``` ## Development Notes -> [!NOTE] -> Make sure to add your discord ID to the sys admin list if you are testing locally. +> [!NOTE] +Make sure to add your discord ID to the sys admin list if you are testing locally. > [!NOTE] -> Make sure to set the prisma schema database ENV variable to the DEV database URL. +Make sure to set the prisma schema database ENV variable to the DEV database URL. ## License -This project is licensed under the terms of the The GNU General Public License v3.0. See the [LICENSE](LICENSE.md) file for details. +This project is licensed under the terms of the The GNU General Public License v3.0. + +See [LICENSE](LICENSE.md) for details. ## Metrics -![Alt](https://repobeats.axiom.co/api/embed/cd24c48127e0b6fbc9467711d6d4bd74b30ff8d2.svg "Repobeats analytics image") \ No newline at end of file +![Alt](https://repobeats.axiom.co/api/embed/cd24c48127e0b6fbc9467711d6d4bd74b30ff8d2.svg "Repobeats analytics image") diff --git a/docs/cli.md b/docs/cli.md index d24297c..9b3c707 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -1,6 +1,8 @@ -# Project Documentation +# Project Documentation -This document outlines the essential commands and workflows needed for the installation, development, and management of this project. Each section provides relevant commands and instructions for specific tasks. +This document outlines the essential commands and workflows needed for the installation, development, and management of this project. + +Each section provides relevant commands and instructions for specific tasks. ## Table of Contents @@ -116,4 +118,4 @@ git commit -m "Your commit message" # Push changes to the remote repository. git push -``` \ No newline at end of file +``` diff --git a/docs/commands.md b/docs/commands.md index efa9ff4..9d062d5 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -9,7 +9,6 @@ - Moderation - Utility - ## Commands ### Admin @@ -94,4 +93,4 @@ - `remindme` - `snippets` - `tldr` -- `wiki` \ No newline at end of file +- `wiki` diff --git a/docs/development.md b/docs/development.md index fdabf22..08c0537 100644 --- a/docs/development.md +++ b/docs/development.md @@ -2,17 +2,13 @@ ## Introduction -This document is intended to provide a guide for developers who want to contribute to the development of the project. It is assumed that the reader has a basic understanding of the project and its goals and has a working knowledge of the tools and technologies used in the project. +This document is intended to provide a guide for developers who want to contribute to the development of the project. + +It is assumed that the reader has a basic understanding of the project and its goals as well as the tools and technologies used in the project. ## Getting Started -To get started with the development of the project, you will need to have the following tools and technologies installed on your system: - -- Python 3.12 -- [Poetry](https://python-poetry.org/docs/) -- Optional: [Docker](https://docs.docker.com/get-docker/) if you want to run the bot in a container. -- Optional: [Docker Compose](https://docs.docker.com/compose/install/) if you want to define the container environment in a `docker-compose.yml` file. -- Optional: [Just](https://github.com/casey/just/) if you want to use the Justfile for easy CLI commands. +To get started with the development of the project, refer to the installation instructions in the project [README](../README.md). ## Installation @@ -85,9 +81,13 @@ The project is structured as follows: There comes a point in your bot’s development when you want to organize a collection of commands, listeners, and some state into one class. Cogs allow you to do just that. -It should be noted that cogs are typically used alongside with Extensions. An extension at its core is a python file with an entry point called setup. This setup function must be a Python coroutine. It takes a single parameter of the Bot that loads the extension. +It should be noted that cogs are typically used alongside with Extensions. -With regards to Tux, we typically define one cog per extension. Furthermore, we have a `CogLoader` class that loads our cogs (technically, extensions) from the `cogs` directory and registers them with the bot at startup. +An extension at its core is a python file with an entry point called setup. This setup function must be a Python coroutine. It takes a single parameter of the Bot that loads the extension. + +With regards to Tux, we typically define one cog per extension. This allows us to keep our code organized and modular. + +Furthermore, we have a `CogLoader` class that loads our cogs (technically, extensions) from the `cogs` directory and registers them with the bot at startup. ### Cog Essentials diff --git a/docs/permissions.md b/docs/permissions.md index 9a85ac5..a4ac8fa 100644 --- a/docs/permissions.md +++ b/docs/permissions.md @@ -1,14 +1,22 @@ # Permissions Management -Tux employs a level-based permissions system to control command execution. Each command is associated with a specific permission level, ensuring that only users with the necessary clearance can execute it. +Tux employs a level-based permissions system to control command execution. + +Each command is associated with a specific permission level, ensuring that only users with the necessary clearance can execute it. ## Initial Setup -When setting up Tux for a new server, the server owner can assign one or multiple roles to each permission level. Users then inherit the highest permission level from their assigned roles. For instance, if a user has one role with a permission level of 2 and another with a level of 3, their effective permission level will be 3. +When setting up Tux for a new server, the server owner can assign one or multiple roles to each permission level. Users then inherit the highest permission level from their assigned roles. + +For instance, if a user has one role with a permission level of 2 and another with a level of 3, their effective permission level will be 3. ## Advantages -The level-based system allows Tux to manage command execution efficiently across different servers. It offers a more flexible solution than just relying on Discord's built-in permissions, avoiding the need to hardcode permissions into the bot. This flexibility makes it easier to modify permissions without changing the bot’s underlying code, accommodating servers with custom role names seamlessly. +The level-based system allows Tux to manage command execution efficiently across different servers. + +It offers a more flexible solution than just relying on Discord's built-in permissions, avoiding the need to hardcode permissions into the bot. + +This flexibility makes it easier to modify permissions without changing the bot’s underlying code, accommodating servers with custom role names seamlessly. ## Available Permission Levels @@ -25,4 +33,5 @@ Below is the hierarchy of permission levels available in Tux: - **8: Sys Admin** (User ID list in `config.json`) - **9: Bot Owner** (User ID in `config.json`) -By leveraging these permission levels, Tux provides a robust and adaptable way to manage who can execute specific commands, making it suitable for various server environments. \ No newline at end of file +By leveraging these permission levels, Tux provides a robust and adaptable way to manage who can execute specific commands, +making it suitable for various server environments. diff --git a/docs/services.md b/docs/services.md index 426f63f..ec1618d 100644 --- a/docs/services.md +++ b/docs/services.md @@ -2,7 +2,9 @@ ## Overview -Services within the context of this bot are background tasks that run continuously and provide various functionalities to the server. They are typically not directly interacted with by users, but rather provide additional features to the server that are not covered by commands. +Services within the context of this bot are background tasks that run continuously and provide various functionalities to the server. + +They are typically not directly interacted with by users, but rather provide additional features to the server that are not covered by commands. ## Available Services @@ -15,4 +17,4 @@ Services within the context of this bot are background tasks that run continuous - **Auto Welcome**: Sends a welcome message to new members when they join the server. - **Auto Role**: Assigns a role to new members when they join the server. - **Auto Mod**: Automatically moderates the server by enforcing rules and restrictions -- **Auto Unban**: Automatically unbans users after a specified duration \ No newline at end of file +- **Auto Unban**: Automatically unbans users after a specified duration From 353a4b7759d27ffe4bcfc1fff43ca6615d0d2c8c Mon Sep 17 00:00:00 2001 From: kzndotsh Date: Sat, 17 Aug 2024 23:16:32 +0000 Subject: [PATCH 07/84] feat(emojis): add new tux_notify and tux_tag emojis for better visual representation feat(event.py): add on_thread_create event handler to notify in general chat when a new support thread is created, improving user engagement and support efficiency --- assets/emojis/tux_notify.png | Bin 0 -> 7136 bytes assets/emojis/tux_tag.png | Bin 0 -> 8169 bytes tux/handlers/event.py | 19 +++++++++++++++++++ 3 files changed, 19 insertions(+) create mode 100644 assets/emojis/tux_notify.png create mode 100644 assets/emojis/tux_tag.png diff --git a/assets/emojis/tux_notify.png b/assets/emojis/tux_notify.png new file mode 100644 index 0000000000000000000000000000000000000000..6dcb009cd998eb061aa4468630145440ed81ff9f GIT binary patch literal 7136 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 NciPATUSa4O_h0;8a!LRI literal 0 HcmV?d00001 diff --git a/assets/emojis/tux_tag.png b/assets/emojis/tux_tag.png new file mode 100644 index 0000000000000000000000000000000000000000..9a77efedc82fc3ab3caaf9b2346791e3073a46ab GIT binary patch literal 8169 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 uMF$z4;&QlZPwfG{SP!jE56p`nzfyhO>&I None: + # Temporary hardcoded support forum ID and general chat ID + support_forum = 1172312653797007461 + general_chat = 1172245377395728467 + + if thread.parent_id == support_forum: + owner_mention = thread.owner.mention if thread.owner else {thread.owner_id} + tags = [tag.name for tag in thread.applied_tags] + if tags: + tag_list = ", ".join(tags) + msg = f"<:tux_notify:1274504953666474025> **New support thread created** - help is appreciated!\n{thread.mention} by {owner_mention}\n<:tux_tag:1274504955163709525> **Tags**: `{tag_list}`" + else: + msg = f"<:tux_notify:1274504953666474025> **New support thread created** - help is appreciated!\n{thread.mention} by {owner_mention}" + embed = discord.Embed(description=msg, color=discord.Color.random()) + channel = self.bot.get_channel(general_chat) + if channel is not None and isinstance(channel, discord.TextChannel): + await channel.send(embed=embed, allowed_mentions=discord.AllowedMentions.none()) + async def setup(bot: commands.Bot) -> None: await bot.add_cog(EventHandler(bot)) From 7a49694b77e02ef5fd406e161b4ffe85762541ac Mon Sep 17 00:00:00 2001 From: lilyyllyyllyly Date: Sun, 18 Aug 2024 02:06:48 -0300 Subject: [PATCH 08/84] Change LFS role icon --- assets/roles/distro/lfs.png | Bin 11218 -> 31092 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/assets/roles/distro/lfs.png b/assets/roles/distro/lfs.png index 4c69e947e8ef8c8da992ce8990793506d321c9e0..6aacd022d4ee41e86e871a61678803b1b3136e87 100644 GIT binary patch literal 31092 zcmXt9by$?m*M4?skZzG~X#o+IZbeDyZWIIrDFIm;kw!wLLj*-&K~U-Ll5(ZHyZam7 z-}U{$wOs7OGc#w-oO7RZ-}6>eU4;<;Ha-MFgb!5}wIK)&euYE0*x=*9d-x1|;JKw{E_yRlEEt-7rR$pmTtC?mzNj+GiOJ4YfD#Keit|UPg|0=A&3cj zs3`Z;J8fgqEA8nB8fRzPeLKCfx*Sm+P4wqo&>u_+ETu9eR&^OC_Hc`$^>9sHiBR%^ z6-6^Y+ih}dtg@CV>qhMR6or4f`K!zQ3nqR0&)ajfEK|@Q5XTdUeLopSnQc0YzSGu~ zDf>RDWpTC;$hxPID!zbU%PgDwbv~XJ9u_khE)4fm2nnXVA{Vn}HiQppsWJ&5$zTr9 zqdHM5QmDV`4If_CAzeMofGQ@2Y$8$>8djFAMmmto=CTan%ClnL#u|XIqVd1zX*{p) zUSEIyGI5~%z$-+^4mJlz3i%i<2Pr z6~C|tPI6e%$&NFZK4Es^P;k-}wKjHjc2D0(i(2I2hiJM#x8~g!O*#;}b&HuRIFv*o zLeK#B7EF*vR@PIoGY1+aw+t41CKdl{isEk;Suyd5h;!qUruX!5WhSjykTq3d_17G+ z!cMKcfj}rahy+J|hJ*y)*517k6M4vVc1!+S5J_g^Q~e}8Wxe^wmK{XxspBtU4F+C( zEE;Tv5|160yvb;YEhLd;5I!4cH}ykk9oK*vmnXtO=~f`|>)y#JeguuRa}yZK)bR0h z1qJW-f;|tRC)l%^5bI#suNd)gA`437vFsc@Sy%P!$1vRs{J^q4FvQ<6#ibO=ME%F1 zg2Xt36wHD!Sxry=v66SD9r-pYt%YE4Yc;%bj0 z(ShBGtc~p`o3MZkFml8!KMMFf-Pe^4ZP=rwhImVys%y=&va)RNX5Ar(ux54@gc8!< zJ+#E1u|}GO2%0$@e4mh_<7v7nsG*)8MpTq3oFT7S zF#FhM9_%bh*%4zjAeeaXKlIJ>+uBM;VF)D?@k6O!hp&pigg^K0VbftlmJUNob zZb2@1a}|l^e}_X<&z*#HWXs@xCTtyK<(P3g`L$ zO>NEScvViWsfo+(`(H@1CozpJ7pV!yfR?c8$VKO;(sUT z$cj)m2%+W$Ip|O|^FEs#`GGVHLkWiQqPCD)op4539e5*ew1h5 z`HbY1D6-k6-LmHg<`f0Kv%vVs$pdGy&l?YTtH36HD^IOCj2{qZYq^F*?{xH5=Nt%1-*c&^NUZn z60}YAzlSv>L62;fEqPr?Zd=Z&Q_l6Xr-ki48$)?ek5S zZZ z92po5AA-QT=dP6~-XiC0H*0^FdQ)030fY@j@bc1OSza(?k=xo^M%ji#8Gk5V>UK`F z5u(ZkPk%xk?uR3b{B z;D`29hjIoi6u9ccNbI}%nrPgqV3PUI=2X~I?mNFv4@CzPApJ@sBoCEOj9B)Skr%-0 z+V1c?BB{q66S>3_!!z}yNe2G1@UsnTRv{#)9?zm?gEvZO>_jnjDc@FKq*efd+s(ia zxeZs2pJTzXUWmn$$Y5mwSEX^Mr*8D++Zqqs3CrRbJHmt!6LlWo2}uMA=wGZc1TI- zpRCpKq)L|NhfKcJNl1nx>)~8Cb4a$wjb>Zf+p&!gw^6ZNorO1qR!&X{Xp%Tw5+WHp zfz7&IFeeH=W$@g+InL>!9>ib_ohm^QIib}zQ?vav?zaM?HQ(<)`tNw*tI<Igh}L;`ns->6b7j!^xb12d#N3usi=v5N-2G zvq!jV>3%H_LRETR3L*nOYyKtmfaAX@2>4sxy?*;NyiVZeTS|y_DN$e!uOA2Vx*1H+ z5p!aUitppt@mjGK+HjAM8oK`#B=oAB)mV&FuA2#n2W5E?W~UpyRNCZ6Ywh8IC!T-d zmr|9LBm}t$`0A42I3;Xr zUn3ThkVSV^Enn$f;k;1+WPA85DWJ9TyCvibQvIU!oPY)AnGm$0F zT2u8nAwvG-Hlo^PKstTUsC%uk;tW4g=53=xhqQ+^ z>Cf12_Oa)eYQ#G0$gHu=vMT{Uq)lQ-Q>Tk`j!O>~YWw?U@~@_8*H?&pMjtvSHKe%` z$8=7<`Nlwu;Y5`9bIPiRFk@2(#u zZdG?*BRE(uuvOFaB}#TJ(Q;`K1W5lcB}C@34KfW+-q2Y0Y$hnadhKntFpUa+0QPiR zyI#-_ACDJSb!;0|W)jaNR}dx;E6Ib1NOXnmh#rn)=i+&P6gUDVO&j`7qK^At_$Dcs zfm_6$ttBYnb#+6LHx?I4rhM|5MEHNZ9YO92gR)j#vIE1^x1lc}n1ki$jnIA;&!3C@ ze25C}NIs56e%$8g7Jap7xO3wUdvd>u2I@Z*$%9$jbZ^5ggnSP3P&6dXwkqJ|cGR~m zNu**<7ZTGIKTGmM5x3VA0x(=FHrRmOoxE&Xble1FeS9YnE(FN1wS;j@*TDbmWRvaA zGLl_WtNcJf45nPvJdNdA_1_nHc4JsX|=LAfvKqn3J+AL1^}Is4ywJxY8s^fEu9TRq{0&&$~-FGXHa zM=)MRmG#6gg&Rp@8?`=gNSe>miweDc`<5C%F(r0SGT(q>GV8DX7b}SApuy7n`!5Z} z1{NYX{ZzPJKWwm}llOTbLf!0NK@QnBhB?+&Q)v>OdEkA{NKo9KjkKEf;uQW(ch*;x zz3ol#wzt2@3H^M0!Y8^VKY#zW`&os7IyH1S!X6IqTE|@QsN!rh@FJ}+)0v@jlaRcO z4JF&)L<}&%U-!DPjS47PHCqdgJKErMS+dxX#c*Yg{WS4RaOyX{YvX|_IY$B_g}KZbk8GVvVPz9VzL>+u&`QIYHg3X+P`$rk z2P;h-R%N@m6_U!0z_;$1RSooNc)1f@cPnE`2j(_C%SGxU>oiguQDV~Z6+Y^F1DGD9 zao~B zrr|&QDmJ*ZBfMF27$*b&a_S=E_b*?do+Vkn=Fm0K8o!H(pq!{0&nI6H+MWYBd}ICn z41{UVC+B)(?Q_0^1MP)s3a)azPrJe&Mg`lnzbx0f%YMy^2+?)3!eT-RzQV@jyNOOq zBzX;Dd-4>Ie-SQlzfW&7>|x7cyucfztjBS4v96X?E znV1zTuuMO9;b81!HWOalj!BWZd%LE&FM+<*A5*@G;@DEC5S44wG?D@vImW#v#(VjG zFM>{wyT4#lY z$>R|r$u_F4y}sDN?J5|QhpzQ+3e3IZPH0{esAoxb=xkl@Q;BLFfuES z$(xkk{Gy@@ZzuNGttmN9j7XK|ROK(Itupx){TAhMrS6InxHINwktwyyG|*k5R2A0; zF1B@k;;vxJJ^0~d)=Z9ae--`}A9=R6CyhC84cVf~e};})!{OmqcYyUeVo=oO9*BR_ zSK?%JKQpWgwbWFYy_*y?gD!1UI(!r|;zr=uF1tzxxeP0DrMmHS=&&Ikul2C~o4%&BVqAKh^2Cl= zV|tZK;_6MD9uwlx6zQOZh>SR$a`SDpmPsIHIyETFksEgkU6o)1eqIj?GoI?fX56Js zHQ|(8l1t9*Z{{^XfYh$2J+h9-TfaYy;@v%vp;8jQ#`|*t@EKpZEy2eW1`Off#-0>n zU&ekc1SX|aj&=~CWWIZ2WpO6S1TtQk{<+(pdvc&}dI^s5_RNr`-`Q8qX1}IHSO%<} z)qBGKVkhM>-jT}S%VSuP26zwG{z^tDN+GWaz;6$NCKYacGi+H@`Gcg~%-X6}5A}hn(Hw9+bZ_n;hMa` z)ua5}`$14S!z^I{k*@1X^v;2dp64yGm28aq1o|AF7;tp!vus$nyT&0C|8ea~uEr}a z=}mU7Kzy_osy`xkaaA~OV)yd~8*lgfCNr*#efayFI#g~ce}n`5M=D05@>)GliUMKF zyDIO^wCW#w#`(Wo!f)u!lhdLCYdin!4OTFDx(kdw5}q^9IAaDAkC9Zu?KA2k4xrAG z6~`}_WJZM1acTC18YsGrmv}bCW6Ap4o=Cu*oSts;V})Naf-{4%{u@~%hpAz5E5EdA zgNv63H*{!OfsdXUfvXm8->|zA{aX>N>cN*ruG6<0B9tg z%B5a7?9Zd1Ouc)>glMS}Ukmur5j60aC5;Ucu7lNf6+yQEnpqhb(;Wo1?G{v;kvKUH zZRV%2IV+<@%oAl$6^7BJkrwict9wS2xcAk2yz3-Tnl156*TjDfJm5K6=uWQ1noD*v zJsSeMJ0vIDp^YU@fDM7yt=X8D8GMnCvi=E8ZsidM#&APxth0_#qqz%$H1$NrkN?b3 z^)PqkZ+-Q86YCKn$QKIh@#dcl-6>DiNNEDnH)!}}Sy@?L&F!eOejCo0zwJ!b zh}@fObNRzoZ@dJ%$;Svn8Aii|{Hu1VRCj7q{(yUj9VagjmI0j!8nIts_I zV=6Q=ghK&E6Ex#s%+Rx?Yeo_yT{>1#)(@z;xzeOFUAW!j8?wdr>_+b_u1Zm6yb-o( zca>(Y8!P?ts$zrY4*{ezL#*C0`!rDDP0RGvu>v9C=E-@>9SuHutZO-UV0A-^RM*-K zT<0u&oeM)x+(P21u(dyEC?@_xV`m3-nAcx%U_^P4Ei@Grz~iPY6zb=~)D`0D>q4YP z+s(G^uEDvsj0ErXp^3*Ik*A1f`_)L7g-|9wuz83t(jetO@x&j+nvU4dJ_KbhcDbCz z#Sawg&yYPFUXEv8Q>r}B?YnAq1Ouvu`v?Z^9mWp-%Iaoe3l7OzkV+bl0{z&zQvDJ) zXnHs@q=$a~RqK^IJY;_6JCjENecEELXPRr9Hr+Xpxb?^txk2-tJk?JaN}vz+e|J5@ zBX@NVEtDGVP01vRc>&*l*u`(hjV1fCW+5Cp>r5Tzt5N8GPv4kS(e-kN=mRSP`}{Wk z`(~Nf8DKEGjH`B8ZU!#kfw#UKhvZ!lSoy*g=y_|Z~Fu^iy!d>58f2}THaX8Maq=p`8*AvO+PwxMI>(%f$PJkB?=E&U$+u2>k zQ_;ltZSMDKTjm4*bvmqqkNYL-=aqH!@{b?TO>2S(Hh!)4n^54|o?q~&9a+=+!-@Dh zwcA^iBbFOWYGzYL$McpxQJfH8X)p$~K7fnF*7!oJuHsQ(#T=~1y4JiF@qeuL)p;ouCT z|A!5V9z4}huYp38&QGN>Ih%1uH)DL{QnlJ6tU=<<)b0`0z=G{pXl}R2%x&DbrlD;b zE;;GdZV7K3fX^DAI2mJ==0k!B2jn|d*Oz~~+L7=@Ff9s=>1Ud1VI48?UMDBi=Mu%s zY+P7=Cp=2**ztVRCMYxLhJhf+VYKnhFlp!>LCfw$FN}ZQRJ_s#kcbC??M|jWvnp^n zMOSsYyM>ix=#mkC2uDiJ;p2v&q%`c|?wDj3LZ+B+UD2m~J^4ON{C z?jjzU?GRqaY~UJNiul8FJ+Tfx9$Sih4<4++vs7b)&Yd=Adjhk6 zMrVf69SgWeG3_)vsPRDGfhfl@z7@{o=nJvLCx}B~c8(V&xw>w!I8)4R3+Z=hFQx*w z_@JoI0s}|?Vh6mhnIr119oHGIrdgsMkU28S?I+|8>}`k-tV{QGNveZ1#)1eMf9z!N zR6^7^*xrRat3d;G-;+qYn>e1HdHEDOlLH~It`l@=V%YI<(5T{a`L2l6E3Ub(+Za6| zPlO5B#AITZRAQCK9c6-OCtnQJ>^Usr1XqACvc;k}s`sZ{8yeOPZNq5EH;a59^oT1| z%C>!l3RK;sr+4i3l~a2^SyVb<_2g|Bm}+;cC=l-EJgo$73=olZUf(e3xwrI3hBV~K z@--B~%o#37YVL2Y0z(~Hiqmf7czc(-i{S<8tWm#@?P>dKh{liw@wr-|*!8F?AX_DS z+mEhuvqWL~lqzcJ3CZB^+Uu+wIl`lGvfJ1IHtrmJf{8ACL{BKI5m zF)5bMj%TnN6lx$|v{(W?$8#owrNvb}V&EjTMYnkcG&1g>z#W zg?aznfvclvP>m5JdS)6)z@K}k4&VBvx-QzfOB_JzBEP;*o#l_dmN=UwhU~hjL}cE; z$<~uQVywiAtSSE-JSD|CW^gHYc>+VCH5+aGwA96{zo+0Z2GCFvmTVSP93z`rV9Ije)Ypb`_P z7FJpxI64EMYg@;na9l)C=ik(V>d!|>_!E){^I zfeTB^PM`MGGR(pQE8yeHy}A@!;YOZW8#L$dC-%sUGSX_dpM2#fJ6{lR2sl?p3-RWL zg?GoyJd(#%GsiH{gG8Pt#lT!sb>|leK6rh8=S^;uAxR7Tq5Fdjm0Y*`GV{^Hq+0T? z(hmMHrE8~wQ~$38@ans>v?`9LE_szQvBzSRDZ|(_yM3-hd49sLm~ssew;G^>Dp~1@ z)QC~f1-tPfa*lv=4bbHhur-T!Q0^YGmn|{nx4pW?78S|=cv`}Sl z()tZ#Qu(Xq*8R@Ghu@3g$~=<)`iV+EJVi&X-0ho`04{F#5!z52+Az~JR8;)sgJqEX zkQ;hTUo2q4c0GG7S{Az{J5DF_E?%EHYfwQL8`<@5o`rtc{OYzrc zu#OHT(c?qj3M(cxd`S0U8iDK;vkSF0dkx&dcl|Y6Rju85%{lq?27}`CbrPR@A$zdN znE&8gRRY_)#w_Dkevg>T?~S88C_i%NV6)700Myu-9yi}?z#)y2G8qx=QV zKQpg4DN9FH&a!Uj0VwHM@@doR=uzvO4P0N`irUv$Q610Ga!$Ep{3-{*GF>t8_qVX( z;#oZfxH^CKziz`q<~U3TlUG62m_7=Xy(3pkha|ho<4bCv1P6}(9G6dVA_U8n+JBbMQq!yN>BpXgouRl_q~ei!3WKnV3K9~+bo&!B`wK53C8Bi zR`WWQJ+@e&eON4DmM}tx{R1^PLCmC}?y&w`{I`?%ei9UBcLTuKqNF%TALGFGJEhPm1$&}bH3>9&%>#*II#Lm4c z|JI*KI7objn``-Gv`wF9F(BB3rx%uDpd?d6u6zmLzv|f4lmIhXFUtDW%{tX(J6CAa zJpTt6<$L;U+_2+vY<;i;S0bnFePVd;%{lmPK9Wr}q&4Fy|5m6s6>kcq_R^7_57d5| z`NlR!7~wBz?iuAg3ukg?b6o>~MQQbo^d=lRclHcg5H0JceB`MQ0jqVci8&xvM7QmW z&p#MR+~t{H!uVn03>!FFI`pYMxPDQlQDowQnIQR0rs{7BZs^CN%=cUELjn-R)`k6B z-pu*v;%|LxC!TNXuIVCdzAnxeWiun6uM&RpQQ%3bdY!Wk(nm6Ne($y-3p(s?QGItz zyu=jJ5pRfM`SUrt0w4Kt&4N8-#;k~n#BgHKxpGt9ZT+hu-L$%01yxxrsr2}ug6rg{;g`2x#S6%jnO6DGx*bcf17mn1OU;35j~+|xNNW|5z?u?rjJ

uc==5o@+08N&u&MOQ-?R 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 Date: Sun, 18 Aug 2024 02:17:58 -0300 Subject: [PATCH 09/84] Use lfs emote rather than tux in rolecount.py --- tux/cogs/guild/rolecount.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tux/cogs/guild/rolecount.py b/tux/cogs/guild/rolecount.py index 513a130..7badd56 100644 --- a/tux/cogs/guild/rolecount.py +++ b/tux/cogs/guild/rolecount.py @@ -54,7 +54,7 @@ distro_ids = [ [1182152672447569972, "_slackware"], [1178347123905929316, "_popos"], [1175177750143848520, "_kisslinux"], - [1180570700734546031, "tux"], + [1180570700734546031, "_lfs"], [1191106506276479067, "_garuda"], [1192177499413684226, "_asahi"], [1207599112585740309, "_fedoraatomic"], @@ -353,4 +353,4 @@ class RoleCount(commands.Cog): async def setup(bot: commands.Bot): - await bot.add_cog(RoleCount(bot)) + await bot.add_cog(RoleCount(bot)) \ No newline at end of file From ec6790d395f6c7e5c58c19a0dc01abfd50b985bc Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 18 Aug 2024 05:31:46 +0000 Subject: [PATCH 10/84] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- tux/cogs/guild/rolecount.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tux/cogs/guild/rolecount.py b/tux/cogs/guild/rolecount.py index 7badd56..6afa6e9 100644 --- a/tux/cogs/guild/rolecount.py +++ b/tux/cogs/guild/rolecount.py @@ -353,4 +353,4 @@ class RoleCount(commands.Cog): async def setup(bot: commands.Bot): - await bot.add_cog(RoleCount(bot)) \ No newline at end of file + await bot.add_cog(RoleCount(bot)) From 5a051ef7d5e454007c576b403fa22c72e394cb23 Mon Sep 17 00:00:00 2001 From: kzndotsh Date: Sun, 18 Aug 2024 20:22:35 +0000 Subject: [PATCH 11/84] feat(event.py): add support_role variable to notify support team when a new thread is created in the support forum fix(event.py): modify channel.send method to include support_role in the content parameter to ensure support team is notified --- tux/handlers/event.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tux/handlers/event.py b/tux/handlers/event.py index c9f77d3..6422e72 100644 --- a/tux/handlers/event.py +++ b/tux/handlers/event.py @@ -84,6 +84,7 @@ class EventHandler(commands.Cog): # Temporary hardcoded support forum ID and general chat ID support_forum = 1172312653797007461 general_chat = 1172245377395728467 + support_role = "<@&1274823545087590533>" if thread.parent_id == support_forum: owner_mention = thread.owner.mention if thread.owner else {thread.owner_id} @@ -96,7 +97,7 @@ class EventHandler(commands.Cog): embed = discord.Embed(description=msg, color=discord.Color.random()) channel = self.bot.get_channel(general_chat) if channel is not None and isinstance(channel, discord.TextChannel): - await channel.send(embed=embed, allowed_mentions=discord.AllowedMentions.none()) + await channel.send(content=support_role, embed=embed, allowed_mentions=discord.AllowedMentions.none()) async def setup(bot: commands.Bot) -> None: From cd60e35bcdb031b7d8c57978815811b915338f98 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 19 Aug 2024 12:47:09 +0000 Subject: [PATCH 12/84] chore(deps): update dependency mkdocs-material to v9.5.32 --- poetry.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/poetry.lock b/poetry.lock index 4c482be..9640738 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1059,13 +1059,13 @@ pyyaml = ">=5.1" [[package]] name = "mkdocs-material" -version = "9.5.31" +version = "9.5.32" description = "Documentation that simply works" optional = false python-versions = ">=3.8" files = [ - {file = "mkdocs_material-9.5.31-py3-none-any.whl", hash = "sha256:1b1f49066fdb3824c1e96d6bacd2d4375de4ac74580b47e79ff44c4d835c5fcb"}, - {file = "mkdocs_material-9.5.31.tar.gz", hash = "sha256:31833ec664772669f5856f4f276bf3fdf0e642a445e64491eda459249c3a1ca8"}, + {file = "mkdocs_material-9.5.32-py3-none-any.whl", hash = "sha256:f3704f46b63d31b3cd35c0055a72280bed825786eccaf19c655b44e0cd2c6b3f"}, + {file = "mkdocs_material-9.5.32.tar.gz", hash = "sha256:38ed66e6d6768dde4edde022554553e48b2db0d26d1320b19e2e2b9da0be1120"}, ] [package.dependencies] From a84247798e6f3df3788c5b57b32edd721911e49a Mon Sep 17 00:00:00 2001 From: stingleyisme <102343489+stingleyisme@users.noreply.github.com> Date: Tue, 20 Aug 2024 02:38:23 -0500 Subject: [PATCH 13/84] modified: tux/cogs/utility/snippets.py --- tux/cogs/utility/snippets.py | 46 ++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/tux/cogs/utility/snippets.py b/tux/cogs/utility/snippets.py index 908e39f..a2e9b15 100644 --- a/tux/cogs/utility/snippets.py +++ b/tux/cogs/utility/snippets.py @@ -333,6 +333,52 @@ class Snippets(commands.Cog): await ctx.send("Snippet created.", delete_after=30, ephemeral=True) logger.info(f"{ctx.author} created a snippet with the name {name} and content {content}.") + @commands.command( + name="editsnippit", + aliases=["es"], + usage="editsnippit [name]", + ) + @commands.guild_only() + async def edit_snippit(self, ctx: commands.Context[commands.Bot], *, arg: str) -> None: + """ + Edit a snippet. + + Parameters + ---------- + ctx : commands.Context[commands.Bot] + The context object. + arg : str + The name and content of the snippet. + """ + + if ctx.guild is None: + await ctx.send("This command cannot be used in direct messages.") + return + + args = arg.split(" ") + if len(args) < 2: + embed = create_error_embed(error="Please provide a name and content for the snippet.") + await ctx.send(embed=embed, delete_after=30, ephemeral=True) + return + + name = args[0] + content = " ".join(args[1:]) + author_id = ctx.author.id + snippet = await self.db.get_snippet_by_name_and_guild_id(name, ctx.guild.id) + + # Check if the author of the snippet is the same as the user who wants to edit it and if theres no author don't allow editing + author_id = snippet.snippet_user_id or 0 + if author_id != ctx.author.id: + embed = create_error_embed(error="You can only edit your own snippets.") + await ctx.send(embed=embed, delete_after=30, ephemeral=True) + return + + async def update_snippet_by_id(self, snippet_id: int, snippet_content: str) -> Snippet | None: + return await self.table.update( + where={"snippet_id": name}, + data={"snippet_content": content}, + ) + async def setup(bot: commands.Bot) -> None: await bot.add_cog(Snippets(bot)) From aaccbec0964b10997308226b52aaf2e42ba54b5d Mon Sep 17 00:00:00 2001 From: stingleyisme <102343489+stingleyisme@users.noreply.github.com> Date: Tue, 20 Aug 2024 04:01:02 -0500 Subject: [PATCH 14/84] modified: tux/cogs/utility/snippets.py --- tux/cogs/utility/snippets.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/tux/cogs/utility/snippets.py b/tux/cogs/utility/snippets.py index a2e9b15..18979a0 100644 --- a/tux/cogs/utility/snippets.py +++ b/tux/cogs/utility/snippets.py @@ -373,11 +373,12 @@ class Snippets(commands.Cog): await ctx.send(embed=embed, delete_after=30, ephemeral=True) return - async def update_snippet_by_id(self, snippet_id: int, snippet_content: str) -> Snippet | None: - return await self.table.update( - where={"snippet_id": name}, - data={"snippet_content": content}, - ) + await self.db.update_snippet_by_id( + snippet_id=name, + snippet_content=content, + ) + await ctx.send("Snippet Edited.", delete_after=30, ephemeral=True) # Correct indentation + logger.info(f"{ctx.author} Edited a snippet with the name {name} and content {content}.") # Correct indentation async def setup(bot: commands.Bot) -> None: From aabb4f9d99d606642844b1a68d69ec6d1155ae08 Mon Sep 17 00:00:00 2001 From: Atmois Date: Tue, 20 Aug 2024 22:51:09 +0100 Subject: [PATCH 15/84] Add slowmode get subcommand --- tux/cogs/moderation/slowmode.py | 50 ++++++++++++++++++++++++++++++++- 1 file changed, 49 insertions(+), 1 deletion(-) diff --git a/tux/cogs/moderation/slowmode.py b/tux/cogs/moderation/slowmode.py index b695096..48ce121 100644 --- a/tux/cogs/moderation/slowmode.py +++ b/tux/cogs/moderation/slowmode.py @@ -9,7 +9,7 @@ class Slowmode(commands.Cog): def __init__(self, bot: commands.Bot) -> None: self.bot = bot - @commands.hybrid_command( + @commands.hybrid_group( name="slowmode", aliases=["sm"], usage="slowmode [delay] ", @@ -76,6 +76,54 @@ class Slowmode(commands.Cog): await ctx.send(f"Failed to set slowmode. Error: {error}", delete_after=30, ephemeral=True) logger.error(f"Failed to set slowmode. Error: {error}") + @slowmode.command( + name="get", + aliases=["g"], + usage="slowmodeget ", + ) + @commands.guild_only() + @checks.has_pl(2) + async def slowmodeget( + self, + ctx: commands.Context[commands.Bot], + channel: discord.TextChannel | discord.Thread | None = None, + ) -> None: + """ + Get the slowmode delay for the current channel or specified channel. + Parameters + ---------- + self : Slowmode + The Slowmode cog instance. + ctx : commands.Context[commands.Bot] + The context of the command. + channel : discord.TextChannel | discord.Thread | None + The channel to get the slowmode delay from. + """ + if ctx.guild is None: + return + + # If the channel is not specified, default to the current channel + if channel is None: + # Check if the current channel is a text channel or thread + if not isinstance(ctx.channel, discord.TextChannel | discord.Thread): + await ctx.send( + "Invalid channel type, must be a text channel or thread.", + delete_after=30, + ephemeral=True, + ) + return + channel = ctx.channel + + try: + await ctx.send( + f"The slowmode for {channel.mention} is {channel.slowmode_delay} seconds.", + delete_after=30, + ephemeral=True, + ) + except Exception as error: + await ctx.send(f"Failed to get slowmode. Error: {error}", delete_after=30, ephemeral=True) + logger.error(f"Failed to get slowmode. Error: {error}") + async def setup(bot: commands.Bot) -> None: await bot.add_cog(Slowmode(bot)) From 1cc152b6f64d9c03fbda182c183b2cbd4902221c Mon Sep 17 00:00:00 2001 From: electron271 <66094410+electron271@users.noreply.github.com> Date: Tue, 20 Aug 2024 19:29:33 -0500 Subject: [PATCH 16/84] Revert "Update harmful command filter" --- tux/handlers/event.py | 21 ++++----------------- tux/utils/functions.py | 21 +-------------------- 2 files changed, 5 insertions(+), 37 deletions(-) diff --git a/tux/handlers/event.py b/tux/handlers/event.py index 3cf65b3..6422e72 100644 --- a/tux/handlers/event.py +++ b/tux/handlers/event.py @@ -2,7 +2,7 @@ import discord from discord.ext import commands from tux.database.controllers import DatabaseController -from tux.utils.functions import get_harmful_command_type, is_harmful, strip_formatting +from tux.utils.functions import is_harmful, strip_formatting class EventHandler(commands.Cog): @@ -25,22 +25,9 @@ class EventHandler(commands.Cog): stripped_content = strip_formatting(message.content) if is_harmful(stripped_content): - bad_command_type: str = get_harmful_command_type(stripped_content) - if bad_command_type == "rm": - await message.reply( - "⚠️ **This command is likely harmful.**\n-# By running it, **all directory contents will be deleted. There is no undo.** Ensure you fully understand the consequences before proceeding. If you have received this message in error, please disregard it. [Learn more]()", - ) - else: - await message.reply( - f"⚠️ **This command may be harmful.** Please ensure you understand its effects before proceeding. If you received this message in error, please disregard it.", - ) - await message.reply( - "⚠️ **This command is likely harmful.**\n-# By running it, **all directory contents will be deleted. There is no undo.** Ensure you fully understand the consequences before proceeding. If you have received this message in error, please disregard it. [Learn more]()", - ) - elif bad_command_type == "dd": - await message.reply( - "⚠️ **This command is likely harmful.**\n-# By running it, **all data on the specified disk will be erased. There is no undo.** Ensure you fully understand the consequences before proceeding. If you have received this message in error, please disregard it.", - ) + await message.reply( + "-# ⚠️ **This command is likely harmful. By running it, all directory contents will be deleted. There is no undo. Ensure you fully understand the consequences before proceeding. If you have received this message in error, please disregard it.**", + ) @commands.Cog.listener() async def on_message_edit(self, before: discord.Message, after: discord.Message) -> None: diff --git a/tux/utils/functions.py b/tux/utils/functions.py index 8e71c5e..2f32761 100644 --- a/tux/utils/functions.py +++ b/tux/utils/functions.py @@ -5,31 +5,12 @@ from typing import Any import discord harmful_command_pattern = r"(?:sudo\s+|doas\s+|run0\s+)?rm\s+(-[frR]*|--force|--recursive|--no-preserve-root|\s+)*([/\∕~]\s*|\*|/bin|/boot|/etc|/lib|/proc|/root|/sbin|/sys|/tmp|/usr|/var|/var/log|/network.|/system)(\s+--no-preserve-root|\s+\*)*|:\(\)\{ :|:& \};:" # noqa: RUF001 -harmful_dd_command_pattern = r"dd\s+if=\/dev\/(zero|random|urandom)\s+of=\/dev\/.*da.*" def is_harmful(command: str) -> bool: first_test: bool = re.search(harmful_command_pattern, command, re.IGNORECASE) is not None second_test: bool = re.search(r"rm.{0,5}[rfRF]", command, re.IGNORECASE) is not None - third_test: bool = re.search(r"X\s*=\s*/\s*&&\s*(sudo\s*)?rm\s*-\s*rf", command, re.IGNORECASE) is not None - ret: bool = first_test and second_test or third_test - if not ret: - # Check for a harmful dd command - ret = re.search(harmful_dd_command_pattern, command, re.IGNORECASE) is not None - return ret - - -def get_harmful_command_type(command: str) -> str: - bad_command_type = "" - first_test: bool = re.search(harmful_command_pattern, command, re.IGNORECASE) is not None - second_test: bool = re.search(r"rm.{0,5}[rfRF]", command, re.IGNORECASE) is not None - third_test: bool = re.search(r"X\s*=\s*/\s*&&\s*(sudo\s*)?rm\s*-\s*rf", command, re.IGNORECASE) is not None - if first_test and second_test or third_test: - bad_command_type = "rm" - else: - if re.search(harmful_dd_command_pattern, command, re.IGNORECASE) is not None: - bad_command_type = "dd" - return bad_command_type + return first_test and second_test def strip_formatting(content: str) -> str: From 6cd41458e2e0f72ae5a835d122655421f3806e6d Mon Sep 17 00:00:00 2001 From: stingleyisme <102343489+stingleyisme@users.noreply.github.com> Date: Tue, 20 Aug 2024 21:15:11 -0500 Subject: [PATCH 17/84] modified: tux/cogs/utility/snippets.py --- tux/cogs/utility/snippets.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tux/cogs/utility/snippets.py b/tux/cogs/utility/snippets.py index 18979a0..431a619 100644 --- a/tux/cogs/utility/snippets.py +++ b/tux/cogs/utility/snippets.py @@ -374,9 +374,10 @@ class Snippets(commands.Cog): return await self.db.update_snippet_by_id( - snippet_id=name, - snippet_content=content, + snippet.snippet_id, + snippet.snippet_content, ) + await ctx.send("Snippet Edited.", delete_after=30, ephemeral=True) # Correct indentation logger.info(f"{ctx.author} Edited a snippet with the name {name} and content {content}.") # Correct indentation From 5dc9ed766db2f522f517ed9c4ffd8d6fa238c796 Mon Sep 17 00:00:00 2001 From: stingleyisme <102343489+stingleyisme@users.noreply.github.com> Date: Tue, 20 Aug 2024 21:25:24 -0500 Subject: [PATCH 18/84] modified: tux/cogs/utility/snippets.py --- tux/cogs/utility/snippets.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tux/cogs/utility/snippets.py b/tux/cogs/utility/snippets.py index 431a619..67c2eaf 100644 --- a/tux/cogs/utility/snippets.py +++ b/tux/cogs/utility/snippets.py @@ -334,12 +334,12 @@ class Snippets(commands.Cog): logger.info(f"{ctx.author} created a snippet with the name {name} and content {content}.") @commands.command( - name="editsnippit", + name="editsnippet", aliases=["es"], - usage="editsnippit [name]", + usage="editsnippet [name]", ) @commands.guild_only() - async def edit_snippit(self, ctx: commands.Context[commands.Bot], *, arg: str) -> None: + async def edit_snippet(self, ctx: commands.Context[commands.Bot], *, arg: str) -> None: """ Edit a snippet. @@ -375,7 +375,7 @@ class Snippets(commands.Cog): await self.db.update_snippet_by_id( snippet.snippet_id, - snippet.snippet_content, + snippet_content=content, ) await ctx.send("Snippet Edited.", delete_after=30, ephemeral=True) # Correct indentation From 33eed888b1e150861e5c9dd0aa0facc6e26493e1 Mon Sep 17 00:00:00 2001 From: electron271 <66094410+electron271@users.noreply.github.com> Date: Tue, 20 Aug 2024 21:32:28 -0500 Subject: [PATCH 19/84] Snippet Statistics --- prisma/schema.prisma | 1 + tux/cogs/utility/snippets.py | 59 +++++++++++++++++++++++++++++ tux/database/controllers/snippet.py | 10 +++++ 3 files changed, 70 insertions(+) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 39ab607..549d3ab 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -106,6 +106,7 @@ model Snippet { snippet_created_at DateTime @default(now()) guild_id BigInt guild Guild @relation(fields: [guild_id], references: [guild_id]) + uses BigInt @default(0) @@unique([snippet_name, guild_id]) @@index([snippet_name, guild_id]) diff --git a/tux/cogs/utility/snippets.py b/tux/cogs/utility/snippets.py index 908e39f..3be6002 100644 --- a/tux/cogs/utility/snippets.py +++ b/tux/cogs/utility/snippets.py @@ -101,6 +101,61 @@ class Snippets(commands.Cog): return embed + @commands.command( + name="topsnippets", + aliases=["ts"], + usage="topsnippets", + ) + @commands.guild_only() + async def top_snippets(self, ctx: commands.Context[commands.Bot]) -> None: + """ + List top snippets by pagination. + + Parameters + ---------- + ctx : commands.Context[commands.Bot] + The context object. + """ + + # find the top 10 snippets by uses + snippets: list[Snippet] = await self.db.get_all_snippets() + + # Remove snippets that are not in the current server + snippets = [snippet for snippet in snippets if snippet.guild_id == ctx.guild.id] # type: ignore # because of guild_only this will never be None + + # If there are no snippets, send an error message + if not snippets: + embed = EmbedCreator.create_error_embed( + title="Error", + description="No snippets found.", + ctx=ctx, + ) + await ctx.send(embed=embed, delete_after=30) + return + + # sort the snippets by uses + snippets.sort(key=lambda x: x.uses, reverse=True) + + # print in this format + # 1. snippet_name | uses: 10 + # 2. snippet_name | uses: 9 + # 3. snippet_name | uses: 8 + # ... + + text = "```\n" + for i, snippet in enumerate(snippets[:10]): + text += f"{i+1}. {snippet.snippet_name.ljust(20)} | uses: {snippet.uses}\n" + text += "```" + + # only show top 10, no pagination + embed = discord.Embed( + title="Top Snippets", + description=text, + color=CONST.EMBED_COLORS["DEFAULT"], + ) + + await ctx.send(embed=embed) + @commands.command( name="deletesnippet", aliases=["ds"], @@ -214,6 +269,9 @@ class Snippets(commands.Cog): await ctx.send(embed=embed, delete_after=30, ephemeral=True) return + # increment the usage count of the snippet + await self.db.increment_snippet_uses(snippet.snippet_id) + text = f"`/snippets/{snippet.snippet_name}.txt` || {snippet.snippet_content}" await ctx.send(text, allowed_mentions=AllowedMentions.none()) @@ -263,6 +321,7 @@ class Snippets(commands.Cog): embed.add_field(name="Name", value=snippet.snippet_name, inline=False) embed.add_field(name="Author", value=f"{author.mention}", inline=False) embed.add_field(name="Content", value=f"> {snippet.snippet_content}", inline=False) + embed.add_field(name="Uses", value=snippet.uses, inline=False) embed.timestamp = snippet.snippet_created_at or datetime.datetime.fromtimestamp( 0, diff --git a/tux/database/controllers/snippet.py b/tux/database/controllers/snippet.py index 03a01e4..a010871 100644 --- a/tux/database/controllers/snippet.py +++ b/tux/database/controllers/snippet.py @@ -63,3 +63,13 @@ class SnippetController: where={"snippet_id": snippet_id}, data={"snippet_content": snippet_content}, ) + + async def increment_snippet_uses(self, snippet_id: int) -> Snippet | None: + snippet = await self.table.find_first(where={"snippet_id": snippet_id}) + if snippet is None: + return None + + return await self.table.update( + where={"snippet_id": snippet_id}, + data={"uses": snippet.uses + 1}, + ) From f9b68c27d8aa6b26440eefdec38923a0e333a896 Mon Sep 17 00:00:00 2001 From: electron271 <66094410+electron271@users.noreply.github.com> Date: Tue, 20 Aug 2024 21:35:34 -0500 Subject: [PATCH 20/84] fix docstring --- tux/cogs/utility/snippets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tux/cogs/utility/snippets.py b/tux/cogs/utility/snippets.py index 3be6002..bfb7aaa 100644 --- a/tux/cogs/utility/snippets.py +++ b/tux/cogs/utility/snippets.py @@ -109,7 +109,7 @@ class Snippets(commands.Cog): @commands.guild_only() async def top_snippets(self, ctx: commands.Context[commands.Bot]) -> None: """ - List top snippets by pagination. + List top snippets. Parameters ---------- From 2b32d2a4c8946da82bd74a8f8538205cddd1b93f Mon Sep 17 00:00:00 2001 From: electron271 <66094410+electron271@users.noreply.github.com> Date: Tue, 20 Aug 2024 21:50:01 -0500 Subject: [PATCH 21/84] part 1 of improving performance --- tux/database/controllers/snippet.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tux/database/controllers/snippet.py b/tux/database/controllers/snippet.py index a010871..268fc96 100644 --- a/tux/database/controllers/snippet.py +++ b/tux/database/controllers/snippet.py @@ -18,6 +18,9 @@ class SnippetController: async def get_all_snippets(self) -> list[Snippet]: return await self.table.find_many() + async def get_all_snippets_by_guild_id(self, guild_id: int) -> list[Snippet]: + return await self.table.find_many(where={"guild_id": guild_id}) + async def get_all_snippets_sorted(self, newestfirst: bool = True) -> list[Snippet]: return await self.table.find_many( order={"snippet_created_at": "desc" if newestfirst else "asc"}, From d64ef215eb4a0f2719ecacdce9ee6a87750dc1e0 Mon Sep 17 00:00:00 2001 From: electron271 <66094410+electron271@users.noreply.github.com> Date: Tue, 20 Aug 2024 21:50:39 -0500 Subject: [PATCH 22/84] part 2 --- tux/cogs/utility/snippets.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/tux/cogs/utility/snippets.py b/tux/cogs/utility/snippets.py index bfb7aaa..0240c69 100644 --- a/tux/cogs/utility/snippets.py +++ b/tux/cogs/utility/snippets.py @@ -118,10 +118,7 @@ class Snippets(commands.Cog): """ # find the top 10 snippets by uses - snippets: list[Snippet] = await self.db.get_all_snippets() - - # Remove snippets that are not in the current server - snippets = [snippet for snippet in snippets if snippet.guild_id == ctx.guild.id] # type: ignore # because of guild_only this will never be None + snippets: list[Snippet] = await self.db.get_all_snippets_by_guild_id(ctx.guild.id) # type: ignore # wio # If there are no snippets, send an error message if not snippets: From 4e7073c1a71276d3bf0941e730e9d65a9d455f6f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 21 Aug 2024 03:42:14 +0000 Subject: [PATCH 23/84] fix(deps): update dependency pyright to v1.1.377 --- poetry.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/poetry.lock b/poetry.lock index 9640738..efca975 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1649,13 +1649,13 @@ tests = ["hypothesis (>=3.27.0)", "pytest (>=3.2.1,!=3.3.0)"] [[package]] name = "pyright" -version = "1.1.376" +version = "1.1.377" description = "Command line wrapper for pyright" optional = false python-versions = ">=3.7" files = [ - {file = "pyright-1.1.376-py3-none-any.whl", hash = "sha256:0f2473b12c15c46b3207f0eec224c3cea2bdc07cd45dd4a037687cbbca0fbeff"}, - {file = "pyright-1.1.376.tar.gz", hash = "sha256:bffd63b197cd0810395bb3245c06b01f95a85ddf6bfa0e5644ed69c841e954dd"}, + {file = "pyright-1.1.377-py3-none-any.whl", hash = "sha256:af0dd2b6b636c383a6569a083f8c5a8748ae4dcde5df7914b3f3f267e14dd162"}, + {file = "pyright-1.1.377.tar.gz", hash = "sha256:aabc30fedce0ded34baa0c49b24f10e68f4bfc8f68ae7f3d175c4b0f256b4fcf"}, ] [package.dependencies] From c7ffc27977dace4fddfb873e7142133bfa1c7874 Mon Sep 17 00:00:00 2001 From: electron271 <66094410+electron271@users.noreply.github.com> Date: Tue, 20 Aug 2024 22:42:30 -0500 Subject: [PATCH 24/84] fix(snippets.py) add error if snippet not found --- tux/cogs/utility/snippets.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tux/cogs/utility/snippets.py b/tux/cogs/utility/snippets.py index bd3d8be..759581b 100644 --- a/tux/cogs/utility/snippets.py +++ b/tux/cogs/utility/snippets.py @@ -422,6 +422,11 @@ class Snippets(commands.Cog): author_id = ctx.author.id snippet = await self.db.get_snippet_by_name_and_guild_id(name, ctx.guild.id) + if snippet is None: + embed = create_error_embed(error="Snippet not found.") + await ctx.send(embed=embed, delete_after=30, ephemeral=True) + return + # Check if the author of the snippet is the same as the user who wants to edit it and if theres no author don't allow editing author_id = snippet.snippet_user_id or 0 if author_id != ctx.author.id: From 902ee244db56b9f97e03fbadbf4bd74afdf4651e Mon Sep 17 00:00:00 2001 From: electron271 <66094410+electron271@users.noreply.github.com> Date: Tue, 20 Aug 2024 23:01:16 -0500 Subject: [PATCH 25/84] feat(snippets.py) implement snippet locking --- prisma/schema.prisma | 1 + tux/cogs/utility/snippets.py | 88 +++++++++++++++++++++++++++++ tux/database/controllers/snippet.py | 22 ++++++++ 3 files changed, 111 insertions(+) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 549d3ab..8ed537c 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -107,6 +107,7 @@ model Snippet { guild_id BigInt guild Guild @relation(fields: [guild_id], references: [guild_id]) uses BigInt @default(0) + locked Boolean @default(false) @@unique([snippet_name, guild_id]) @@index([snippet_name, guild_id]) diff --git a/tux/cogs/utility/snippets.py b/tux/cogs/utility/snippets.py index 759581b..1168c3a 100644 --- a/tux/cogs/utility/snippets.py +++ b/tux/cogs/utility/snippets.py @@ -1,3 +1,4 @@ +import contextlib import datetime import string @@ -182,6 +183,14 @@ class Snippets(commands.Cog): await ctx.send(embed=embed, delete_after=30, ephemeral=True) return + # check if the snippet is locked + if snippet.locked: + embed = create_error_embed( + error="This snippet is locked and cannot be deleted. If you are a moderator you can use the `forcedeletesnippet` command.", + ) + await ctx.send(embed=embed, delete_after=30, ephemeral=True) + return + # Check if the author of the snippet is the same as the user who wants to delete it and if theres no author don't allow deletion author_id = snippet.snippet_user_id or 0 if author_id != ctx.author.id: @@ -319,6 +328,7 @@ class Snippets(commands.Cog): embed.add_field(name="Author", value=f"{author.mention}", inline=False) embed.add_field(name="Content", value=f"> {snippet.snippet_content}", inline=False) embed.add_field(name="Uses", value=snippet.uses, inline=False) + embed.add_field(name="Locked", value="Yes" if snippet.locked else "No", inline=False) embed.timestamp = snippet.snippet_created_at or datetime.datetime.fromtimestamp( 0, @@ -434,6 +444,22 @@ class Snippets(commands.Cog): await ctx.send(embed=embed, delete_after=30, ephemeral=True) return + # check if the snippet is locked + if snippet.locked: + logger.info( + f"{ctx.author} is trying to edit a snippet with the name {name} and content {content}. Checking if they have the permission level to edit locked snippets.", + ) + # dont make the check send its own error message + try: + await checks.has_pl(2).predicate(ctx) + except commands.CheckFailure: + embed = create_error_embed( + error="This snippet is locked and cannot be edited. If you are a moderator you can use the `forcedeletesnippet` command.", + ) + await ctx.send(embed=embed, delete_after=30, ephemeral=True) + return + logger.info(f"{ctx.author} has the permission level to edit locked snippets.") + await self.db.update_snippet_by_id( snippet.snippet_id, snippet_content=content, @@ -442,6 +468,68 @@ class Snippets(commands.Cog): await ctx.send("Snippet Edited.", delete_after=30, ephemeral=True) # Correct indentation logger.info(f"{ctx.author} Edited a snippet with the name {name} and content {content}.") # Correct indentation + @commands.command( + name="togglesnippetlock", + aliases=["tsl"], + usage="togglesnippetlock [name]", + ) + @commands.guild_only() + @checks.has_pl(2) + async def toggle_snippet_lock(self, ctx: commands.Context[commands.Bot], name: str) -> None: + """ + Toggle a snippet lock. + + Parameters + ---------- + ctx : commands.Context[commands.Bot] + The context object. + name : str + The name of the snippet. + """ + + if ctx.guild is None: + await ctx.send("This command cannot be used in direct messages.") + return + + snippet = await self.db.get_snippet_by_name_and_guild_id(name, ctx.guild.id) + + if snippet is None: + embed = create_error_embed(error="Snippet not found.") + await ctx.send(embed=embed, delete_after=30, ephemeral=True) + return + + # Check if the author of the snippet is the same as the user who wants to lock it and if theres no author don't allow locking + author_id = snippet.snippet_user_id or 0 + if author_id != ctx.author.id: + embed = create_error_embed(error="You can only lock your own snippets.") + await ctx.send(embed=embed, delete_after=30, ephemeral=True) + return + + status = await self.db.toggle_snippet_lock_by_id(snippet.snippet_id) + + if status is None: + embed = create_error_embed(error="No return value from locking the snippet. It may still have been locked.") + await ctx.send(embed=embed, delete_after=30, ephemeral=True) + return + + # dm the author of the snippet + # if failed to dm just ignore it + author = self.bot.get_user(snippet.snippet_user_id) + if author: + with contextlib.suppress(discord.Forbidden): + await author.send( + f"""Your snippet `{snippet.snippet_name}` has been {'locked' if status.locked else 'unlocked'}. + +**What does this mean?** +If a snippet is locked, it cannot be edited by anyone other than moderators. This means that you can no longer edit this snippet. + +**Why was it locked?** +Snippets are usually locked by moderators if they are important to usual use of the server. Changes or deletions to these snippets can have a big impact on the server. If you believe this was done in error, please open a ticket with /ticket.""", + ) + + await ctx.send("Snippet lock toggled.", delete_after=30, ephemeral=True) + logger.info(f"{ctx.author} toggled the lock of the snippet with the name {name}.") + async def setup(bot: commands.Bot) -> None: await bot.add_cog(Snippets(bot)) diff --git a/tux/database/controllers/snippet.py b/tux/database/controllers/snippet.py index 268fc96..3cecd6f 100644 --- a/tux/database/controllers/snippet.py +++ b/tux/database/controllers/snippet.py @@ -76,3 +76,25 @@ class SnippetController: where={"snippet_id": snippet_id}, data={"uses": snippet.uses + 1}, ) + + async def lock_snippet_by_id(self, snippet_id: int) -> Snippet | None: + return await self.table.update( + where={"snippet_id": snippet_id}, + data={"locked": True}, + ) + + async def unlock_snippet_by_id(self, snippet_id: int) -> Snippet | None: + return await self.table.update( + where={"snippet_id": snippet_id}, + data={"locked": False}, + ) + + async def toggle_snippet_lock_by_id(self, snippet_id: int) -> Snippet | None: + snippet = await self.table.find_first(where={"snippet_id": snippet_id}) + if snippet is None: + return None + + return await self.table.update( + where={"snippet_id": snippet_id}, + data={"locked": not snippet.locked}, + ) From 2a47e5ace712bd9a7c4cb1776503aefc477e3ebe Mon Sep 17 00:00:00 2001 From: electron271 <66094410+electron271@users.noreply.github.com> Date: Tue, 20 Aug 2024 23:03:42 -0500 Subject: [PATCH 26/84] Update tux/cogs/utility/snippets.py Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> --- tux/cogs/utility/snippets.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/tux/cogs/utility/snippets.py b/tux/cogs/utility/snippets.py index 1168c3a..5ba5727 100644 --- a/tux/cogs/utility/snippets.py +++ b/tux/cogs/utility/snippets.py @@ -512,10 +512,7 @@ class Snippets(commands.Cog): await ctx.send(embed=embed, delete_after=30, ephemeral=True) return - # dm the author of the snippet - # if failed to dm just ignore it - author = self.bot.get_user(snippet.snippet_user_id) - if author: + if author := self.bot.get_user(snippet.snippet_user_id): with contextlib.suppress(discord.Forbidden): await author.send( f"""Your snippet `{snippet.snippet_name}` has been {'locked' if status.locked else 'unlocked'}. From f4785221e9b4740ce556b306c0091bfe76c598f0 Mon Sep 17 00:00:00 2001 From: electron271 <66094410+electron271@users.noreply.github.com> Date: Tue, 20 Aug 2024 23:06:38 -0500 Subject: [PATCH 27/84] fix(snippets.py) fix oversight with locking snippets --- tux/cogs/utility/snippets.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/tux/cogs/utility/snippets.py b/tux/cogs/utility/snippets.py index 5ba5727..6972c64 100644 --- a/tux/cogs/utility/snippets.py +++ b/tux/cogs/utility/snippets.py @@ -498,13 +498,6 @@ class Snippets(commands.Cog): await ctx.send(embed=embed, delete_after=30, ephemeral=True) return - # Check if the author of the snippet is the same as the user who wants to lock it and if theres no author don't allow locking - author_id = snippet.snippet_user_id or 0 - if author_id != ctx.author.id: - embed = create_error_embed(error="You can only lock your own snippets.") - await ctx.send(embed=embed, delete_after=30, ephemeral=True) - return - status = await self.db.toggle_snippet_lock_by_id(snippet.snippet_id) if status is None: From 0c83587af4bb533dd0df25aab524088e9646d6d4 Mon Sep 17 00:00:00 2001 From: Atmois Date: Wed, 21 Aug 2024 11:11:54 +0100 Subject: [PATCH 28/84] Change slowmode arguments to be an action which can be a delay or get --- tux/cogs/moderation/slowmode.py | 117 ++++++++++++-------------------- 1 file changed, 43 insertions(+), 74 deletions(-) diff --git a/tux/cogs/moderation/slowmode.py b/tux/cogs/moderation/slowmode.py index 48ce121..c3462e3 100644 --- a/tux/cogs/moderation/slowmode.py +++ b/tux/cogs/moderation/slowmode.py @@ -9,21 +9,21 @@ class Slowmode(commands.Cog): def __init__(self, bot: commands.Bot) -> None: self.bot = bot - @commands.hybrid_group( + @commands.hybrid_command( name="slowmode", aliases=["sm"], - usage="slowmode [delay] ", + usage="slowmode [delay|get] ", ) @commands.guild_only() @checks.has_pl(2) async def slowmode( self, ctx: commands.Context[commands.Bot], - delay: str, + action: str, channel: discord.TextChannel | discord.Thread | None = None, ) -> None: """ - Sets slowmode for the current channel or specified channel. + Set or get the slowmode delay for a channel. Parameters ---------- @@ -38,10 +38,9 @@ class Slowmode(commands.Cog): if ctx.guild is None: return - # If the channel is not specified, default to the current channe + # Default to the current channel if none is specified if channel is None: - # Check if the current channel is a text channel - if not isinstance(ctx.channel, discord.TextChannel | discord.Thread): + if not isinstance(ctx.channel, (discord.TextChannel | discord.Thread)): await ctx.send( "Invalid channel type, must be a text channel or thread.", delete_after=30, @@ -50,79 +49,49 @@ class Slowmode(commands.Cog): return channel = ctx.channel - # Unsure of how to type hint this properly as it can be a string or int - # and I can't use a Union for the argument because discord.py nagging? - try: - if delay[-1] in ["s"]: - delay = delay[:-1] - if delay[-1] == "m": - delay = delay[:-1] - delay = int(delay) * 60 # type: ignore - - delay = int(delay) # type: ignore - except ValueError: - await ctx.send("Invalid delay value, must be an integer.", delete_after=30, ephemeral=True) - return - - if delay < 0 or delay > 21600: # type: ignore - await ctx.send("The slowmode delay must be between 0 and 21600 seconds.", delete_after=30, ephemeral=True) - return - - try: - await channel.edit(slowmode_delay=delay) # type: ignore - await ctx.send(f"Slowmode set to {delay} seconds in {channel.mention}.", delete_after=30, ephemeral=True) - - except Exception as error: - await ctx.send(f"Failed to set slowmode. Error: {error}", delete_after=30, ephemeral=True) - logger.error(f"Failed to set slowmode. Error: {error}") - - @slowmode.command( - name="get", - aliases=["g"], - usage="slowmodeget ", - ) - @commands.guild_only() - @checks.has_pl(2) - async def slowmodeget( - self, - ctx: commands.Context[commands.Bot], - channel: discord.TextChannel | discord.Thread | None = None, - ) -> None: - """ - Get the slowmode delay for the current channel or specified channel. - Parameters - ---------- - self : Slowmode - The Slowmode cog instance. - ctx : commands.Context[commands.Bot] - The context of the command. - channel : discord.TextChannel | discord.Thread | None - The channel to get the slowmode delay from. - """ - if ctx.guild is None: - return - - # If the channel is not specified, default to the current channel - if channel is None: - # Check if the current channel is a text channel or thread - if not isinstance(ctx.channel, discord.TextChannel | discord.Thread): + if action.lower() == "get": + try: await ctx.send( - "Invalid channel type, must be a text channel or thread.", + f"The slowmode for {channel.mention} is {channel.slowmode_delay} seconds.", + delete_after=30, + ephemeral=True, + ) + except Exception as error: + await ctx.send(f"Failed to get slowmode. Error: {error}", delete_after=30, ephemeral=True) + logger.error(f"Failed to get slowmode. Error: {error}") + else: + delay = action + try: + if delay[-1] in ["s"]: + delay = delay[:-1] + if delay[-1] == "m": + delay = delay[:-1] + delay = int(delay) * 60 # type: ignore + + delay = int(delay) # type: ignore + except ValueError: + await ctx.send("Invalid delay value, must be an integer.", delete_after=30, ephemeral=True) + return + + if delay < 0 or delay > 21600: # type: ignore + await ctx.send( + "The slowmode delay must be between 0 and 21600 seconds.", delete_after=30, ephemeral=True, ) return - channel = ctx.channel - try: - await ctx.send( - f"The slowmode for {channel.mention} is {channel.slowmode_delay} seconds.", - delete_after=30, - ephemeral=True, - ) - except Exception as error: - await ctx.send(f"Failed to get slowmode. Error: {error}", delete_after=30, ephemeral=True) - logger.error(f"Failed to get slowmode. Error: {error}") + try: + await channel.edit(slowmode_delay=delay) # type: ignore + await ctx.send( + f"Slowmode set to {delay} seconds in {channel.mention}.", + delete_after=30, + ephemeral=True, + ) + + except Exception as error: + await ctx.send(f"Failed to set slowmode. Error: {error}", delete_after=30, ephemeral=True) + logger.error(f"Failed to set slowmode. Error: {error}") async def setup(bot: commands.Bot) -> None: From 3bad160c49819b72f27fcf663b4be0056c6b7f8d Mon Sep 17 00:00:00 2001 From: Atmois Date: Wed, 21 Aug 2024 11:43:04 +0100 Subject: [PATCH 29/84] make the base of the logic for the snippet bans --- tux/cogs/utility/snippets.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/tux/cogs/utility/snippets.py b/tux/cogs/utility/snippets.py index 6972c64..1e9c6cb 100644 --- a/tux/cogs/utility/snippets.py +++ b/tux/cogs/utility/snippets.py @@ -520,6 +520,38 @@ Snippets are usually locked by moderators if they are important to usual use of await ctx.send("Snippet lock toggled.", delete_after=30, ephemeral=True) logger.info(f"{ctx.author} toggled the lock of the snippet with the name {name}.") + @commands.command( + name="snippetban", + aliases=["sb"], + usage="snippetban [target]", + ) + @commands.guild_only() + @checks.has_pl(3) + async def snippet_ban(self, ctx: commands.Context[commands.Bot], target: discord.Member) -> None: + """ + Ban a user from creating snippets. + + Parameters + ---------- + ctx : commands.Context[commands.Bot] + The context object. + target : discord.Member + The member to snippet ban. + """ + if ctx.guild is None: + await ctx.send("This command cannot be used in direct messages.") + return + + if target.id == ctx.author.id: + embed = create_error_embed(error="You cannot ban yourself.") + await ctx.send(embed=embed, delete_after=30, ephemeral=True) + return + + # note to self: add check if user is already banned and if they have perm above the banner + + await ctx.send(f"{target} has been banned from creating snippets.", delete_after=30, ephemeral=True) + logger.info(f"{ctx.author} banned {target} from creating snippets.") + async def setup(bot: commands.Bot) -> None: await bot.add_cog(Snippets(bot)) From 727adfdcbecaa2902a1972a87f1f3893acdddc43 Mon Sep 17 00:00:00 2001 From: Atmois Date: Wed, 21 Aug 2024 12:08:57 +0100 Subject: [PATCH 30/84] revert change to implement it differently --- tux/cogs/utility/snippets.py | 32 -------------------------------- 1 file changed, 32 deletions(-) diff --git a/tux/cogs/utility/snippets.py b/tux/cogs/utility/snippets.py index 1e9c6cb..6972c64 100644 --- a/tux/cogs/utility/snippets.py +++ b/tux/cogs/utility/snippets.py @@ -520,38 +520,6 @@ Snippets are usually locked by moderators if they are important to usual use of await ctx.send("Snippet lock toggled.", delete_after=30, ephemeral=True) logger.info(f"{ctx.author} toggled the lock of the snippet with the name {name}.") - @commands.command( - name="snippetban", - aliases=["sb"], - usage="snippetban [target]", - ) - @commands.guild_only() - @checks.has_pl(3) - async def snippet_ban(self, ctx: commands.Context[commands.Bot], target: discord.Member) -> None: - """ - Ban a user from creating snippets. - - Parameters - ---------- - ctx : commands.Context[commands.Bot] - The context object. - target : discord.Member - The member to snippet ban. - """ - if ctx.guild is None: - await ctx.send("This command cannot be used in direct messages.") - return - - if target.id == ctx.author.id: - embed = create_error_embed(error="You cannot ban yourself.") - await ctx.send(embed=embed, delete_after=30, ephemeral=True) - return - - # note to self: add check if user is already banned and if they have perm above the banner - - await ctx.send(f"{target} has been banned from creating snippets.", delete_after=30, ephemeral=True) - logger.info(f"{ctx.author} banned {target} from creating snippets.") - async def setup(bot: commands.Bot) -> None: await bot.add_cog(Snippets(bot)) From e3266a859564a2ef17bb22781d77a922dacc8d66 Mon Sep 17 00:00:00 2001 From: Atmois Date: Wed, 21 Aug 2024 12:52:36 +0100 Subject: [PATCH 31/84] add snippet ban as a moderation command --- prisma/schema.prisma | 1 + tux/cogs/moderation/cases.py | 5 +- tux/cogs/moderation/snippetban.py | 103 ++++++++++++++++++++++++++++++ tux/utils/flags.py | 15 +++++ 4 files changed, 123 insertions(+), 1 deletion(-) create mode 100644 tux/cogs/moderation/snippetban.py diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 8ed537c..9cab8c1 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -28,6 +28,7 @@ enum CaseType { HACKBAN TEMPBAN KICK + SNIPPETBAN TIMEOUT UNTIMEOUT WARN diff --git a/tux/cogs/moderation/cases.py b/tux/cogs/moderation/cases.py index 3b831b1..bb47f12 100644 --- a/tux/cogs/moderation/cases.py +++ b/tux/cogs/moderation/cases.py @@ -23,6 +23,7 @@ emojis: dict[str, int] = { "timeout": 1268115809083981886, "warn": 1268115764498399264, "jail": 1268115750392954880, + "snippetban": 1275782294363312172, # Placeholder } @@ -373,6 +374,7 @@ class Cases(ModerationCogBase): CaseType.WARN: "warn", CaseType.JAIL: "jail", CaseType.UNJAIL: "jail", + CaseType.SNIPPETBAN: "snippetban", } emoji_name = emoji_map.get(case_type) if emoji_name is not None: @@ -384,7 +386,8 @@ class Cases(ModerationCogBase): def _get_case_action_emoji(self, case_type: CaseType) -> discord.Emoji | None: action = ( "added" - if case_type in [CaseType.BAN, CaseType.KICK, CaseType.TIMEOUT, CaseType.WARN, CaseType.JAIL] + if case_type + in [CaseType.BAN, CaseType.KICK, CaseType.TIMEOUT, CaseType.WARN, CaseType.JAIL, CaseType.SNIPPETBAN] else "removed" if case_type in [CaseType.UNBAN, CaseType.UNTIMEOUT, CaseType.UNJAIL] else None diff --git a/tux/cogs/moderation/snippetban.py b/tux/cogs/moderation/snippetban.py new file mode 100644 index 0000000..9e780c6 --- /dev/null +++ b/tux/cogs/moderation/snippetban.py @@ -0,0 +1,103 @@ +import discord +from discord.ext import commands +from loguru import logger + +from prisma.enums import CaseType +from prisma.models import Case +from tux.utils import checks +from tux.utils.constants import Constants as CONST +from tux.utils.flags import SnippetBanFlags + +from . import ModerationCogBase + + +class SnippetBan(ModerationCogBase): + def __init__(self, bot: commands.Bot) -> None: + super().__init__(bot) + + @commands.hybrid_command( + name="snippetban", + aliases=["sb"], + usage="snippetban [target]", + ) + @commands.guild_only() + @checks.has_pl(3) + async def snippet_ban( + self, + ctx: commands.Context[commands.Bot], + target: discord.Member, + *, + flags: SnippetBanFlags, + ) -> None: + """ + Ban a user from creating snippets. + + Parameters + ---------- + ctx : commands.Context[commands.Bot] + The context object. + target : discord.Member + The member to snippet ban. + flags : SnippetBanFlags + The flags for the command. (reason: str, silent: bool) + """ + if ctx.guild is None: + logger.warning("Snippet ban command used outside of a guild context.") + return + + await self.send_dm(ctx, flags.silent, target, flags.reason, "Snippet Banned") + + case = await self.db.case.insert_case( + case_target_id=target.id, + case_moderator_id=ctx.author.id, + case_type=CaseType.SNIPPETBAN, + case_reason=flags.reason, + guild_id=ctx.guild.id, + ) + + await self.handle_case_response(ctx, case, "created", flags.reason, target) + + async def handle_case_response( + self, + ctx: commands.Context[commands.Bot], + case: Case | None, + action: str, + reason: str, + target: discord.Member | discord.User, + previous_reason: str | None = None, + ) -> None: + moderator = ctx.author + + fields = [ + ("Moderator", f"__{moderator}__\n`{moderator.id}`", True), + ("Target", f"__{target}__\n`{target.id}`", True), + ("Reason", f"> {reason}", False), + ] + + if previous_reason: + fields.append(("Previous Reason", f"> {previous_reason}", False)) + + if case is not None: + embed = await self.create_embed( + ctx, + title=f"Case #{case.case_number} ({case.case_type}) {action}", + fields=fields, + color=CONST.EMBED_COLORS["CASE"], + icon_url=CONST.EMBED_ICONS["ACTIVE_CASE"], + ) + embed.set_thumbnail(url=target.avatar) + else: + embed = await self.create_embed( + ctx, + title=f"Case {action} ({CaseType.SNIPPETBAN})", + fields=fields, + color=CONST.EMBED_COLORS["CASE"], + icon_url=CONST.EMBED_ICONS["ACTIVE_CASE"], + ) + + await self.send_embed(ctx, embed, log_type="mod") + await ctx.send(embed=embed, delete_after=30, ephemeral=True) + + +async def setup(bot: commands.Bot) -> None: + await bot.add_cog(SnippetBan(bot)) diff --git a/tux/utils/flags.py b/tux/utils/flags.py index 8dd4d5d..7afa75a 100644 --- a/tux/utils/flags.py +++ b/tux/utils/flags.py @@ -190,3 +190,18 @@ class WarnFlags(commands.FlagConverter, delimiter=" ", prefix="-"): aliases=["s", "quiet"], default=False, ) + + +class SnippetBanFlags(commands.FlagConverter, delimiter=" ", prefix="-"): + reason: str = commands.flag( + name="reason", + description="The reason for the snippet ban.", + aliases=["r"], + default=MISSING, + ) + silent: bool = commands.flag( + name="silent", + description="Do not send a DM to the target.", + aliases=["s", "quiet"], + default=False, + ) From a36bc9395efd6f2b701dda1abc4f58235e2e6f3c Mon Sep 17 00:00:00 2001 From: kzndotsh Date: Wed, 21 Aug 2024 10:05:29 -0400 Subject: [PATCH 32/84] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9e3287e..a082e71 100644 --- a/README.md +++ b/README.md @@ -148,4 +148,4 @@ See [LICENSE](LICENSE.md) for details. ## Metrics -![Alt](https://repobeats.axiom.co/api/embed/cd24c48127e0b6fbc9467711d6d4bd74b30ff8d2.svg "Repobeats analytics image") +![Alt](https://repobeats.axiom.co/api/embed/b988ba04401b7c68edf9def00f5132cd2a7f3735.svg "Repobeats analytics image") From 8b8138e7ed156f1be8a16e21dfc967830b22172b Mon Sep 17 00:00:00 2001 From: Atmois Date: Wed, 21 Aug 2024 18:18:43 +0100 Subject: [PATCH 33/84] add check if user is already snippet banned --- tux/cogs/moderation/snippetban.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/tux/cogs/moderation/snippetban.py b/tux/cogs/moderation/snippetban.py index 9e780c6..bb32615 100644 --- a/tux/cogs/moderation/snippetban.py +++ b/tux/cogs/moderation/snippetban.py @@ -4,6 +4,7 @@ from loguru import logger from prisma.enums import CaseType from prisma.models import Case +from tux.database.controllers.case import CaseController from tux.utils import checks from tux.utils.constants import Constants as CONST from tux.utils.flags import SnippetBanFlags @@ -14,6 +15,7 @@ from . import ModerationCogBase class SnippetBan(ModerationCogBase): def __init__(self, bot: commands.Bot) -> None: super().__init__(bot) + self.case_controller = CaseController() @commands.hybrid_command( name="snippetban", @@ -45,7 +47,12 @@ class SnippetBan(ModerationCogBase): logger.warning("Snippet ban command used outside of a guild context.") return - await self.send_dm(ctx, flags.silent, target, flags.reason, "Snippet Banned") + # Check if the user is already snippet banned + cases = await self.case_controller.get_all_cases_by_type(ctx.guild.id, CaseType.SNIPPETBAN) + for case in cases: + if case.case_target_id == target.id: + await ctx.send(f"{target.mention} is already snippet banned.", delete_after=10) + return case = await self.db.case.insert_case( case_target_id=target.id, @@ -55,6 +62,7 @@ class SnippetBan(ModerationCogBase): guild_id=ctx.guild.id, ) + await self.send_dm(ctx, flags.silent, target, flags.reason, "Snippet Banned") await self.handle_case_response(ctx, case, "created", flags.reason, target) async def handle_case_response( From 6af757639140ef38126607c0a0dd11ac6469b2f2 Mon Sep 17 00:00:00 2001 From: Atmois Date: Wed, 21 Aug 2024 18:20:03 +0100 Subject: [PATCH 34/84] add check if user is snippet banned --- tux/cogs/utility/snippets.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/tux/cogs/utility/snippets.py b/tux/cogs/utility/snippets.py index 6972c64..89f883b 100644 --- a/tux/cogs/utility/snippets.py +++ b/tux/cogs/utility/snippets.py @@ -8,8 +8,9 @@ from discord.ext import commands from loguru import logger from reactionmenu import ViewButton, ViewMenu +from prisma.enums import CaseType from prisma.models import Snippet -from tux.database.controllers import DatabaseController +from tux.database.controllers import CaseController, DatabaseController from tux.utils import checks from tux.utils.constants import Constants as CONST from tux.utils.embeds import EmbedCreator, create_embed_footer, create_error_embed @@ -20,6 +21,11 @@ class Snippets(commands.Cog): self.bot = bot self.db = DatabaseController().snippet self.config = DatabaseController().guild_config + self.case_controller = CaseController() + + async def is_snippetbanned(self, guild_id: int, user_id: int) -> bool: + cases = await self.case_controller.get_all_cases_by_type(guild_id, CaseType.SNIPPETBAN) + return any(case.case_target_id == user_id for case in cases) @commands.command( name="snippets", @@ -359,6 +365,10 @@ class Snippets(commands.Cog): await ctx.send("This command cannot be used in direct messages.") return + if await self.is_snippetbanned(ctx.guild.id, ctx.author.id): + await ctx.send("You are banned from using snippets.") + return + args = arg.split(" ") if len(args) < 2: embed = create_error_embed(error="Please provide a name and content for the snippet.") From 73016df2db1581933a0d2ace3b3da5a01029a824 Mon Sep 17 00:00:00 2001 From: kzndotsh Date: Wed, 21 Aug 2024 19:37:39 +0000 Subject: [PATCH 35/84] feat(slowmode.py): add 'g' as an alias for 'get' action to improve user experience and provide a shorthand option --- tux/cogs/moderation/slowmode.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tux/cogs/moderation/slowmode.py b/tux/cogs/moderation/slowmode.py index c3462e3..b555b3c 100644 --- a/tux/cogs/moderation/slowmode.py +++ b/tux/cogs/moderation/slowmode.py @@ -49,7 +49,7 @@ class Slowmode(commands.Cog): return channel = ctx.channel - if action.lower() == "get": + if action.lower() in {"get", "g"}: try: await ctx.send( f"The slowmode for {channel.mention} is {channel.slowmode_delay} seconds.", From 19e47cca4b285403fa642b403aff07a3a184d26a Mon Sep 17 00:00:00 2001 From: Zayan Arshad Date: Thu, 22 Aug 2024 00:07:30 +0100 Subject: [PATCH 36/84] Added timezones command (times need to be fixed) --- tux/cogs/utility/timezones.py | 68 + tux/utils/data/countries.json | 4423 +++++++++++++++++++++++++++++++++ tux/utils/data/timezones.json | 122 + 3 files changed, 4613 insertions(+) create mode 100644 tux/cogs/utility/timezones.py create mode 100644 tux/utils/data/countries.json create mode 100644 tux/utils/data/timezones.json diff --git a/tux/cogs/utility/timezones.py b/tux/cogs/utility/timezones.py new file mode 100644 index 0000000..2f579c7 --- /dev/null +++ b/tux/cogs/utility/timezones.py @@ -0,0 +1,68 @@ +import json +from datetime import datetime +from pathlib import Path + +import pytz +from discord.ext import commands + +from tux.utils.embeds import EmbedCreator + + +class Timezones(commands.Cog): + def __init__(self, bot: commands.Bot) -> None: + self.bot = bot + + def loadjson(self, json_file: str) -> dict: + """ + Opens the JSON file and returns a dictionary + + Parameters + ---------- + json_file : str + The path to the json file + """ + + with Path.open(json_file) as file: + return json.load(file) + + async def buildtzstring(self, json_file: str) -> str: + """ + Formats the timezone data within the timezones.json file into a string. + + Parameters + ---------- + json_file : str + The path to the json file + """ + + timezone_data = self.loadjson(json_file) + + formatted_lines = [] + utc_now = datetime.now(pytz.utc) + + for entry in timezone_data: + entry_tz = pytz.timezone(f'{entry["full_timezone"]}') + entry_time_now = utc_now.astimezone(entry_tz) + formatted_time = entry_time_now.strftime("%H:%M") + line = f'{entry["discord_emoji"]} `{entry["offset"]} {entry["timezone"]}` | **{formatted_time}**' + formatted_lines.append(line) + + return "\n".join(formatted_lines) + + @commands.hybrid_command(name="timezones") + async def timezones(self, ctx: commands.Context) -> None: + """ + Presents a list of the top 20 timezones in the world. + """ + + embed = EmbedCreator.create_info_embed( + title="List of timezones", + description=await self.buildtzstring("./tux/utils/data/timezones.json"), + ctx=ctx, + ) + + await ctx.send(embed=embed) + + +async def setup(bot: commands.Bot) -> None: + await bot.add_cog(Timezones(bot)) diff --git a/tux/utils/data/countries.json b/tux/utils/data/countries.json new file mode 100644 index 0000000..05a645f --- /dev/null +++ b/tux/utils/data/countries.json @@ -0,0 +1,4423 @@ +[ + { + "name": "Afghanistan", + "unicode": "U+1F1E6 U+1F1EB", + "emoji": "🇦🇫", + "alpha2": "AF", + "dialCode": "93", + "alpha3": "AFG", + "region": "Asia", + "capital": "Kabul", + "geo": { + "lat": 33, + "long": 33 + }, + "timezones": [ + "Asia/Kabul" + ] + }, + { + "name": "Albania", + "unicode": "U+1F1E6 U+1F1F1", + "emoji": "🇦🇱", + "alpha2": "AL", + "dialCode": "355", + "alpha3": "ALB", + "region": "Europe", + "capital": "Tirana", + "geo": { + "lat": 41, + "long": 41 + }, + "timezones": [ + "Europe/Tirane" + ] + }, + { + "name": "Algeria", + "unicode": "U+1F1E9 U+1F1FF", + "emoji": "🇩🇿", + "alpha2": "DZ", + "dialCode": "213", + "alpha3": "DZA", + "region": "Africa", + "capital": "Algiers", + "geo": { + "lat": 28, + "long": 28 + }, + "timezones": [ + "Africa/Algiers" + ] + }, + { + "name": "American Samoa", + "unicode": "U+1F1E6 U+1F1F8", + "emoji": "🇦🇸", + "alpha2": "AS", + "dialCode": "1 684", + "alpha3": "ASM", + "region": "Oceania", + "capital": "Pago Pago", + "geo": { + "lat": -14.33333333, + "long": -14.33333333 + }, + "timezones": [ + "Pacific/Pago_Pago" + ] + }, + { + "name": "Andorra", + "unicode": "U+1F1E6 U+1F1E9", + "emoji": "🇦🇩", + "alpha2": "AD", + "dialCode": "376", + "alpha3": "AND", + "region": "Europe", + "capital": "Andorra la Vella", + "geo": { + "lat": 42.5, + "long": 42.5 + }, + "timezones": [ + "Europe/Andorra" + ] + }, + { + "name": "Angola", + "unicode": "U+1F1E6 U+1F1F4", + "emoji": "🇦🇴", + "alpha2": "AO", + "dialCode": "244", + "alpha3": "AGO", + "region": "Africa", + "capital": "Luanda", + "geo": { + "lat": -12.5, + "long": -12.5 + }, + "timezones": [ + "Africa/Luanda" + ] + }, + { + "name": "Anguilla", + "unicode": "U+1F1E6 U+1F1EE", + "emoji": "🇦🇮", + "alpha2": "AI", + "dialCode": "1 264", + "alpha3": "AIA", + "region": "Americas", + "capital": "The Valley", + "geo": { + "lat": 18.25, + "long": 18.25 + }, + "timezones": [ + "America/Anguilla" + ] + }, + { + "name": "Antarctica", + "unicode": "U+1F1E6 U+1F1F6", + "emoji": "🇦🇶", + "alpha2": "AQ", + "dialCode": "", + "alpha3": "ATA", + "region": "", + "capital": null, + "geo": { + "lat": -90, + "long": -90 + }, + "timezones": [ + "Antarctica/McMurdo", + "Antarctica/Casey", + "Antarctica/Davis", + "Antarctica/DumontDUrville", + "Antarctica/Mawson", + "Antarctica/Palmer", + "Antarctica/Rothera", + "Antarctica/Syowa", + "Antarctica/Troll", + "Antarctica/Vostok" + ] + }, + { + "name": "Antigua and Barbuda", + "unicode": "U+1F1E6 U+1F1EC", + "emoji": "🇦🇬", + "alpha2": "AG", + "dialCode": "1268", + "alpha3": "ATG", + "region": "Americas", + "capital": "Saint John's", + "geo": { + "lat": 17.05, + "long": 17.05 + }, + "timezones": [ + "America/Antigua" + ] + }, + { + "name": "Argentina", + "unicode": "U+1F1E6 U+1F1F7", + "emoji": "🇦🇷", + "alpha2": "AR", + "dialCode": "54", + "alpha3": "ARG", + "region": "Americas", + "capital": "Buenos Aires", + "geo": { + "lat": -34, + "long": -34 + }, + "timezones": [ + "America/Argentina/Buenos_Aires", + "America/Argentina/Cordoba", + "America/Argentina/Salta", + "America/Argentina/Jujuy", + "America/Argentina/Tucuman", + "America/Argentina/Catamarca", + "America/Argentina/La_Rioja", + "America/Argentina/San_Juan", + "America/Argentina/Mendoza", + "America/Argentina/San_Luis", + "America/Argentina/Rio_Gallegos", + "America/Argentina/Ushuaia" + ] + }, + { + "name": "Armenia", + "unicode": "U+1F1E6 U+1F1F2", + "emoji": "🇦🇲", + "alpha2": "AM", + "dialCode": "374", + "alpha3": "ARM", + "region": "Asia", + "capital": "Yerevan", + "geo": { + "lat": 40, + "long": 40 + }, + "timezones": [ + "Asia/Yerevan" + ] + }, + { + "name": "Aruba", + "unicode": "U+1F1E6 U+1F1FC", + "emoji": "🇦🇼", + "alpha2": "AW", + "dialCode": "297", + "alpha3": "ABW", + "region": "Americas", + "capital": "Oranjestad", + "geo": { + "lat": 12.5, + "long": 12.5 + }, + "timezones": [ + "America/Aruba" + ] + }, + { + "name": "Australia", + "unicode": "U+1F1E6 U+1F1FA", + "emoji": "🇦🇺", + "alpha2": "AU", + "dialCode": "61", + "alpha3": "AUS", + "region": "Oceania", + "capital": "Canberra", + "geo": { + "lat": -27, + "long": -27 + }, + "timezones": [ + "Australia/Lord_Howe", + "Antarctica/Macquarie", + "Australia/Hobart", + "Australia/Currie", + "Australia/Melbourne", + "Australia/Sydney", + "Australia/Broken_Hill", + "Australia/Brisbane", + "Australia/Lindeman", + "Australia/Adelaide", + "Australia/Darwin", + "Australia/Perth", + "Australia/Eucla" + ] + }, + { + "name": "Austria", + "unicode": "U+1F1E6 U+1F1F9", + "emoji": "🇦🇹", + "alpha2": "AT", + "dialCode": "43", + "alpha3": "AUT", + "region": "Europe", + "capital": "Vienna", + "geo": { + "lat": 47.33333333, + "long": 47.33333333 + }, + "timezones": [ + "Europe/Vienna" + ] + }, + { + "name": "Azerbaijan", + "unicode": "U+1F1E6 U+1F1FF", + "emoji": "🇦🇿", + "alpha2": "AZ", + "dialCode": "994", + "alpha3": "AZE", + "region": "Asia", + "capital": "Baku", + "geo": { + "lat": 40.5, + "long": 40.5 + }, + "timezones": [ + "Asia/Baku" + ] + }, + { + "name": "Bahamas", + "unicode": "U+1F1E7 U+1F1F8", + "emoji": "🇧🇸", + "alpha2": "BS", + "dialCode": "1 242", + "alpha3": "BHS", + "region": "Americas", + "capital": "Nassau", + "geo": { + "lat": 24.25, + "long": 24.25 + }, + "timezones": [ + "America/Nassau" + ] + }, + { + "name": "Bahrain", + "unicode": "U+1F1E7 U+1F1ED", + "emoji": "🇧🇭", + "alpha2": "BH", + "dialCode": "973", + "alpha3": "BHR", + "region": "Asia", + "capital": "Manama", + "geo": { + "lat": 26, + "long": 26 + }, + "timezones": [ + "Asia/Bahrain" + ] + }, + { + "name": "Bangladesh", + "unicode": "U+1F1E7 U+1F1E9", + "emoji": "🇧🇩", + "alpha2": "BD", + "dialCode": "880", + "alpha3": "BGD", + "region": "Asia", + "capital": "Dhaka", + "geo": { + "lat": 24, + "long": 24 + }, + "timezones": [ + "Asia/Dhaka" + ] + }, + { + "name": "Barbados", + "unicode": "U+1F1E7 U+1F1E7", + "emoji": "🇧🇧", + "alpha2": "BB", + "dialCode": "1 246", + "alpha3": "BRB", + "region": "Americas", + "capital": "Bridgetown", + "geo": { + "lat": 13.16666666, + "long": 13.16666666 + }, + "timezones": [ + "America/Barbados" + ] + }, + { + "name": "Belarus", + "unicode": "U+1F1E7 U+1F1FE", + "emoji": "🇧🇾", + "alpha2": "BY", + "dialCode": "375", + "alpha3": "BLR", + "region": "Europe", + "capital": "Minsk", + "geo": { + "lat": 53, + "long": 53 + }, + "timezones": [ + "Europe/Minsk" + ] + }, + { + "name": "Belgium", + "unicode": "U+1F1E7 U+1F1EA", + "emoji": "🇧🇪", + "alpha2": "BE", + "dialCode": "32", + "alpha3": "BEL", + "region": "Europe", + "capital": "Brussels", + "geo": { + "lat": 50.83333333, + "long": 50.83333333 + }, + "timezones": [ + "Europe/Brussels" + ] + }, + { + "name": "Belize", + "unicode": "U+1F1E7 U+1F1FF", + "emoji": "🇧🇿", + "alpha2": "BZ", + "dialCode": "501", + "alpha3": "BLZ", + "region": "Americas", + "capital": "Belmopan", + "geo": { + "lat": 17.25, + "long": 17.25 + }, + "timezones": [ + "America/Belize" + ] + }, + { + "name": "Benin", + "unicode": "U+1F1E7 U+1F1EF", + "emoji": "🇧🇯", + "alpha2": "BJ", + "dialCode": "229", + "alpha3": "BEN", + "region": "Africa", + "capital": "Porto-Novo", + "geo": { + "lat": 9.5, + "long": 9.5 + }, + "timezones": [ + "Africa/Porto-Novo" + ] + }, + { + "name": "Bermuda", + "unicode": "U+1F1E7 U+1F1F2", + "emoji": "🇧🇲", + "alpha2": "BM", + "dialCode": "1 441", + "alpha3": "BMU", + "region": "Americas", + "capital": "Hamilton", + "geo": { + "lat": 32.33333333, + "long": 32.33333333 + }, + "timezones": [ + "Atlantic/Bermuda" + ] + }, + { + "name": "Bhutan", + "unicode": "U+1F1E7 U+1F1F9", + "emoji": "🇧🇹", + "alpha2": "BT", + "dialCode": "975", + "alpha3": "BTN", + "region": "Asia", + "capital": "Thimphu", + "geo": { + "lat": 27.5, + "long": 27.5 + }, + "timezones": [ + "Asia/Thimphu" + ] + }, + { + "name": "Bolivia", + "unicode": "U+1F1E7 U+1F1F4", + "emoji": "🇧🇴", + "alpha2": "BO", + "dialCode": "591", + "alpha3": "BOL", + "region": "Americas", + "capital": "Sucre", + "geo": { + "lat": -17, + "long": -17 + }, + "timezones": [ + "America/La_Paz" + ] + }, + { + "name": "Bonaire, Sint Eustatius and Saba", + "unicode": "U+1F1E7 U+1F1F6", + "emoji": "🇧🇶", + "alpha2": "BQ", + "dialCode": "", + "alpha3": "BES", + "region": "Americas", + "geo": {}, + "capital": "", + "timezones": [] + }, + { + "name": "Bosnia and Herzegovina", + "unicode": "U+1F1E7 U+1F1E6", + "emoji": "🇧🇦", + "alpha2": "BA", + "dialCode": "387", + "alpha3": "BIH", + "region": "Europe", + "capital": "Sarajevo", + "geo": { + "lat": 44, + "long": 44 + }, + "timezones": [ + "Europe/Sarajevo" + ] + }, + { + "name": "Botswana", + "unicode": "U+1F1E7 U+1F1FC", + "emoji": "🇧🇼", + "alpha2": "BW", + "dialCode": "267", + "alpha3": "BWA", + "region": "Africa", + "capital": "Gaborone", + "geo": { + "lat": -22, + "long": -22 + }, + "timezones": [ + "Africa/Gaborone" + ] + }, + { + "name": "Bouvet Island", + "unicode": "U+1F1E7 U+1F1FB", + "emoji": "🇧🇻", + "alpha2": "BV", + "dialCode": "", + "alpha3": "BVT", + "region": "Americas", + "capital": null, + "geo": { + "lat": -54.43333333, + "long": -54.43333333 + }, + "timezones": [ + "Europe/Oslo" + ] + }, + { + "name": "Brazil", + "unicode": "U+1F1E7 U+1F1F7", + "emoji": "🇧🇷", + "alpha2": "BR", + "dialCode": "55", + "alpha3": "BRA", + "region": "Americas", + "capital": "Brasília", + "geo": { + "lat": -10, + "long": -10 + }, + "timezones": [ + "America/Noronha", + "America/Belem", + "America/Fortaleza", + "America/Recife", + "America/Araguaina", + "America/Maceio", + "America/Bahia", + "America/Sao_Paulo", + "America/Campo_Grande", + "America/Cuiaba", + "America/Santarem", + "America/Porto_Velho", + "America/Boa_Vista", + "America/Manaus", + "America/Eirunepe", + "America/Rio_Branco" + ] + }, + { + "name": "British Indian Ocean Territory", + "unicode": "U+1F1EE U+1F1F4", + "emoji": "🇮🇴", + "alpha2": "IO", + "dialCode": "246", + "alpha3": "IOT", + "region": "Africa", + "capital": "Diego Garcia", + "geo": { + "lat": -6, + "long": -6 + }, + "timezones": [ + "Indian/Chagos" + ] + }, + { + "name": "Brunei Darussalam", + "unicode": "U+1F1E7 U+1F1F3", + "emoji": "🇧🇳", + "alpha2": "BN", + "dialCode": "673", + "alpha3": "BRN", + "region": "Asia", + "capital": "Bandar Seri Begawan", + "geo": { + "lat": 4.5, + "long": 4.5 + }, + "timezones": [ + "Asia/Brunei" + ] + }, + { + "name": "Bulgaria", + "unicode": "U+1F1E7 U+1F1EC", + "emoji": "🇧🇬", + "alpha2": "BG", + "dialCode": "359", + "alpha3": "BGR", + "region": "Europe", + "capital": "Sofia", + "geo": { + "lat": 43, + "long": 43 + }, + "timezones": [ + "Europe/Sofia" + ] + }, + { + "name": "Burkina Faso", + "unicode": "U+1F1E7 U+1F1EB", + "emoji": "🇧🇫", + "alpha2": "BF", + "dialCode": "226", + "alpha3": "BFA", + "region": "Africa", + "capital": "Ouagadougou", + "geo": { + "lat": 13, + "long": 13 + }, + "timezones": [ + "Africa/Ouagadougou" + ] + }, + { + "name": "Burundi", + "unicode": "U+1F1E7 U+1F1EE", + "emoji": "🇧🇮", + "alpha2": "BI", + "dialCode": "257", + "alpha3": "BDI", + "region": "Africa", + "capital": "Bujumbura", + "geo": { + "lat": -3.5, + "long": -3.5 + }, + "timezones": [ + "Africa/Bujumbura" + ] + }, + { + "name": "Cambodia", + "unicode": "U+1F1F0 U+1F1ED", + "emoji": "🇰🇭", + "alpha2": "KH", + "dialCode": "855", + "alpha3": "KHM", + "region": "Asia", + "capital": "Phnom Penh", + "geo": { + "lat": 13, + "long": 13 + }, + "timezones": [ + "Asia/Phnom_Penh" + ] + }, + { + "name": "Cameroon", + "unicode": "U+1F1E8 U+1F1F2", + "emoji": "🇨🇲", + "alpha2": "CM", + "dialCode": "237", + "alpha3": "CMR", + "region": "Africa", + "capital": "Yaoundé", + "geo": { + "lat": 6, + "long": 6 + }, + "timezones": [ + "Africa/Douala" + ] + }, + { + "name": "Canada", + "unicode": "U+1F1E8 U+1F1E6", + "emoji": "🇨🇦", + "alpha2": "CA", + "dialCode": "1", + "alpha3": "CAN", + "region": "Americas", + "capital": "Ottawa", + "geo": { + "lat": 60, + "long": 60 + }, + "timezones": [ + "America/St_Johns", + "America/Halifax", + "America/Glace_Bay", + "America/Moncton", + "America/Goose_Bay", + "America/Blanc-Sablon", + "America/Toronto", + "America/Nipigon", + "America/Thunder_Bay", + "America/Iqaluit", + "America/Pangnirtung", + "America/Atikokan", + "America/Winnipeg", + "America/Rainy_River", + "America/Resolute", + "America/Rankin_Inlet", + "America/Regina", + "America/Swift_Current", + "America/Edmonton", + "America/Cambridge_Bay", + "America/Yellowknife", + "America/Inuvik", + "America/Creston", + "America/Dawson_Creek", + "America/Fort_Nelson", + "America/Vancouver", + "America/Whitehorse", + "America/Dawson" + ] + }, + { + "name": "Cape Verde", + "unicode": "U+1F1E8 U+1F1FB", + "emoji": "🇨🇻", + "alpha2": "CV", + "dialCode": "238", + "alpha3": "CPV", + "region": "Africa", + "capital": "Praia", + "geo": { + "lat": 16, + "long": 16 + }, + "timezones": [ + "Atlantic/Cape_Verde" + ] + }, + { + "name": "Cayman Islands", + "unicode": "U+1F1F0 U+1F1FE", + "emoji": "🇰🇾", + "alpha2": "KY", + "dialCode": " 345", + "alpha3": "CYM", + "region": "Americas", + "capital": "George Town", + "geo": { + "lat": 19.5, + "long": 19.5 + }, + "timezones": [ + "America/Cayman" + ] + }, + { + "name": "Central African Republic", + "unicode": "U+1F1E8 U+1F1EB", + "emoji": "🇨🇫", + "alpha2": "CF", + "dialCode": "236", + "alpha3": "CAF", + "region": "Africa", + "capital": "Bangui", + "geo": { + "lat": 7, + "long": 7 + }, + "timezones": [ + "Africa/Bangui" + ] + }, + { + "name": "Chad", + "unicode": "U+1F1F9 U+1F1E9", + "emoji": "🇹🇩", + "alpha2": "TD", + "dialCode": "235", + "alpha3": "TCD", + "region": "Africa", + "capital": "N'Djamena", + "geo": { + "lat": 15, + "long": 15 + }, + "timezones": [ + "Africa/Ndjamena" + ] + }, + { + "name": "Chile", + "unicode": "U+1F1E8 U+1F1F1", + "emoji": "🇨🇱", + "alpha2": "CL", + "dialCode": "56", + "alpha3": "CHL", + "region": "Americas", + "capital": "Santiago", + "geo": { + "lat": -30, + "long": -30 + }, + "timezones": [ + "America/Santiago", + "Pacific/Easter" + ] + }, + { + "name": "China", + "unicode": "U+1F1E8 U+1F1F3", + "emoji": "🇨🇳", + "alpha2": "CN", + "dialCode": "86", + "alpha3": "CHN", + "region": "Asia", + "capital": "Beijing", + "geo": { + "lat": 35, + "long": 35 + }, + "timezones": [ + "Asia/Shanghai", + "Asia/Urumqi" + ] + }, + { + "name": "Christmas Island", + "unicode": "U+1F1E8 U+1F1FD", + "emoji": "🇨🇽", + "alpha2": "CX", + "dialCode": "61", + "alpha3": "CXR", + "region": "Oceania", + "capital": "Flying Fish Cove", + "geo": { + "lat": -10.5, + "long": -10.5 + }, + "timezones": [ + "Indian/Christmas" + ] + }, + { + "name": "Cocos (Keeling) Islands", + "unicode": "U+1F1E8 U+1F1E8", + "emoji": "🇨🇨", + "alpha2": "CC", + "dialCode": "61", + "alpha3": "CCK", + "region": "Oceania", + "capital": "West Island", + "geo": { + "lat": -12.5, + "long": -12.5 + }, + "timezones": [ + "Indian/Cocos" + ] + }, + { + "name": "Colombia", + "unicode": "U+1F1E8 U+1F1F4", + "emoji": "🇨🇴", + "alpha2": "CO", + "dialCode": "57", + "alpha3": "COL", + "region": "Americas", + "capital": "Bogotá", + "geo": { + "lat": 4, + "long": 4 + }, + "timezones": [ + "America/Bogota" + ] + }, + { + "name": "Comoros", + "unicode": "U+1F1F0 U+1F1F2", + "emoji": "🇰🇲", + "alpha2": "KM", + "dialCode": "269", + "alpha3": "COM", + "region": "Africa", + "capital": "Moroni", + "geo": { + "lat": -12.16666666, + "long": -12.16666666 + }, + "timezones": [ + "Indian/Comoro" + ] + }, + { + "name": "Congo", + "unicode": "U+1F1E8 U+1F1E9", + "emoji": "🇨🇩", + "alpha2": "CD", + "dialCode": "243", + "alpha3": "COD", + "region": "Africa", + "capital": "Kinshasa", + "geo": { + "lat": 0, + "long": 0 + }, + "timezones": [ + "Africa/Kinshasa", + "Africa/Lubumbashi" + ] + }, + { + "name": "Congo", + "unicode": "U+1F1E8 U+1F1EC", + "emoji": "🇨🇬", + "alpha2": "CG", + "dialCode": "242", + "alpha3": "COG", + "region": "Africa", + "capital": "Brazzaville", + "geo": { + "lat": -1, + "long": -1 + }, + "timezones": [ + "Africa/Brazzaville" + ] + }, + { + "name": "Cook Islands", + "unicode": "U+1F1E8 U+1F1F0", + "emoji": "🇨🇰", + "alpha2": "CK", + "dialCode": "682", + "alpha3": "COK", + "region": "Oceania", + "capital": "Avarua", + "geo": { + "lat": -21.23333333, + "long": -21.23333333 + }, + "timezones": [ + "Pacific/Rarotonga" + ] + }, + { + "name": "Costa Rica", + "unicode": "U+1F1E8 U+1F1F7", + "emoji": "🇨🇷", + "alpha2": "CR", + "dialCode": "506", + "alpha3": "CRI", + "region": "Americas", + "capital": "San José", + "geo": { + "lat": 10, + "long": 10 + }, + "timezones": [ + "America/Costa_Rica" + ] + }, + { + "name": "Croatia", + "unicode": "U+1F1ED U+1F1F7", + "emoji": "🇭🇷", + "alpha2": "HR", + "dialCode": "385", + "alpha3": "HRV", + "region": "Europe", + "capital": "Zagreb", + "geo": { + "lat": 45.16666666, + "long": 45.16666666 + }, + "timezones": [ + "Europe/Zagreb" + ] + }, + { + "name": "Cuba", + "unicode": "U+1F1E8 U+1F1FA", + "emoji": "🇨🇺", + "alpha2": "CU", + "dialCode": "53", + "alpha3": "CUB", + "region": "Americas", + "capital": "Havana", + "geo": { + "lat": 21.5, + "long": 21.5 + }, + "timezones": [ + "America/Havana" + ] + }, + { + "name": "Curaçao", + "unicode": "U+1F1E8 U+1F1FC", + "emoji": "🇨🇼", + "alpha2": "CW", + "dialCode": "", + "alpha3": "CUW", + "region": "Americas", + "capital": "Willemstad", + "geo": { + "lat": 12.116667, + "long": 12.116667 + }, + "timezones": [ + "America/Curacao" + ] + }, + { + "name": "Cyprus", + "unicode": "U+1F1E8 U+1F1FE", + "emoji": "🇨🇾", + "alpha2": "CY", + "dialCode": "537", + "alpha3": "CYP", + "region": "Asia", + "capital": "Nicosia", + "geo": { + "lat": 35, + "long": 35 + }, + "timezones": [ + "Asia/Nicosia" + ] + }, + { + "name": "Czech Republic", + "unicode": "U+1F1E8 U+1F1FF", + "emoji": "🇨🇿", + "alpha2": "CZ", + "dialCode": "420", + "alpha3": "CZE", + "region": "Europe", + "capital": "Prague", + "geo": { + "lat": 49.75, + "long": 49.75 + }, + "timezones": [ + "Europe/Prague" + ] + }, + { + "name": "Côte D'Ivoire", + "unicode": "U+1F1E8 U+1F1EE", + "emoji": "🇨🇮", + "alpha2": "CI", + "dialCode": "225", + "alpha3": "CIV", + "region": "Africa", + "capital": "Yamoussoukro", + "geo": { + "lat": 8, + "long": 8 + }, + "timezones": [ + "Africa/Abidjan" + ] + }, + { + "name": "Denmark", + "unicode": "U+1F1E9 U+1F1F0", + "emoji": "🇩🇰", + "alpha2": "DK", + "dialCode": "45", + "alpha3": "DNK", + "region": "Europe", + "capital": "Copenhagen", + "geo": { + "lat": 56, + "long": 56 + }, + "timezones": [ + "Europe/Copenhagen" + ] + }, + { + "name": "Djibouti", + "unicode": "U+1F1E9 U+1F1EF", + "emoji": "🇩🇯", + "alpha2": "DJ", + "dialCode": "253", + "alpha3": "DJI", + "region": "Africa", + "capital": "Djibouti", + "geo": { + "lat": 11.5, + "long": 11.5 + }, + "timezones": [ + "Africa/Djibouti" + ] + }, + { + "name": "Dominica", + "unicode": "U+1F1E9 U+1F1F2", + "emoji": "🇩🇲", + "alpha2": "DM", + "dialCode": "1 767", + "alpha3": "DMA", + "region": "Americas", + "capital": "Roseau", + "geo": { + "lat": 15.41666666, + "long": 15.41666666 + }, + "timezones": [ + "America/Dominica" + ] + }, + { + "name": "Dominican Republic", + "unicode": "U+1F1E9 U+1F1F4", + "emoji": "🇩🇴", + "alpha2": "DO", + "dialCode": "1 849", + "alpha3": "DOM", + "region": "Americas", + "capital": "Santo Domingo", + "geo": { + "lat": 19, + "long": 19 + }, + "timezones": [ + "America/Santo_Domingo" + ] + }, + { + "name": "Ecuador", + "unicode": "U+1F1EA U+1F1E8", + "emoji": "🇪🇨", + "alpha2": "EC", + "dialCode": "593", + "alpha3": "ECU", + "region": "Americas", + "capital": "Quito", + "geo": { + "lat": -2, + "long": -2 + }, + "timezones": [ + "America/Guayaquil", + "Pacific/Galapagos" + ] + }, + { + "name": "Egypt", + "unicode": "U+1F1EA U+1F1EC", + "emoji": "🇪🇬", + "alpha2": "EG", + "dialCode": "20", + "alpha3": "EGY", + "region": "Africa", + "capital": "Cairo", + "geo": { + "lat": 27, + "long": 27 + }, + "timezones": [ + "Africa/Cairo" + ] + }, + { + "name": "El Salvador", + "unicode": "U+1F1F8 U+1F1FB", + "emoji": "🇸🇻", + "alpha2": "SV", + "dialCode": "503", + "alpha3": "SLV", + "region": "Americas", + "capital": "San Salvador", + "geo": { + "lat": 13.83333333, + "long": 13.83333333 + }, + "timezones": [ + "America/El_Salvador" + ] + }, + { + "name": "Equatorial Guinea", + "unicode": "U+1F1EC U+1F1F6", + "emoji": "🇬🇶", + "alpha2": "GQ", + "dialCode": "240", + "alpha3": "GNQ", + "region": "Africa", + "capital": "Malabo", + "geo": { + "lat": 2, + "long": 2 + }, + "timezones": [ + "Africa/Malabo" + ] + }, + { + "name": "Eritrea", + "unicode": "U+1F1EA U+1F1F7", + "emoji": "🇪🇷", + "alpha2": "ER", + "dialCode": "291", + "alpha3": "ERI", + "region": "Africa", + "capital": "Asmara", + "geo": { + "lat": 15, + "long": 15 + }, + "timezones": [ + "Africa/Asmara" + ] + }, + { + "name": "Estonia", + "unicode": "U+1F1EA U+1F1EA", + "emoji": "🇪🇪", + "alpha2": "EE", + "dialCode": "372", + "alpha3": "EST", + "region": "Europe", + "capital": "Tallinn", + "geo": { + "lat": 59, + "long": 59 + }, + "timezones": [ + "Europe/Tallinn" + ] + }, + { + "name": "Ethiopia", + "unicode": "U+1F1EA U+1F1F9", + "emoji": "🇪🇹", + "alpha2": "ET", + "dialCode": "251", + "alpha3": "ETH", + "region": "Africa", + "capital": "Addis Ababa", + "geo": { + "lat": 8, + "long": 8 + }, + "timezones": [ + "Africa/Addis_Ababa" + ] + }, + { + "name": "European Union", + "unicode": "U+1F1EA U+1F1FA", + "emoji": "🇪🇺", + "alpha2": "EU", + "dialCode": "", + "alpha3": "", + "region": "", + "geo": {}, + "capital": "", + "timezones": [] + }, + { + "name": "Falkland Islands (Malvinas)", + "unicode": "U+1F1EB U+1F1F0", + "emoji": "🇫🇰", + "alpha2": "FK", + "dialCode": "500", + "alpha3": "FLK", + "region": "Americas", + "capital": "Stanley", + "geo": { + "lat": -51.75, + "long": -51.75 + }, + "timezones": [ + "Atlantic/Stanley" + ] + }, + { + "name": "Faroe Islands", + "unicode": "U+1F1EB U+1F1F4", + "emoji": "🇫🇴", + "alpha2": "FO", + "dialCode": "298", + "alpha3": "FRO", + "region": "Europe", + "capital": "Tórshavn", + "geo": { + "lat": 62, + "long": 62 + }, + "timezones": [ + "Atlantic/Faroe" + ] + }, + { + "name": "Fiji", + "unicode": "U+1F1EB U+1F1EF", + "emoji": "🇫🇯", + "alpha2": "FJ", + "dialCode": "679", + "alpha3": "FJI", + "region": "Oceania", + "capital": "Suva", + "geo": { + "lat": -18, + "long": -18 + }, + "timezones": [ + "Pacific/Fiji" + ] + }, + { + "name": "Finland", + "unicode": "U+1F1EB U+1F1EE", + "emoji": "🇫🇮", + "alpha2": "FI", + "dialCode": "358", + "alpha3": "FIN", + "region": "Europe", + "capital": "Helsinki", + "geo": { + "lat": 64, + "long": 64 + }, + "timezones": [ + "Europe/Helsinki" + ] + }, + { + "name": "France", + "unicode": "U+1F1EB U+1F1F7", + "emoji": "🇫🇷", + "alpha2": "FR", + "dialCode": "33", + "alpha3": "FRA", + "region": "Europe", + "capital": "Paris", + "geo": { + "lat": 46, + "long": 46 + }, + "timezones": [ + "Europe/Paris" + ] + }, + { + "name": "French Guiana", + "unicode": "U+1F1EC U+1F1EB", + "emoji": "🇬🇫", + "alpha2": "GF", + "dialCode": "594", + "alpha3": "GUF", + "region": "Americas", + "capital": "Cayenne", + "geo": { + "lat": 4, + "long": 4 + }, + "timezones": [ + "America/Cayenne" + ] + }, + { + "name": "French Polynesia", + "unicode": "U+1F1F5 U+1F1EB", + "emoji": "🇵🇫", + "alpha2": "PF", + "dialCode": "689", + "alpha3": "PYF", + "region": "Oceania", + "capital": "Papeetē", + "geo": { + "lat": -15, + "long": -15 + }, + "timezones": [ + "Pacific/Tahiti", + "Pacific/Marquesas", + "Pacific/Gambier" + ] + }, + { + "name": "French Southern Territories", + "unicode": "U+1F1F9 U+1F1EB", + "emoji": "🇹🇫", + "alpha2": "TF", + "dialCode": "", + "alpha3": "ATF", + "region": "Africa", + "capital": "Port-aux-Français", + "geo": { + "lat": -49.25, + "long": -49.25 + }, + "timezones": [ + "Indian/Kerguelen" + ] + }, + { + "name": "Gabon", + "unicode": "U+1F1EC U+1F1E6", + "emoji": "🇬🇦", + "alpha2": "GA", + "dialCode": "241", + "alpha3": "GAB", + "region": "Africa", + "capital": "Libreville", + "geo": { + "lat": -1, + "long": -1 + }, + "timezones": [ + "Africa/Libreville" + ] + }, + { + "name": "Gambia", + "unicode": "U+1F1EC U+1F1F2", + "emoji": "🇬🇲", + "alpha2": "GM", + "dialCode": "220", + "alpha3": "GMB", + "region": "Africa", + "capital": "Banjul", + "geo": { + "lat": 13.46666666, + "long": 13.46666666 + }, + "timezones": [ + "Africa/Banjul" + ] + }, + { + "name": "Georgia", + "unicode": "U+1F1EC U+1F1EA", + "emoji": "🇬🇪", + "alpha2": "GE", + "dialCode": "995", + "alpha3": "GEO", + "region": "Asia", + "capital": "Tbilisi", + "geo": { + "lat": 42, + "long": 42 + }, + "timezones": [ + "Asia/Tbilisi" + ] + }, + { + "name": "Germany", + "unicode": "U+1F1E9 U+1F1EA", + "emoji": "🇩🇪", + "alpha2": "DE", + "dialCode": "49", + "alpha3": "DEU", + "region": "Europe", + "capital": "Berlin", + "geo": { + "lat": 51, + "long": 51 + }, + "timezones": [ + "Europe/Berlin", + "Europe/Busingen" + ] + }, + { + "name": "Ghana", + "unicode": "U+1F1EC U+1F1ED", + "emoji": "🇬🇭", + "alpha2": "GH", + "dialCode": "233", + "alpha3": "GHA", + "region": "Africa", + "capital": "Accra", + "geo": { + "lat": 8, + "long": 8 + }, + "timezones": [ + "Africa/Accra" + ] + }, + { + "name": "Gibraltar", + "unicode": "U+1F1EC U+1F1EE", + "emoji": "🇬🇮", + "alpha2": "GI", + "dialCode": "350", + "alpha3": "GIB", + "region": "Europe", + "capital": "Gibraltar", + "geo": { + "lat": 36.13333333, + "long": 36.13333333 + }, + "timezones": [ + "Europe/Gibraltar" + ] + }, + { + "name": "Greece", + "unicode": "U+1F1EC U+1F1F7", + "emoji": "🇬🇷", + "alpha2": "GR", + "dialCode": "30", + "alpha3": "GRC", + "region": "Europe", + "capital": "Athens", + "geo": { + "lat": 39, + "long": 39 + }, + "timezones": [ + "Europe/Athens" + ] + }, + { + "name": "Greenland", + "unicode": "U+1F1EC U+1F1F1", + "emoji": "🇬🇱", + "alpha2": "GL", + "dialCode": "299", + "alpha3": "GRL", + "region": "Americas", + "capital": "Nuuk", + "geo": { + "lat": 72, + "long": 72 + }, + "timezones": [ + "America/Godthab", + "America/Danmarkshavn", + "America/Scoresbysund", + "America/Thule" + ] + }, + { + "name": "Grenada", + "unicode": "U+1F1EC U+1F1E9", + "emoji": "🇬🇩", + "alpha2": "GD", + "dialCode": "1 473", + "alpha3": "GRD", + "region": "Americas", + "capital": "St. George's", + "geo": { + "lat": 12.11666666, + "long": 12.11666666 + }, + "timezones": [ + "America/Grenada" + ] + }, + { + "name": "Guadeloupe", + "unicode": "U+1F1EC U+1F1F5", + "emoji": "🇬🇵", + "alpha2": "GP", + "dialCode": "590", + "alpha3": "GLP", + "region": "Americas", + "capital": "Basse-Terre", + "geo": { + "lat": 16.25, + "long": 16.25 + }, + "timezones": [ + "America/Guadeloupe" + ] + }, + { + "name": "Guam", + "unicode": "U+1F1EC U+1F1FA", + "emoji": "🇬🇺", + "alpha2": "GU", + "dialCode": "1 671", + "alpha3": "GUM", + "region": "Oceania", + "capital": "Hagåtña", + "geo": { + "lat": 13.46666666, + "long": 13.46666666 + }, + "timezones": [ + "Pacific/Guam" + ] + }, + { + "name": "Guatemala", + "unicode": "U+1F1EC U+1F1F9", + "emoji": "🇬🇹", + "alpha2": "GT", + "dialCode": "502", + "alpha3": "GTM", + "region": "Americas", + "capital": "Guatemala City", + "geo": { + "lat": 15.5, + "long": 15.5 + }, + "timezones": [ + "America/Guatemala" + ] + }, + { + "name": "Guernsey", + "unicode": "U+1F1EC U+1F1EC", + "emoji": "🇬🇬", + "alpha2": "GG", + "dialCode": "44", + "alpha3": "GGY", + "region": "Europe", + "capital": "St. Peter Port", + "geo": { + "lat": 49.46666666, + "long": 49.46666666 + }, + "timezones": [ + "Europe/Guernsey" + ] + }, + { + "name": "Guinea", + "unicode": "U+1F1EC U+1F1F3", + "emoji": "🇬🇳", + "alpha2": "GN", + "dialCode": "224", + "alpha3": "GIN", + "region": "Africa", + "capital": "Conakry", + "geo": { + "lat": 11, + "long": 11 + }, + "timezones": [ + "Africa/Conakry" + ] + }, + { + "name": "Guinea-Bissau", + "unicode": "U+1F1EC U+1F1FC", + "emoji": "🇬🇼", + "alpha2": "GW", + "dialCode": "245", + "alpha3": "GNB", + "region": "Africa", + "capital": "Bissau", + "geo": { + "lat": 12, + "long": 12 + }, + "timezones": [ + "Africa/Bissau" + ] + }, + { + "name": "Guyana", + "unicode": "U+1F1EC U+1F1FE", + "emoji": "🇬🇾", + "alpha2": "GY", + "dialCode": "595", + "alpha3": "GUY", + "region": "Americas", + "capital": "Georgetown", + "geo": { + "lat": 5, + "long": 5 + }, + "timezones": [ + "America/Guyana" + ] + }, + { + "name": "Haiti", + "unicode": "U+1F1ED U+1F1F9", + "emoji": "🇭🇹", + "alpha2": "HT", + "dialCode": "509", + "alpha3": "HTI", + "region": "Americas", + "capital": "Port-au-Prince", + "geo": { + "lat": 19, + "long": 19 + }, + "timezones": [ + "America/Port-au-Prince" + ] + }, + { + "name": "Heard Island and Mcdonald Islands", + "unicode": "U+1F1ED U+1F1F2", + "emoji": "🇭🇲", + "alpha2": "HM", + "dialCode": "", + "alpha3": "HMD", + "region": "Oceania", + "geo": {}, + "capital": "", + "timezones": [] + }, + { + "name": "Honduras", + "unicode": "U+1F1ED U+1F1F3", + "emoji": "🇭🇳", + "alpha2": "HN", + "dialCode": "504", + "alpha3": "HND", + "region": "Americas", + "capital": "Tegucigalpa", + "geo": { + "lat": 15, + "long": 15 + }, + "timezones": [ + "America/Tegucigalpa" + ] + }, + { + "name": "Hong Kong", + "unicode": "U+1F1ED U+1F1F0", + "emoji": "🇭🇰", + "alpha2": "HK", + "dialCode": "852", + "alpha3": "HKG", + "region": "Asia", + "capital": "City of Victoria", + "geo": { + "lat": 22.267, + "long": 22.267 + }, + "timezones": [ + "Asia/Hong_Kong" + ] + }, + { + "name": "Hungary", + "unicode": "U+1F1ED U+1F1FA", + "emoji": "🇭🇺", + "alpha2": "HU", + "dialCode": "36", + "alpha3": "HUN", + "region": "Europe", + "capital": "Budapest", + "geo": { + "lat": 47, + "long": 47 + }, + "timezones": [ + "Europe/Budapest" + ] + }, + { + "name": "Iceland", + "unicode": "U+1F1EE U+1F1F8", + "emoji": "🇮🇸", + "alpha2": "IS", + "dialCode": "354", + "alpha3": "ISL", + "region": "Europe", + "capital": "Reykjavik", + "geo": { + "lat": 65, + "long": 65 + }, + "timezones": [ + "Atlantic/Reykjavik" + ] + }, + { + "name": "India", + "unicode": "U+1F1EE U+1F1F3", + "emoji": "🇮🇳", + "alpha2": "IN", + "dialCode": "91", + "alpha3": "IND", + "region": "Asia", + "capital": "New Delhi", + "geo": { + "lat": 20, + "long": 20 + }, + "timezones": [ + "Asia/Kolkata" + ] + }, + { + "name": "Indonesia", + "unicode": "U+1F1EE U+1F1E9", + "emoji": "🇮🇩", + "alpha2": "ID", + "dialCode": "62", + "alpha3": "IDN", + "region": "Asia", + "capital": "Jakarta", + "geo": { + "lat": -5, + "long": -5 + }, + "timezones": [ + "Asia/Jakarta", + "Asia/Pontianak", + "Asia/Makassar", + "Asia/Jayapura" + ] + }, + { + "name": "Iran", + "unicode": "U+1F1EE U+1F1F7", + "emoji": "🇮🇷", + "alpha2": "IR", + "dialCode": "98", + "alpha3": "IRN", + "region": "Asia", + "capital": "Tehran", + "geo": { + "lat": 32, + "long": 32 + }, + "timezones": [ + "Asia/Tehran" + ] + }, + { + "name": "Iraq", + "unicode": "U+1F1EE U+1F1F6", + "emoji": "🇮🇶", + "alpha2": "IQ", + "dialCode": "964", + "alpha3": "IRQ", + "region": "Asia", + "capital": "Baghdad", + "geo": { + "lat": 33, + "long": 33 + }, + "timezones": [ + "Asia/Baghdad" + ] + }, + { + "name": "Ireland", + "unicode": "U+1F1EE U+1F1EA", + "emoji": "🇮🇪", + "alpha2": "IE", + "dialCode": "353", + "alpha3": "IRL", + "region": "Europe", + "capital": "Dublin", + "geo": { + "lat": 53, + "long": 53 + }, + "timezones": [ + "Europe/Dublin" + ] + }, + { + "name": "Isle of Man", + "unicode": "U+1F1EE U+1F1F2", + "emoji": "🇮🇲", + "alpha2": "IM", + "dialCode": "44", + "alpha3": "IMN", + "region": "Europe", + "capital": "Douglas", + "geo": { + "lat": 54.25, + "long": 54.25 + }, + "timezones": [ + "Europe/Isle_of_Man" + ] + }, + { + "name": "Israel", + "unicode": "U+1F1EE U+1F1F1", + "emoji": "🇮🇱", + "alpha2": "IL", + "dialCode": "972", + "alpha3": "ISR", + "region": "Asia", + "capital": "Jerusalem", + "geo": { + "lat": 31.47, + "long": 31.47 + }, + "timezones": [ + "Asia/Jerusalem" + ] + }, + { + "name": "Italy", + "unicode": "U+1F1EE U+1F1F9", + "emoji": "🇮🇹", + "alpha2": "IT", + "dialCode": "39", + "alpha3": "ITA", + "region": "Europe", + "capital": "Rome", + "geo": { + "lat": 42.83333333, + "long": 42.83333333 + }, + "timezones": [ + "Europe/Rome" + ] + }, + { + "name": "Jamaica", + "unicode": "U+1F1EF U+1F1F2", + "emoji": "🇯🇲", + "alpha2": "JM", + "dialCode": "1 876", + "alpha3": "JAM", + "region": "Americas", + "capital": "Kingston", + "geo": { + "lat": 18.25, + "long": 18.25 + }, + "timezones": [ + "America/Jamaica" + ] + }, + { + "name": "Japan", + "unicode": "U+1F1EF U+1F1F5", + "emoji": "🇯🇵", + "alpha2": "JP", + "dialCode": "81", + "alpha3": "JPN", + "region": "Asia", + "capital": "Tokyo", + "geo": { + "lat": 36, + "long": 36 + }, + "timezones": [ + "Asia/Tokyo" + ] + }, + { + "name": "Jersey", + "unicode": "U+1F1EF U+1F1EA", + "emoji": "🇯🇪", + "alpha2": "JE", + "dialCode": "44", + "alpha3": "JEY", + "region": "Europe", + "capital": "Saint Helier", + "geo": { + "lat": 49.25, + "long": 49.25 + }, + "timezones": [ + "Europe/Jersey" + ] + }, + { + "name": "Jordan", + "unicode": "U+1F1EF U+1F1F4", + "emoji": "🇯🇴", + "alpha2": "JO", + "dialCode": "962", + "alpha3": "JOR", + "region": "Asia", + "capital": "Amman", + "geo": { + "lat": 31, + "long": 31 + }, + "timezones": [ + "Asia/Amman" + ] + }, + { + "name": "Kazakhstan", + "unicode": "U+1F1F0 U+1F1FF", + "emoji": "🇰🇿", + "alpha2": "KZ", + "dialCode": "7 7", + "alpha3": "KAZ", + "region": "Asia", + "capital": "Astana", + "geo": { + "lat": 48, + "long": 48 + }, + "timezones": [ + "Asia/Almaty", + "Asia/Qyzylorda", + "Asia/Aqtobe", + "Asia/Aqtau", + "Asia/Oral" + ] + }, + { + "name": "Kenya", + "unicode": "U+1F1F0 U+1F1EA", + "emoji": "🇰🇪", + "alpha2": "KE", + "dialCode": "254", + "alpha3": "KEN", + "region": "Africa", + "capital": "Nairobi", + "geo": { + "lat": 1, + "long": 1 + }, + "timezones": [ + "Africa/Nairobi" + ] + }, + { + "name": "Kiribati", + "unicode": "U+1F1F0 U+1F1EE", + "emoji": "🇰🇮", + "alpha2": "KI", + "dialCode": "686", + "alpha3": "KIR", + "region": "Oceania", + "capital": "South Tarawa", + "geo": { + "lat": 1.41666666, + "long": 1.41666666 + }, + "timezones": [ + "Pacific/Tarawa", + "Pacific/Enderbury", + "Pacific/Kiritimati" + ] + }, + { + "name": "Kosovo", + "unicode": "U+1F1FD U+1F1F0", + "emoji": "🇽🇰", + "alpha2": "XK", + "dialCode": "383", + "alpha3": "", + "region": "", + "capital": "Pristina", + "geo": { + "lat": 42.666667, + "long": 42.666667 + }, + "timezones": [ + "Europe/Belgrade" + ] + }, + { + "name": "Kuwait", + "unicode": "U+1F1F0 U+1F1FC", + "emoji": "🇰🇼", + "alpha2": "KW", + "dialCode": "965", + "alpha3": "KWT", + "region": "Asia", + "capital": "Kuwait City", + "geo": { + "lat": 29.5, + "long": 29.5 + }, + "timezones": [ + "Asia/Kuwait" + ] + }, + { + "name": "Kyrgyzstan", + "unicode": "U+1F1F0 U+1F1EC", + "emoji": "🇰🇬", + "alpha2": "KG", + "dialCode": "996", + "alpha3": "KGZ", + "region": "Asia", + "capital": "Bishkek", + "geo": { + "lat": 41, + "long": 41 + }, + "timezones": [ + "Asia/Bishkek" + ] + }, + { + "name": "Lao People's Democratic Republic", + "unicode": "U+1F1F1 U+1F1E6", + "emoji": "🇱🇦", + "alpha2": "LA", + "dialCode": "856", + "alpha3": "LAO", + "region": "Asia", + "capital": "Vientiane", + "geo": { + "lat": 18, + "long": 18 + }, + "timezones": [ + "Asia/Vientiane" + ] + }, + { + "name": "Latvia", + "unicode": "U+1F1F1 U+1F1FB", + "emoji": "🇱🇻", + "alpha2": "LV", + "dialCode": "371", + "alpha3": "LVA", + "region": "Europe", + "capital": "Riga", + "geo": { + "lat": 57, + "long": 57 + }, + "timezones": [ + "Europe/Riga" + ] + }, + { + "name": "Lebanon", + "unicode": "U+1F1F1 U+1F1E7", + "emoji": "🇱🇧", + "alpha2": "LB", + "dialCode": "961", + "alpha3": "LBN", + "region": "Asia", + "capital": "Beirut", + "geo": { + "lat": 33.83333333, + "long": 33.83333333 + }, + "timezones": [ + "Asia/Beirut" + ] + }, + { + "name": "Lesotho", + "unicode": "U+1F1F1 U+1F1F8", + "emoji": "🇱🇸", + "alpha2": "LS", + "dialCode": "266", + "alpha3": "LSO", + "region": "Africa", + "capital": "Maseru", + "geo": { + "lat": -29.5, + "long": -29.5 + }, + "timezones": [ + "Africa/Maseru" + ] + }, + { + "name": "Liberia", + "unicode": "U+1F1F1 U+1F1F7", + "emoji": "🇱🇷", + "alpha2": "LR", + "dialCode": "231", + "alpha3": "LBR", + "region": "Africa", + "capital": "Monrovia", + "geo": { + "lat": 6.5, + "long": 6.5 + }, + "timezones": [ + "Africa/Monrovia" + ] + }, + { + "name": "Libya", + "unicode": "U+1F1F1 U+1F1FE", + "emoji": "🇱🇾", + "alpha2": "LY", + "dialCode": "218", + "alpha3": "LBY", + "region": "Africa", + "capital": "Tripoli", + "geo": { + "lat": 25, + "long": 25 + }, + "timezones": [ + "Africa/Tripoli" + ] + }, + { + "name": "Liechtenstein", + "unicode": "U+1F1F1 U+1F1EE", + "emoji": "🇱🇮", + "alpha2": "LI", + "dialCode": "423", + "alpha3": "LIE", + "region": "Europe", + "capital": "Vaduz", + "geo": { + "lat": 47.26666666, + "long": 47.26666666 + }, + "timezones": [ + "Europe/Vaduz" + ] + }, + { + "name": "Lithuania", + "unicode": "U+1F1F1 U+1F1F9", + "emoji": "🇱🇹", + "alpha2": "LT", + "dialCode": "370", + "alpha3": "LTU", + "region": "Europe", + "capital": "Vilnius", + "geo": { + "lat": 56, + "long": 56 + }, + "timezones": [ + "Europe/Vilnius" + ] + }, + { + "name": "Luxembourg", + "unicode": "U+1F1F1 U+1F1FA", + "emoji": "🇱🇺", + "alpha2": "LU", + "dialCode": "352", + "alpha3": "LUX", + "region": "Europe", + "capital": "Luxembourg", + "geo": { + "lat": 49.75, + "long": 49.75 + }, + "timezones": [ + "Europe/Luxembourg" + ] + }, + { + "name": "Macao", + "unicode": "U+1F1F2 U+1F1F4", + "emoji": "🇲🇴", + "alpha2": "MO", + "dialCode": "853", + "alpha3": "MAC", + "region": "Asia", + "capital": null, + "geo": { + "lat": 22.16666666, + "long": 22.16666666 + }, + "timezones": [ + "Asia/Macau" + ] + }, + { + "name": "Macedonia", + "unicode": "U+1F1F2 U+1F1F0", + "emoji": "🇲🇰", + "alpha2": "MK", + "dialCode": "389", + "alpha3": "MKD", + "region": "Europe", + "capital": "Skopje", + "geo": { + "lat": 41.83333333, + "long": 41.83333333 + }, + "timezones": [ + "Europe/Skopje" + ] + }, + { + "name": "Madagascar", + "unicode": "U+1F1F2 U+1F1EC", + "emoji": "🇲🇬", + "alpha2": "MG", + "dialCode": "261", + "alpha3": "MDG", + "region": "Africa", + "capital": "Antananarivo", + "geo": { + "lat": -20, + "long": -20 + }, + "timezones": [ + "Indian/Antananarivo" + ] + }, + { + "name": "Malawi", + "unicode": "U+1F1F2 U+1F1FC", + "emoji": "🇲🇼", + "alpha2": "MW", + "dialCode": "265", + "alpha3": "MWI", + "region": "Africa", + "capital": "Lilongwe", + "geo": { + "lat": -13.5, + "long": -13.5 + }, + "timezones": [ + "Africa/Blantyre" + ] + }, + { + "name": "Malaysia", + "unicode": "U+1F1F2 U+1F1FE", + "emoji": "🇲🇾", + "alpha2": "MY", + "dialCode": "60", + "alpha3": "MYS", + "region": "Asia", + "capital": "Kuala Lumpur", + "geo": { + "lat": 2.5, + "long": 2.5 + }, + "timezones": [ + "Asia/Kuala_Lumpur", + "Asia/Kuching" + ] + }, + { + "name": "Maldives", + "unicode": "U+1F1F2 U+1F1FB", + "emoji": "🇲🇻", + "alpha2": "MV", + "dialCode": "960", + "alpha3": "MDV", + "region": "Asia", + "capital": "Malé", + "geo": { + "lat": 3.25, + "long": 3.25 + }, + "timezones": [ + "Indian/Maldives" + ] + }, + { + "name": "Mali", + "unicode": "U+1F1F2 U+1F1F1", + "emoji": "🇲🇱", + "alpha2": "ML", + "dialCode": "223", + "alpha3": "MLI", + "region": "Africa", + "capital": "Bamako", + "geo": { + "lat": 17, + "long": 17 + }, + "timezones": [ + "Africa/Bamako" + ] + }, + { + "name": "Malta", + "unicode": "U+1F1F2 U+1F1F9", + "emoji": "🇲🇹", + "alpha2": "MT", + "dialCode": "356", + "alpha3": "MLT", + "region": "Europe", + "capital": "Valletta", + "geo": { + "lat": 35.83333333, + "long": 35.83333333 + }, + "timezones": [ + "Europe/Malta" + ] + }, + { + "name": "Marshall Islands", + "unicode": "U+1F1F2 U+1F1ED", + "emoji": "🇲🇭", + "alpha2": "MH", + "dialCode": "692", + "alpha3": "MHL", + "region": "Oceania", + "capital": "Majuro", + "geo": { + "lat": 9, + "long": 9 + }, + "timezones": [ + "Pacific/Majuro", + "Pacific/Kwajalein" + ] + }, + { + "name": "Martinique", + "unicode": "U+1F1F2 U+1F1F6", + "emoji": "🇲🇶", + "alpha2": "MQ", + "dialCode": "596", + "alpha3": "MTQ", + "region": "Americas", + "capital": "Fort-de-France", + "geo": { + "lat": 14.666667, + "long": 14.666667 + }, + "timezones": [ + "America/Martinique" + ] + }, + { + "name": "Mauritania", + "unicode": "U+1F1F2 U+1F1F7", + "emoji": "🇲🇷", + "alpha2": "MR", + "dialCode": "222", + "alpha3": "MRT", + "region": "Africa", + "capital": "Nouakchott", + "geo": { + "lat": 20, + "long": 20 + }, + "timezones": [ + "Africa/Nouakchott" + ] + }, + { + "name": "Mauritius", + "unicode": "U+1F1F2 U+1F1FA", + "emoji": "🇲🇺", + "alpha2": "MU", + "dialCode": "230", + "alpha3": "MUS", + "region": "Africa", + "capital": "Port Louis", + "geo": { + "lat": -20.28333333, + "long": -20.28333333 + }, + "timezones": [ + "Indian/Mauritius" + ] + }, + { + "name": "Mayotte", + "unicode": "U+1F1FE U+1F1F9", + "emoji": "🇾🇹", + "alpha2": "YT", + "dialCode": "262", + "alpha3": "MYT", + "region": "Africa", + "capital": "Mamoudzou", + "geo": { + "lat": -12.83333333, + "long": -12.83333333 + }, + "timezones": [ + "Indian/Mayotte" + ] + }, + { + "name": "Mexico", + "unicode": "U+1F1F2 U+1F1FD", + "emoji": "🇲🇽", + "alpha2": "MX", + "dialCode": "52", + "alpha3": "MEX", + "region": "Americas", + "capital": "Mexico City", + "geo": { + "lat": 23, + "long": 23 + }, + "timezones": [ + "America/Mexico_City", + "America/Cancun", + "America/Merida", + "America/Monterrey", + "America/Matamoros", + "America/Mazatlan", + "America/Chihuahua", + "America/Ojinaga", + "America/Hermosillo", + "America/Tijuana", + "America/Bahia_Banderas" + ] + }, + { + "name": "Micronesia", + "unicode": "U+1F1EB U+1F1F2", + "emoji": "🇫🇲", + "alpha2": "FM", + "dialCode": "691", + "alpha3": "FSM", + "region": "Oceania", + "capital": "Palikir", + "geo": { + "lat": 6.91666666, + "long": 6.91666666 + }, + "timezones": [ + "Pacific/Chuuk", + "Pacific/Pohnpei", + "Pacific/Kosrae" + ] + }, + { + "name": "Moldova", + "unicode": "U+1F1F2 U+1F1E9", + "emoji": "🇲🇩", + "alpha2": "MD", + "dialCode": "373", + "alpha3": "MDA", + "region": "Europe", + "capital": "Chișinău", + "geo": { + "lat": 47, + "long": 47 + }, + "timezones": [ + "Europe/Chisinau" + ] + }, + { + "name": "Monaco", + "unicode": "U+1F1F2 U+1F1E8", + "emoji": "🇲🇨", + "alpha2": "MC", + "dialCode": "377", + "alpha3": "MCO", + "region": "Europe", + "capital": "Monaco", + "geo": { + "lat": 43.73333333, + "long": 43.73333333 + }, + "timezones": [ + "Europe/Monaco" + ] + }, + { + "name": "Mongolia", + "unicode": "U+1F1F2 U+1F1F3", + "emoji": "🇲🇳", + "alpha2": "MN", + "dialCode": "976", + "alpha3": "MNG", + "region": "Asia", + "capital": "Ulan Bator", + "geo": { + "lat": 46, + "long": 46 + }, + "timezones": [ + "Asia/Ulaanbaatar", + "Asia/Hovd", + "Asia/Choibalsan" + ] + }, + { + "name": "Montenegro", + "unicode": "U+1F1F2 U+1F1EA", + "emoji": "🇲🇪", + "alpha2": "ME", + "dialCode": "382", + "alpha3": "MNE", + "region": "Europe", + "capital": "Podgorica", + "geo": { + "lat": 42.5, + "long": 42.5 + }, + "timezones": [ + "Europe/Podgorica" + ] + }, + { + "name": "Montserrat", + "unicode": "U+1F1F2 U+1F1F8", + "emoji": "🇲🇸", + "alpha2": "MS", + "dialCode": "1664", + "alpha3": "MSR", + "region": "Americas", + "capital": "Plymouth", + "geo": { + "lat": 16.75, + "long": 16.75 + }, + "timezones": [ + "America/Montserrat" + ] + }, + { + "name": "Morocco", + "unicode": "U+1F1F2 U+1F1E6", + "emoji": "🇲🇦", + "alpha2": "MA", + "dialCode": "212", + "alpha3": "MAR", + "region": "Africa", + "capital": "Rabat", + "geo": { + "lat": 32, + "long": 32 + }, + "timezones": [ + "Africa/Casablanca" + ] + }, + { + "name": "Mozambique", + "unicode": "U+1F1F2 U+1F1FF", + "emoji": "🇲🇿", + "alpha2": "MZ", + "dialCode": "258", + "alpha3": "MOZ", + "region": "Africa", + "capital": "Maputo", + "geo": { + "lat": -18.25, + "long": -18.25 + }, + "timezones": [ + "Africa/Maputo" + ] + }, + { + "name": "Myanmar", + "unicode": "U+1F1F2 U+1F1F2", + "emoji": "🇲🇲", + "alpha2": "MM", + "dialCode": "95", + "alpha3": "MMR", + "region": "Asia", + "capital": "Naypyidaw", + "geo": { + "lat": 22, + "long": 22 + }, + "timezones": [ + "Asia/Rangoon" + ] + }, + { + "name": "Namibia", + "unicode": "U+1F1F3 U+1F1E6", + "emoji": "🇳🇦", + "alpha2": "NA", + "dialCode": "264", + "alpha3": "NAM", + "region": "Africa", + "capital": "Windhoek", + "geo": { + "lat": -22, + "long": -22 + }, + "timezones": [ + "Africa/Windhoek" + ] + }, + { + "name": "Nauru", + "unicode": "U+1F1F3 U+1F1F7", + "emoji": "🇳🇷", + "alpha2": "NR", + "dialCode": "674", + "alpha3": "NRU", + "region": "Oceania", + "capital": "Yaren", + "geo": { + "lat": -0.53333333, + "long": -0.53333333 + }, + "timezones": [ + "Pacific/Nauru" + ] + }, + { + "name": "Nepal", + "unicode": "U+1F1F3 U+1F1F5", + "emoji": "🇳🇵", + "alpha2": "NP", + "dialCode": "977", + "alpha3": "NPL", + "region": "Asia", + "capital": "Kathmandu", + "geo": { + "lat": 28, + "long": 28 + }, + "timezones": [ + "Asia/Kathmandu" + ] + }, + { + "name": "Netherlands", + "unicode": "U+1F1F3 U+1F1F1", + "emoji": "🇳🇱", + "alpha2": "NL", + "dialCode": "31", + "alpha3": "NLD", + "region": "Europe", + "capital": "Amsterdam", + "geo": { + "lat": 52.5, + "long": 52.5 + }, + "timezones": [ + "Europe/Amsterdam" + ] + }, + { + "name": "New Caledonia", + "unicode": "U+1F1F3 U+1F1E8", + "emoji": "🇳🇨", + "alpha2": "NC", + "dialCode": "687", + "alpha3": "NCL", + "region": "Oceania", + "capital": "Nouméa", + "geo": { + "lat": -21.5, + "long": -21.5 + }, + "timezones": [ + "Pacific/Noumea" + ] + }, + { + "name": "New Zealand", + "unicode": "U+1F1F3 U+1F1FF", + "emoji": "🇳🇿", + "alpha2": "NZ", + "dialCode": "64", + "alpha3": "NZL", + "region": "Oceania", + "capital": "Wellington", + "geo": { + "lat": -41, + "long": -41 + }, + "timezones": [ + "Pacific/Auckland", + "Pacific/Chatham" + ] + }, + { + "name": "Nicaragua", + "unicode": "U+1F1F3 U+1F1EE", + "emoji": "🇳🇮", + "alpha2": "NI", + "dialCode": "505", + "alpha3": "NIC", + "region": "Americas", + "capital": "Managua", + "geo": { + "lat": 13, + "long": 13 + }, + "timezones": [ + "America/Managua" + ] + }, + { + "name": "Niger", + "unicode": "U+1F1F3 U+1F1EA", + "emoji": "🇳🇪", + "alpha2": "NE", + "dialCode": "227", + "alpha3": "NER", + "region": "Africa", + "capital": "Niamey", + "geo": { + "lat": 16, + "long": 16 + }, + "timezones": [ + "Africa/Niamey" + ] + }, + { + "name": "Nigeria", + "unicode": "U+1F1F3 U+1F1EC", + "emoji": "🇳🇬", + "alpha2": "NG", + "dialCode": "234", + "alpha3": "NGA", + "region": "Africa", + "capital": "Abuja", + "geo": { + "lat": 10, + "long": 10 + }, + "timezones": [ + "Africa/Lagos" + ] + }, + { + "name": "Niue", + "unicode": "U+1F1F3 U+1F1FA", + "emoji": "🇳🇺", + "alpha2": "NU", + "dialCode": "683", + "alpha3": "NIU", + "region": "Oceania", + "capital": "Alofi", + "geo": { + "lat": -19.03333333, + "long": -19.03333333 + }, + "timezones": [ + "Pacific/Niue" + ] + }, + { + "name": "Norfolk Island", + "unicode": "U+1F1F3 U+1F1EB", + "emoji": "🇳🇫", + "alpha2": "NF", + "dialCode": "672", + "alpha3": "NFK", + "region": "Oceania", + "capital": "Kingston", + "geo": { + "lat": -29.03333333, + "long": -29.03333333 + }, + "timezones": [ + "Pacific/Norfolk" + ] + }, + { + "name": "North Korea", + "unicode": "U+1F1F0 U+1F1F5", + "emoji": "🇰🇵", + "alpha2": "KP", + "dialCode": "850", + "alpha3": "PRK", + "region": "Asia", + "capital": "Pyongyang", + "geo": { + "lat": 40, + "long": 40 + }, + "timezones": [ + "Asia/Pyongyang" + ] + }, + { + "name": "Northern Mariana Islands", + "unicode": "U+1F1F2 U+1F1F5", + "emoji": "🇲🇵", + "alpha2": "MP", + "dialCode": "1 670", + "alpha3": "MNP", + "region": "Oceania", + "capital": "Saipan", + "geo": { + "lat": 15.2, + "long": 15.2 + }, + "timezones": [ + "Pacific/Saipan" + ] + }, + { + "name": "Norway", + "unicode": "U+1F1F3 U+1F1F4", + "emoji": "🇳🇴", + "alpha2": "NO", + "dialCode": "47", + "alpha3": "NOR", + "region": "Europe", + "capital": "Oslo", + "geo": { + "lat": 62, + "long": 62 + }, + "timezones": [ + "Europe/Oslo" + ] + }, + { + "name": "Oman", + "unicode": "U+1F1F4 U+1F1F2", + "emoji": "🇴🇲", + "alpha2": "OM", + "dialCode": "968", + "alpha3": "OMN", + "region": "Asia", + "capital": "Muscat", + "geo": { + "lat": 21, + "long": 21 + }, + "timezones": [ + "Asia/Muscat" + ] + }, + { + "name": "Pakistan", + "unicode": "U+1F1F5 U+1F1F0", + "emoji": "🇵🇰", + "alpha2": "PK", + "dialCode": "92", + "alpha3": "PAK", + "region": "Asia", + "capital": "Islamabad", + "geo": { + "lat": 30, + "long": 30 + }, + "timezones": [ + "Asia/Karachi" + ] + }, + { + "name": "Palau", + "unicode": "U+1F1F5 U+1F1FC", + "emoji": "🇵🇼", + "alpha2": "PW", + "dialCode": "680", + "alpha3": "PLW", + "region": "Oceania", + "capital": "Ngerulmud", + "geo": { + "lat": 7.5, + "long": 7.5 + }, + "timezones": [ + "Pacific/Palau" + ] + }, + { + "name": "Palestinian Territory", + "unicode": "U+1F1F5 U+1F1F8", + "emoji": "🇵🇸", + "alpha2": "PS", + "dialCode": "970", + "alpha3": "PSE", + "region": "Asia", + "capital": "Ramallah", + "geo": { + "lat": 31.9, + "long": 31.9 + }, + "timezones": [ + "Asia/Gaza", + "Asia/Hebron" + ] + }, + { + "name": "Panama", + "unicode": "U+1F1F5 U+1F1E6", + "emoji": "🇵🇦", + "alpha2": "PA", + "dialCode": "507", + "alpha3": "PAN", + "region": "Americas", + "capital": "Panama City", + "geo": { + "lat": 9, + "long": 9 + }, + "timezones": [ + "America/Panama" + ] + }, + { + "name": "Papua New Guinea", + "unicode": "U+1F1F5 U+1F1EC", + "emoji": "🇵🇬", + "alpha2": "PG", + "dialCode": "675", + "alpha3": "PNG", + "region": "Oceania", + "capital": "Port Moresby", + "geo": { + "lat": -6, + "long": -6 + }, + "timezones": [ + "Pacific/Port_Moresby", + "Pacific/Bougainville" + ] + }, + { + "name": "Paraguay", + "unicode": "U+1F1F5 U+1F1FE", + "emoji": "🇵🇾", + "alpha2": "PY", + "dialCode": "595", + "alpha3": "PRY", + "region": "Americas", + "capital": "Asunción", + "geo": { + "lat": -23, + "long": -23 + }, + "timezones": [ + "America/Asuncion" + ] + }, + { + "name": "Peru", + "unicode": "U+1F1F5 U+1F1EA", + "emoji": "🇵🇪", + "alpha2": "PE", + "dialCode": "51", + "alpha3": "PER", + "region": "Americas", + "capital": "Lima", + "geo": { + "lat": -10, + "long": -10 + }, + "timezones": [ + "America/Lima" + ] + }, + { + "name": "Philippines", + "unicode": "U+1F1F5 U+1F1ED", + "emoji": "🇵🇭", + "alpha2": "PH", + "dialCode": "63", + "alpha3": "PHL", + "region": "Asia", + "capital": "Manila", + "geo": { + "lat": 13, + "long": 13 + }, + "timezones": [ + "Asia/Manila" + ] + }, + { + "name": "Pitcairn", + "unicode": "U+1F1F5 U+1F1F3", + "emoji": "🇵🇳", + "alpha2": "PN", + "dialCode": "872", + "alpha3": "PCN", + "region": "Oceania", + "capital": "Adamstown", + "geo": { + "lat": -25.06666666, + "long": -25.06666666 + }, + "timezones": [ + "Pacific/Pitcairn" + ] + }, + { + "name": "Poland", + "unicode": "U+1F1F5 U+1F1F1", + "emoji": "🇵🇱", + "alpha2": "PL", + "dialCode": "48", + "alpha3": "POL", + "region": "Europe", + "capital": "Warsaw", + "geo": { + "lat": 52, + "long": 52 + }, + "timezones": [ + "Europe/Warsaw" + ] + }, + { + "name": "Portugal", + "unicode": "U+1F1F5 U+1F1F9", + "emoji": "🇵🇹", + "alpha2": "PT", + "dialCode": "351", + "alpha3": "PRT", + "region": "Europe", + "capital": "Lisbon", + "geo": { + "lat": 39.5, + "long": 39.5 + }, + "timezones": [ + "Europe/Lisbon", + "Atlantic/Madeira", + "Atlantic/Azores" + ] + }, + { + "name": "Puerto Rico", + "unicode": "U+1F1F5 U+1F1F7", + "emoji": "🇵🇷", + "alpha2": "PR", + "dialCode": "1 939", + "alpha3": "PRI", + "region": "Americas", + "capital": "San Juan", + "geo": { + "lat": 18.25, + "long": 18.25 + }, + "timezones": [ + "America/Puerto_Rico" + ] + }, + { + "name": "Qatar", + "unicode": "U+1F1F6 U+1F1E6", + "emoji": "🇶🇦", + "alpha2": "QA", + "dialCode": "974", + "alpha3": "QAT", + "region": "Asia", + "capital": "Doha", + "geo": { + "lat": 25.5, + "long": 25.5 + }, + "timezones": [ + "Asia/Qatar" + ] + }, + { + "name": "Romania", + "unicode": "U+1F1F7 U+1F1F4", + "emoji": "🇷🇴", + "alpha2": "RO", + "dialCode": "40", + "alpha3": "ROU", + "region": "Europe", + "capital": "Bucharest", + "geo": { + "lat": 46, + "long": 46 + }, + "timezones": [ + "Europe/Bucharest" + ] + }, + { + "name": "Russia", + "unicode": "U+1F1F7 U+1F1FA", + "emoji": "🇷🇺", + "alpha2": "RU", + "dialCode": "7", + "alpha3": "RUS", + "region": "Europe", + "capital": "Moscow", + "geo": { + "lat": 60, + "long": 60 + }, + "timezones": [ + "Europe/Kaliningrad", + "Europe/Moscow", + "Europe/Simferopol", + "Europe/Volgograd", + "Europe/Kirov", + "Europe/Astrakhan", + "Europe/Samara", + "Europe/Ulyanovsk", + "Asia/Yekaterinburg", + "Asia/Omsk", + "Asia/Novosibirsk", + "Asia/Barnaul", + "Asia/Tomsk", + "Asia/Novokuznetsk", + "Asia/Krasnoyarsk", + "Asia/Irkutsk", + "Asia/Chita", + "Asia/Yakutsk", + "Asia/Khandyga", + "Asia/Vladivostok", + "Asia/Ust-Nera", + "Asia/Magadan", + "Asia/Sakhalin", + "Asia/Srednekolymsk", + "Asia/Kamchatka", + "Asia/Anadyr" + ] + }, + { + "name": "Rwanda", + "unicode": "U+1F1F7 U+1F1FC", + "emoji": "🇷🇼", + "alpha2": "RW", + "dialCode": "250", + "alpha3": "RWA", + "region": "Africa", + "capital": "Kigali", + "geo": { + "lat": -2, + "long": -2 + }, + "timezones": [ + "Africa/Kigali" + ] + }, + { + "name": "Réunion", + "unicode": "U+1F1F7 U+1F1EA", + "emoji": "🇷🇪", + "alpha2": "RE", + "dialCode": "262", + "alpha3": "REU", + "region": "Africa", + "capital": "Saint-Denis", + "geo": { + "lat": -21.15, + "long": -21.15 + }, + "timezones": [ + "Indian/Reunion" + ] + }, + { + "name": "Saint Barthélemy", + "unicode": "U+1F1E7 U+1F1F1", + "emoji": "🇧🇱", + "alpha2": "BL", + "dialCode": "590", + "alpha3": "BLM", + "region": "Americas", + "capital": "Gustavia", + "geo": { + "lat": 18.5, + "long": 18.5 + }, + "timezones": [ + "America/St_Barthelemy" + ] + }, + { + "name": "Saint Helena, Ascension and Tristan Da Cunha", + "unicode": "U+1F1F8 U+1F1ED", + "emoji": "🇸🇭", + "alpha2": "SH", + "dialCode": "290", + "alpha3": "SHN", + "region": "Africa", + "geo": {}, + "capital": "", + "timezones": [] + }, + { + "name": "Saint Kitts and Nevis", + "unicode": "U+1F1F0 U+1F1F3", + "emoji": "🇰🇳", + "alpha2": "KN", + "dialCode": "1 869", + "alpha3": "KNA", + "region": "Americas", + "capital": "Basseterre", + "geo": { + "lat": 17.33333333, + "long": 17.33333333 + }, + "timezones": [ + "America/St_Kitts" + ] + }, + { + "name": "Saint Lucia", + "unicode": "U+1F1F1 U+1F1E8", + "emoji": "🇱🇨", + "alpha2": "LC", + "dialCode": "1 758", + "alpha3": "LCA", + "region": "Americas", + "capital": "Castries", + "geo": { + "lat": 13.88333333, + "long": 13.88333333 + }, + "timezones": [ + "America/St_Lucia" + ] + }, + { + "name": "Saint Martin (French Part)", + "unicode": "U+1F1F2 U+1F1EB", + "emoji": "🇲🇫", + "alpha2": "MF", + "dialCode": "590", + "alpha3": "MAF", + "region": "Americas", + "capital": "Marigot", + "geo": { + "lat": 18.08333333, + "long": 18.08333333 + }, + "timezones": [ + "America/Marigot" + ] + }, + { + "name": "Saint Pierre and Miquelon", + "unicode": "U+1F1F5 U+1F1F2", + "emoji": "🇵🇲", + "alpha2": "PM", + "dialCode": "508", + "alpha3": "SPM", + "region": "Americas", + "capital": "Saint-Pierre", + "geo": { + "lat": 46.83333333, + "long": 46.83333333 + }, + "timezones": [ + "America/Miquelon" + ] + }, + { + "name": "Saint Vincent and The Grenadines", + "unicode": "U+1F1FB U+1F1E8", + "emoji": "🇻🇨", + "alpha2": "VC", + "dialCode": "1 784", + "alpha3": "VCT", + "region": "Americas", + "capital": "Kingstown", + "geo": { + "lat": 13.25, + "long": 13.25 + }, + "timezones": [ + "America/St_Vincent" + ] + }, + { + "name": "Samoa", + "unicode": "U+1F1FC U+1F1F8", + "emoji": "🇼🇸", + "alpha2": "WS", + "dialCode": "685", + "alpha3": "WSM", + "region": "Oceania", + "capital": "Apia", + "geo": { + "lat": -13.58333333, + "long": -13.58333333 + }, + "timezones": [ + "Pacific/Apia" + ] + }, + { + "name": "San Marino", + "unicode": "U+1F1F8 U+1F1F2", + "emoji": "🇸🇲", + "alpha2": "SM", + "dialCode": "378", + "alpha3": "SMR", + "region": "Europe", + "capital": "City of San Marino", + "geo": { + "lat": 43.76666666, + "long": 43.76666666 + }, + "timezones": [ + "Europe/San_Marino" + ] + }, + { + "name": "Sao Tome and Principe", + "unicode": "U+1F1F8 U+1F1F9", + "emoji": "🇸🇹", + "alpha2": "ST", + "dialCode": "239", + "alpha3": "STP", + "region": "Africa", + "capital": "São Tomé", + "geo": { + "lat": 1, + "long": 1 + }, + "timezones": [ + "Africa/Sao_Tome" + ] + }, + { + "name": "Saudi Arabia", + "unicode": "U+1F1F8 U+1F1E6", + "emoji": "🇸🇦", + "alpha2": "SA", + "dialCode": "966", + "alpha3": "SAU", + "region": "Asia", + "capital": "Riyadh", + "geo": { + "lat": 25, + "long": 25 + }, + "timezones": [ + "Asia/Riyadh" + ] + }, + { + "name": "Senegal", + "unicode": "U+1F1F8 U+1F1F3", + "emoji": "🇸🇳", + "alpha2": "SN", + "dialCode": "221", + "alpha3": "SEN", + "region": "Africa", + "capital": "Dakar", + "geo": { + "lat": 14, + "long": 14 + }, + "timezones": [ + "Africa/Dakar" + ] + }, + { + "name": "Serbia", + "unicode": "U+1F1F7 U+1F1F8", + "emoji": "🇷🇸", + "alpha2": "RS", + "dialCode": "381", + "alpha3": "SRB", + "region": "Europe", + "capital": "Belgrade", + "geo": { + "lat": 44, + "long": 44 + }, + "timezones": [ + "Europe/Belgrade" + ] + }, + { + "name": "Seychelles", + "unicode": "U+1F1F8 U+1F1E8", + "emoji": "🇸🇨", + "alpha2": "SC", + "dialCode": "248", + "alpha3": "SYC", + "region": "Africa", + "capital": "Victoria", + "geo": { + "lat": -4.58333333, + "long": -4.58333333 + }, + "timezones": [ + "Indian/Mahe" + ] + }, + { + "name": "Sierra Leone", + "unicode": "U+1F1F8 U+1F1F1", + "emoji": "🇸🇱", + "alpha2": "SL", + "dialCode": "232", + "alpha3": "SLE", + "region": "Africa", + "capital": "Freetown", + "geo": { + "lat": 8.5, + "long": 8.5 + }, + "timezones": [ + "Africa/Freetown" + ] + }, + { + "name": "Singapore", + "unicode": "U+1F1F8 U+1F1EC", + "emoji": "🇸🇬", + "alpha2": "SG", + "dialCode": "65", + "alpha3": "SGP", + "region": "Asia", + "capital": "Singapore", + "geo": { + "lat": 1.36666666, + "long": 1.36666666 + }, + "timezones": [ + "Asia/Singapore" + ] + }, + { + "name": "Sint Maarten (Dutch Part)", + "unicode": "U+1F1F8 U+1F1FD", + "emoji": "🇸🇽", + "alpha2": "SX", + "dialCode": "", + "alpha3": "SXM", + "region": "Americas", + "capital": "Philipsburg", + "geo": { + "lat": 18.033333, + "long": 18.033333 + }, + "timezones": [ + "America/Lower_Princes" + ] + }, + { + "name": "Slovakia", + "unicode": "U+1F1F8 U+1F1F0", + "emoji": "🇸🇰", + "alpha2": "SK", + "dialCode": "421", + "alpha3": "SVK", + "region": "Europe", + "capital": "Bratislava", + "geo": { + "lat": 48.66666666, + "long": 48.66666666 + }, + "timezones": [ + "Europe/Bratislava" + ] + }, + { + "name": "Slovenia", + "unicode": "U+1F1F8 U+1F1EE", + "emoji": "🇸🇮", + "alpha2": "SI", + "dialCode": "386", + "alpha3": "SVN", + "region": "Europe", + "capital": "Ljubljana", + "geo": { + "lat": 46.11666666, + "long": 46.11666666 + }, + "timezones": [ + "Europe/Ljubljana" + ] + }, + { + "name": "Solomon Islands", + "unicode": "U+1F1F8 U+1F1E7", + "emoji": "🇸🇧", + "alpha2": "SB", + "dialCode": "677", + "alpha3": "SLB", + "region": "Oceania", + "capital": "Honiara", + "geo": { + "lat": -8, + "long": -8 + }, + "timezones": [ + "Pacific/Guadalcanal" + ] + }, + { + "name": "Somalia", + "unicode": "U+1F1F8 U+1F1F4", + "emoji": "🇸🇴", + "alpha2": "SO", + "dialCode": "252", + "alpha3": "SOM", + "region": "Africa", + "capital": "Mogadishu", + "geo": { + "lat": 10, + "long": 10 + }, + "timezones": [ + "Africa/Mogadishu" + ] + }, + { + "name": "South Africa", + "unicode": "U+1F1FF U+1F1E6", + "emoji": "🇿🇦", + "alpha2": "ZA", + "dialCode": "27", + "alpha3": "ZAF", + "region": "Africa", + "capital": "Pretoria", + "geo": { + "lat": -29, + "long": -29 + }, + "timezones": [ + "Africa/Johannesburg" + ] + }, + { + "name": "South Georgia", + "unicode": "U+1F1EC U+1F1F8", + "emoji": "🇬🇸", + "alpha2": "GS", + "dialCode": "500", + "alpha3": "SGS", + "region": "Americas", + "capital": "King Edward Point", + "geo": { + "lat": -54.5, + "long": -54.5 + }, + "timezones": [ + "Atlantic/South_Georgia" + ] + }, + { + "name": "South Korea", + "unicode": "U+1F1F0 U+1F1F7", + "emoji": "🇰🇷", + "alpha2": "KR", + "dialCode": "82", + "alpha3": "KOR", + "region": "Asia", + "capital": "Seoul", + "geo": { + "lat": 37, + "long": 37 + }, + "timezones": [ + "Asia/Seoul" + ] + }, + { + "name": "South Sudan", + "unicode": "U+1F1F8 U+1F1F8", + "emoji": "🇸🇸", + "alpha2": "SS", + "dialCode": "", + "alpha3": "SSD", + "region": "Africa", + "capital": "Juba", + "geo": { + "lat": 7, + "long": 7 + }, + "timezones": [ + "Africa/Juba" + ] + }, + { + "name": "Spain", + "unicode": "U+1F1EA U+1F1F8", + "emoji": "🇪🇸", + "alpha2": "ES", + "dialCode": "34", + "alpha3": "ESP", + "region": "Europe", + "capital": "Madrid", + "geo": { + "lat": 40, + "long": 40 + }, + "timezones": [ + "Europe/Madrid", + "Africa/Ceuta", + "Atlantic/Canary" + ] + }, + { + "name": "Sri Lanka", + "unicode": "U+1F1F1 U+1F1F0", + "emoji": "🇱🇰", + "alpha2": "LK", + "dialCode": "94", + "alpha3": "LKA", + "region": "Asia", + "capital": "Colombo", + "geo": { + "lat": 7, + "long": 7 + }, + "timezones": [ + "Asia/Colombo" + ] + }, + { + "name": "Sudan", + "unicode": "U+1F1F8 U+1F1E9", + "emoji": "🇸🇩", + "alpha2": "SD", + "dialCode": "249", + "alpha3": "SDN", + "region": "Africa", + "capital": "Khartoum", + "geo": { + "lat": 15, + "long": 15 + }, + "timezones": [ + "Africa/Khartoum" + ] + }, + { + "name": "Suriname", + "unicode": "U+1F1F8 U+1F1F7", + "emoji": "🇸🇷", + "alpha2": "SR", + "dialCode": "597", + "alpha3": "SUR", + "region": "Americas", + "capital": "Paramaribo", + "geo": { + "lat": 4, + "long": 4 + }, + "timezones": [ + "America/Paramaribo" + ] + }, + { + "name": "Svalbard and Jan Mayen", + "unicode": "U+1F1F8 U+1F1EF", + "emoji": "🇸🇯", + "alpha2": "SJ", + "dialCode": "47", + "alpha3": "SJM", + "region": "Europe", + "capital": "Longyearbyen", + "geo": { + "lat": 78, + "long": 78 + }, + "timezones": [ + "Arctic/Longyearbyen" + ] + }, + { + "name": "Swaziland", + "unicode": "U+1F1F8 U+1F1FF", + "emoji": "🇸🇿", + "alpha2": "SZ", + "dialCode": "268", + "alpha3": "SWZ", + "region": "Africa", + "capital": "Lobamba", + "geo": { + "lat": -26.5, + "long": -26.5 + }, + "timezones": [ + "Africa/Mbabane" + ] + }, + { + "name": "Sweden", + "unicode": "U+1F1F8 U+1F1EA", + "emoji": "🇸🇪", + "alpha2": "SE", + "dialCode": "46", + "alpha3": "SWE", + "region": "Europe", + "capital": "Stockholm", + "geo": { + "lat": 62, + "long": 62 + }, + "timezones": [ + "Europe/Stockholm" + ] + }, + { + "name": "Switzerland", + "unicode": "U+1F1E8 U+1F1ED", + "emoji": "🇨🇭", + "alpha2": "CH", + "dialCode": "41", + "alpha3": "CHE", + "region": "Europe", + "capital": "Bern", + "geo": { + "lat": 47, + "long": 47 + }, + "timezones": [ + "Europe/Zurich" + ] + }, + { + "name": "Syrian Arab Republic", + "unicode": "U+1F1F8 U+1F1FE", + "emoji": "🇸🇾", + "alpha2": "SY", + "dialCode": "963", + "alpha3": "SYR", + "region": "Asia", + "capital": "Damascus", + "geo": { + "lat": 35, + "long": 35 + }, + "timezones": [ + "Asia/Damascus" + ] + }, + { + "name": "Taiwan", + "unicode": "U+1F1F9 U+1F1FC", + "emoji": "🇹🇼", + "alpha2": "TW", + "dialCode": "886", + "alpha3": "TWN", + "region": "Asia", + "capital": "Taipei", + "geo": { + "lat": 23.5, + "long": 23.5 + }, + "timezones": [ + "Asia/Taipei" + ] + }, + { + "name": "Tajikistan", + "unicode": "U+1F1F9 U+1F1EF", + "emoji": "🇹🇯", + "alpha2": "TJ", + "dialCode": "992", + "alpha3": "TJK", + "region": "Asia", + "capital": "Dushanbe", + "geo": { + "lat": 39, + "long": 39 + }, + "timezones": [ + "Asia/Dushanbe" + ] + }, + { + "name": "Tanzania", + "unicode": "U+1F1F9 U+1F1FF", + "emoji": "🇹🇿", + "alpha2": "TZ", + "dialCode": "255", + "alpha3": "TZA", + "region": "Africa", + "capital": "Dodoma", + "geo": { + "lat": -6, + "long": -6 + }, + "timezones": [ + "Africa/Dar_es_Salaam" + ] + }, + { + "name": "Thailand", + "unicode": "U+1F1F9 U+1F1ED", + "emoji": "🇹🇭", + "alpha2": "TH", + "dialCode": "66", + "alpha3": "THA", + "region": "Asia", + "capital": "Bangkok", + "geo": { + "lat": 15, + "long": 15 + }, + "timezones": [ + "Asia/Bangkok" + ] + }, + { + "name": "Timor-Leste", + "unicode": "U+1F1F9 U+1F1F1", + "emoji": "🇹🇱", + "alpha2": "TL", + "dialCode": "670", + "alpha3": "TLS", + "region": "Asia", + "capital": "Dili", + "geo": { + "lat": -8.83333333, + "long": -8.83333333 + }, + "timezones": [ + "Asia/Dili" + ] + }, + { + "name": "Togo", + "unicode": "U+1F1F9 U+1F1EC", + "emoji": "🇹🇬", + "alpha2": "TG", + "dialCode": "228", + "alpha3": "TGO", + "region": "Africa", + "capital": "Lomé", + "geo": { + "lat": 8, + "long": 8 + }, + "timezones": [ + "Africa/Lome" + ] + }, + { + "name": "Tokelau", + "unicode": "U+1F1F9 U+1F1F0", + "emoji": "🇹🇰", + "alpha2": "TK", + "dialCode": "690", + "alpha3": "TKL", + "region": "Oceania", + "capital": "Fakaofo", + "geo": { + "lat": -9, + "long": -9 + }, + "timezones": [ + "Pacific/Fakaofo" + ] + }, + { + "name": "Tonga", + "unicode": "U+1F1F9 U+1F1F4", + "emoji": "🇹🇴", + "alpha2": "TO", + "dialCode": "676", + "alpha3": "TON", + "region": "Oceania", + "capital": "Nuku'alofa", + "geo": { + "lat": -20, + "long": -20 + }, + "timezones": [ + "Pacific/Tongatapu" + ] + }, + { + "name": "Trinidad and Tobago", + "unicode": "U+1F1F9 U+1F1F9", + "emoji": "🇹🇹", + "alpha2": "TT", + "dialCode": "1 868", + "alpha3": "TTO", + "region": "Americas", + "capital": "Port of Spain", + "geo": { + "lat": 11, + "long": 11 + }, + "timezones": [ + "America/Port_of_Spain" + ] + }, + { + "name": "Tunisia", + "unicode": "U+1F1F9 U+1F1F3", + "emoji": "🇹🇳", + "alpha2": "TN", + "dialCode": "216", + "alpha3": "TUN", + "region": "Africa", + "capital": "Tunis", + "geo": { + "lat": 34, + "long": 34 + }, + "timezones": [ + "Africa/Tunis" + ] + }, + { + "name": "Turkey", + "unicode": "U+1F1F9 U+1F1F7", + "emoji": "🇹🇷", + "alpha2": "TR", + "dialCode": "90", + "alpha3": "TUR", + "region": "Asia", + "capital": "Ankara", + "geo": { + "lat": 39, + "long": 39 + }, + "timezones": [ + "Europe/Istanbul" + ] + }, + { + "name": "Turkmenistan", + "unicode": "U+1F1F9 U+1F1F2", + "emoji": "🇹🇲", + "alpha2": "TM", + "dialCode": "993", + "alpha3": "TKM", + "region": "Asia", + "capital": "Ashgabat", + "geo": { + "lat": 40, + "long": 40 + }, + "timezones": [ + "Asia/Ashgabat" + ] + }, + { + "name": "Turks and Caicos Islands", + "unicode": "U+1F1F9 U+1F1E8", + "emoji": "🇹🇨", + "alpha2": "TC", + "dialCode": "1 649", + "alpha3": "TCA", + "region": "Americas", + "capital": "Cockburn Town", + "geo": { + "lat": 21.75, + "long": 21.75 + }, + "timezones": [ + "America/Grand_Turk" + ] + }, + { + "name": "Tuvalu", + "unicode": "U+1F1F9 U+1F1FB", + "emoji": "🇹🇻", + "alpha2": "TV", + "dialCode": "688", + "alpha3": "TUV", + "region": "Oceania", + "capital": "Funafuti", + "geo": { + "lat": -8, + "long": -8 + }, + "timezones": [ + "Pacific/Funafuti" + ] + }, + { + "name": "Uganda", + "unicode": "U+1F1FA U+1F1EC", + "emoji": "🇺🇬", + "alpha2": "UG", + "dialCode": "256", + "alpha3": "UGA", + "region": "Africa", + "capital": "Kampala", + "geo": { + "lat": 1, + "long": 1 + }, + "timezones": [ + "Africa/Kampala" + ] + }, + { + "name": "Ukraine", + "unicode": "U+1F1FA U+1F1E6", + "emoji": "🇺🇦", + "alpha2": "UA", + "dialCode": "380", + "alpha3": "UKR", + "region": "Europe", + "capital": "Kiev", + "geo": { + "lat": 49, + "long": 49 + }, + "timezones": [ + "Europe/Kiev", + "Europe/Uzhgorod", + "Europe/Zaporozhye" + ] + }, + { + "name": "United Arab Emirates", + "unicode": "U+1F1E6 U+1F1EA", + "emoji": "🇦🇪", + "alpha2": "AE", + "dialCode": "971", + "alpha3": "ARE", + "region": "Asia", + "capital": "Abu Dhabi", + "geo": { + "lat": 24, + "long": 24 + }, + "timezones": [ + "Asia/Dubai" + ] + }, + { + "name": "United Kingdom", + "unicode": "U+1F1EC U+1F1E7", + "emoji": "🇬🇧", + "alpha2": "GB", + "dialCode": "44", + "alpha3": "GBR", + "region": "Europe", + "capital": "London", + "geo": { + "lat": 54, + "long": 54 + }, + "timezones": [ + "Europe/London" + ] + }, + { + "name": "United States", + "unicode": "U+1F1FA U+1F1F8", + "emoji": "🇺🇸", + "alpha2": "US", + "dialCode": "1", + "alpha3": "USA", + "region": "Americas", + "capital": "Washington D.C.", + "geo": { + "lat": 38, + "long": 38 + }, + "timezones": [ + "America/New_York", + "America/Detroit", + "America/Kentucky/Louisville", + "America/Kentucky/Monticello", + "America/Indiana/Indianapolis", + "America/Indiana/Vincennes", + "America/Indiana/Winamac", + "America/Indiana/Marengo", + "America/Indiana/Petersburg", + "America/Indiana/Vevay", + "America/Chicago", + "America/Indiana/Tell_City", + "America/Indiana/Knox", + "America/Menominee", + "America/North_Dakota/Center", + "America/North_Dakota/New_Salem", + "America/North_Dakota/Beulah", + "America/Denver", + "America/Boise", + "America/Phoenix", + "America/Los_Angeles", + "America/Anchorage", + "America/Juneau", + "America/Sitka", + "America/Metlakatla", + "America/Yakutat", + "America/Nome", + "America/Adak", + "Pacific/Honolulu" + ] + }, + { + "name": "United States Minor Outlying Islands", + "unicode": "U+1F1FA U+1F1F2", + "emoji": "🇺🇲", + "alpha2": "UM", + "dialCode": "", + "alpha3": "UMI", + "region": "Oceania", + "capital": null, + "geo": { + "lat": 19.2911437, + "long": 19.2911437 + }, + "timezones": [ + "Pacific/Johnston", + "Pacific/Midway", + "Pacific/Wake" + ] + }, + { + "name": "Uruguay", + "unicode": "U+1F1FA U+1F1FE", + "emoji": "🇺🇾", + "alpha2": "UY", + "dialCode": "598", + "alpha3": "URY", + "region": "Americas", + "capital": "Montevideo", + "geo": { + "lat": -33, + "long": -33 + }, + "timezones": [ + "America/Montevideo" + ] + }, + { + "name": "Uzbekistan", + "unicode": "U+1F1FA U+1F1FF", + "emoji": "🇺🇿", + "alpha2": "UZ", + "dialCode": "998", + "alpha3": "UZB", + "region": "Asia", + "capital": "Tashkent", + "geo": { + "lat": 41, + "long": 41 + }, + "timezones": [ + "Asia/Samarkand", + "Asia/Tashkent" + ] + }, + { + "name": "Vanuatu", + "unicode": "U+1F1FB U+1F1FA", + "emoji": "🇻🇺", + "alpha2": "VU", + "dialCode": "678", + "alpha3": "VUT", + "region": "Oceania", + "capital": "Port Vila", + "geo": { + "lat": -16, + "long": -16 + }, + "timezones": [ + "Pacific/Efate" + ] + }, + { + "name": "Vatican City", + "unicode": "U+1F1FB U+1F1E6", + "emoji": "🇻🇦", + "alpha2": "VA", + "dialCode": "379", + "alpha3": "VAT", + "region": "Europe", + "capital": "Vatican City", + "geo": { + "lat": 41.9, + "long": 41.9 + }, + "timezones": [ + "Europe/Vatican" + ] + }, + { + "name": "Venezuela", + "unicode": "U+1F1FB U+1F1EA", + "emoji": "🇻🇪", + "alpha2": "VE", + "dialCode": "58", + "alpha3": "VEN", + "region": "Americas", + "capital": "Caracas", + "geo": { + "lat": 8, + "long": 8 + }, + "timezones": [ + "America/Caracas" + ] + }, + { + "name": "Viet Nam", + "unicode": "U+1F1FB U+1F1F3", + "emoji": "🇻🇳", + "alpha2": "VN", + "dialCode": "84", + "alpha3": "VNM", + "region": "Asia", + "capital": "Hanoi", + "geo": { + "lat": 16.16666666, + "long": 16.16666666 + }, + "timezones": [ + "Asia/Ho_Chi_Minh" + ] + }, + { + "name": "Virgin Islands, British", + "unicode": "U+1F1FB U+1F1EC", + "emoji": "🇻🇬", + "alpha2": "VG", + "dialCode": "1 284", + "alpha3": "VGB", + "region": "Americas", + "capital": "Road Town", + "geo": { + "lat": 18.431383, + "long": 18.431383 + }, + "timezones": [ + "America/Tortola" + ] + }, + { + "name": "Virgin Islands, U.S.", + "unicode": "U+1F1FB U+1F1EE", + "emoji": "🇻🇮", + "alpha2": "VI", + "dialCode": "1 340", + "alpha3": "VIR", + "region": "Americas", + "capital": "Charlotte Amalie", + "geo": { + "lat": 18.35, + "long": 18.35 + }, + "timezones": [ + "America/St_Thomas" + ] + }, + { + "name": "Wallis and Futuna", + "unicode": "U+1F1FC U+1F1EB", + "emoji": "🇼🇫", + "alpha2": "WF", + "dialCode": "681", + "alpha3": "WLF", + "region": "Oceania", + "capital": "Mata-Utu", + "geo": { + "lat": -13.3, + "long": -13.3 + }, + "timezones": [ + "Pacific/Wallis" + ] + }, + { + "name": "Western Sahara", + "unicode": "U+1F1EA U+1F1ED", + "emoji": "🇪🇭", + "alpha2": "EH", + "dialCode": "", + "alpha3": "ESH", + "region": "Africa", + "capital": "El Aaiún", + "geo": { + "lat": 24.5, + "long": 24.5 + }, + "timezones": [ + "Africa/El_Aaiun" + ] + }, + { + "name": "Yemen", + "unicode": "U+1F1FE U+1F1EA", + "emoji": "🇾🇪", + "alpha2": "YE", + "dialCode": "967", + "alpha3": "YEM", + "region": "Asia", + "capital": "Sana'a", + "geo": { + "lat": 15, + "long": 15 + }, + "timezones": [ + "Asia/Aden" + ] + }, + { + "name": "Zambia", + "unicode": "U+1F1FF U+1F1F2", + "emoji": "🇿🇲", + "alpha2": "ZM", + "dialCode": "260", + "alpha3": "ZMB", + "region": "Africa", + "capital": "Lusaka", + "geo": { + "lat": -15, + "long": -15 + }, + "timezones": [ + "Africa/Lusaka" + ] + }, + { + "name": "Zimbabwe", + "unicode": "U+1F1FF U+1F1FC", + "emoji": "🇿🇼", + "alpha2": "ZW", + "dialCode": "263", + "alpha3": "ZWE", + "region": "Africa", + "capital": "Harare", + "geo": { + "lat": -20, + "long": -20 + }, + "timezones": [ + "Africa/Harare" + ] + }, + { + "name": "Åland Islands", + "unicode": "U+1F1E6 U+1F1FD", + "emoji": "🇦🇽", + "alpha2": "AX", + "dialCode": "", + "alpha3": "ALA", + "region": "Europe", + "capital": "Mariehamn", + "geo": { + "lat": 60.116667, + "long": 60.116667 + }, + "timezones": [ + "Europe/Mariehamn" + ] + } +] \ No newline at end of file diff --git a/tux/utils/data/timezones.json b/tux/utils/data/timezones.json new file mode 100644 index 0000000..6f5ab73 --- /dev/null +++ b/tux/utils/data/timezones.json @@ -0,0 +1,122 @@ +[ + { + "offset": "-10:00", + "timezone": "HST", + "full_timezone": "Pacific/Honolulu", + "discord_emoji": ":flag_hn:" + }, + { + "offset": "-09:00", + "timezone": "AKST", + "full_timezone": "America/Anchorage", + "discord_emoji": ":flag_us:" + }, + { + "offset": "-08:00", + "timezone": "PST", + "full_timezone": "America/Los_Angeles", + "discord_emoji": ":flag_us:" + }, + { + "offset": "-07:00", + "timezone": "MST", + "full_timezone": "America/Denver", + "discord_emoji": ":flag_us:" + }, + { + "offset": "-06:00", + "timezone": "CST", + "full_timezone": "America/Chicago", + "discord_emoji": ":flag_us:" + }, + { + "offset": "-05:00", + "timezone": "EST", + "full_timezone": "America/New_York", + "discord_emoji": ":flag_us:" + }, + { + "offset": "-04:00", + "timezone": "AST", + "full_timezone": "America/Puerto_Rico", + "discord_emoji": ":flag_pr:" + }, + { + "offset": "-03:00", + "timezone": "ART", + "full_timezone": "America/Argentina/Buenos_Aires", + "discord_emoji": ":flag_ar:" + }, + { + "offset": "-02:00", + "timezone": "GST", + "full_timezone": "Atlantic/South_Georgia", + "discord_emoji": ":flag_gs:" + }, + { + "offset": "-01:00", + "timezone": "CVT", + "full_timezone": "Atlantic/Cape_Verde", + "discord_emoji": ":flag_cv:" + }, + { + "offset": "+00:00", + "timezone": "GMT", + "full_timezone": "Europe/London", + "discord_emoji": ":flag_gb:" + }, + { + "offset": "+01:00", + "timezone": "CET", + "full_timezone": "Europe/Berlin", + "discord_emoji": ":flag_de:" + }, + { + "offset": "+02:00", + "timezone": "EET", + "full_timezone": "Europe/Athens", + "discord_emoji": ":flag_gr:" + }, + { + "offset": "+03:00", + "timezone": "MSK", + "full_timezone": "Europe/Moscow", + "discord_emoji": ":flag_ru:" + }, + { + "offset": "+04:00", + "timezone": "GST", + "full_timezone": "Asia/Dubai", + "discord_emoji": ":flag_ae:" + }, + { + "offset": "+05:00", + "timezone": "PKT", + "full_timezone": "Asia/Karachi", + "discord_emoji": ":flag_pk:" + }, + { + "offset": "+06:00", + "timezone": "BST", + "full_timezone": "Asia/Dhaka", + "discord_emoji": ":flag_bd:" + }, + { + "offset": "+07:00", + "timezone": "THA", + "full_timezone": "Asia/Bangkok", + "discord_emoji": ":flag_th:" + }, + { + "offset": "+08:00", + "timezone": "CST", + "full_timezone": "Asia/Singapore", + "discord_emoji": ":flag_sg:" + }, + { + "offset": "+09:00", + "timezone": "JST", + "full_timezone": "Asia/Tokyo", + "discord_emoji": ":flag_jp:" + } +] From 829762d8d180cdd19a7a975b6fff0ebbc0152dbc Mon Sep 17 00:00:00 2001 From: kzndotsh Date: Thu, 22 Aug 2024 01:44:00 +0000 Subject: [PATCH 37/84] chore: switch from JSON to YAML for configuration file feat: add pyyaml to dependencies to parse YAML files fix: remove unused LOG_CHANNELS constant from constants.py refactor: add default values for environment variables in constants.py feat: ignore YAML configuration file in .gitignore refactor: delete unused JSON example configuration file --- .gitignore | 3 +- config/settings.json.example | 58 ------------------------------------ poetry.lock | 2 +- pyproject.toml | 1 + tux/utils/constants.py | 27 +++++++---------- 5 files changed, 15 insertions(+), 76 deletions(-) delete mode 100644 config/settings.json.example diff --git a/.gitignore b/.gitignore index 82c37a0..efe1365 100644 --- a/.gitignore +++ b/.gitignore @@ -164,6 +164,7 @@ github-private-key.pem # Miscellaneous /debug.csv config/settings.json +config/settings.yml # MacOS -.DS_Store \ No newline at end of file +.DS_Store diff --git a/config/settings.json.example b/config/settings.json.example deleted file mode 100644 index 7dfae7b..0000000 --- a/config/settings.json.example +++ /dev/null @@ -1,58 +0,0 @@ -{ - "PREFIX": { - "PROD": "$", - "DEV": "$" - }, - "ROLES": { - "ADMIN": 123456789012345679, - "MOD": 123456789012345679, - "JR_MOD": 123456789012345679, - "OWNER": 123456789012345679, - "TESTING": 123456789012345679 - }, - "USER_IDS": { - "SYSADMINS": [ - 123456789012345679, - 123456789012345679 - ], - "BOT_OWNER": 123456789012345679 - }, - "TEMPVC_CATEGORY_ID": 1235096247442870292, - "TEMPVC_CHANNEL_ID": 1235096247442870292, - "LOG_CHANNELS": { - "AUDIT": 1235096271350399076, - "MOD": 1235096291672068106, - "REPORTS": 1235096305160814652, - "GATE": 1235096247442870292, - "DEV": 1235095919788167269, - "PRIVATE": 1235108340791513129 - }, - "EMBED_COLORS": { - "DEFAULT": 16044058, - "INFO": 12634869, - "WARNING": 16634507, - "ERROR": 16067173, - "SUCCESS": 10407530, - "POLL": 14724968, - "CASE": 16217742, - "NOTE": 16752228 - }, - "EMBED_ICONS": { - "DEFAULT": "https://i.imgur.com/owW4EZk.png", - "INFO": "https://i.imgur.com/8GRtR2G.png", - "SUCCESS": "https://i.imgur.com/JsNbN7D.png", - "ERROR": "https://i.imgur.com/zZjuWaU.png", - "CASE": "https://i.imgur.com/c43cwnV.png", - "NOTE": "https://i.imgur.com/VqPFbil.png", - "POLL": "https://i.imgur.com/pkPeG5q.png", - "ACTIVE_CASE": "https://github.com/allthingslinux/tux/blob/main/assets/embeds/active_case.png?raw=true", - "INACTIVE_CASE": "https://github.com/allthingslinux/tux/blob/main/assets/embeds/inactive_case.png?raw=true", - "ADD": "https://github.com/allthingslinux/tux/blob/main/assets/emojis/added.png?raw=true", - "REMOVE": "https://github.com/allthingslinux/tux/blob/main/assets/emojis/removed.png?raw=true", - "BAN": "https://github.com/allthingslinux/tux/blob/main/assets/emojis/ban.png?raw=true", - "JAIL": "https://github.com/allthingslinux/tux/blob/main/assets/emojis/jail.png?raw=true", - "KICK": "https://github.com/allthingslinux/tux/blob/main/assets/emojis/kick.png?raw=true", - "TIMEOUT": "https://github.com/allthingslinux/tux/blob/main/assets/emojis/timeout.png?raw=true", - "WARN": "https://github.com/allthingslinux/tux/blob/main/assets/emojis/warn.png?raw=true" - } -} \ No newline at end of file diff --git a/poetry.lock b/poetry.lock index efca975..540ece8 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2324,4 +2324,4 @@ multidict = ">=4.0" [metadata] lock-version = "2.0" python-versions = ">=3.12,<4" -content-hash = "30272c710183da26e169ea88fd077632a69fac2a891ba3f9a862bc815f368141" +content-hash = "b7e79ee5391b21488b2a71b8a447ffd2cf76aa9ced53f6ed96da383bf77c1aa7" diff --git a/pyproject.toml b/pyproject.toml index 8986cd4..9db141a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,6 +34,7 @@ sentry-sdk = {extras = ["httpx", "loguru"], version = "^2.7.0"} types-aiofiles = "^24.1.0.20240626" types-psutil = "^6.0.0.20240621" typing-extensions = "^4.12.2" +pyyaml = "^6.0.2" [tool.poetry.group.docs.dependencies] mkdocs-material = "^9.5.30" diff --git a/tux/utils/constants.py b/tux/utils/constants.py index 69e0dd5..da2d732 100644 --- a/tux/utils/constants.py +++ b/tux/utils/constants.py @@ -1,15 +1,15 @@ import base64 -import json import os from pathlib import Path from typing import Final +import yaml from dotenv import load_dotenv, set_key load_dotenv(verbose=True) -config_file = Path("config/settings.json") -config = json.loads(config_file.read_text()) +config_file = Path("config/settings.yml") +config = yaml.safe_load(config_file.read_text()) class Constants: @@ -37,7 +37,7 @@ class Constants: COG_IGNORE_LIST: Final[set[str]] = DEV_COG_IGNORE_LIST if DEV and DEV.lower() == "true" else PROD_COG_IGNORE_LIST # Sentry-related constants - SENTRY_URL: Final[str | None] = os.getenv("SENTRY_URL") + SENTRY_URL: Final[str | None] = os.getenv("SENTRY_URL", "") # Database constants PROD_DATABASE_URL: Final[str] = os.getenv("PROD_DATABASE_URL", "") @@ -53,25 +53,20 @@ class Constants: GITHUB_REPO: Final[str] = os.getenv("GITHUB_REPO", "") GITHUB_TOKEN: Final[str] = os.getenv("GITHUB_TOKEN", "") GITHUB_APP_ID: Final[int] = int(os.getenv("GITHUB_APP_ID", 0)) - GITHUB_CLIENT_ID = os.getenv("GITHUB_CLIENT_ID") - GITHUB_CLIENT_SECRET = os.getenv("GITHUB_CLIENT_SECRET") - GITHUB_PUBLIC_KEY = os.getenv("GITHUB_PUBLIC_KEY") + GITHUB_CLIENT_ID = os.getenv("GITHUB_CLIENT_ID", "") + GITHUB_CLIENT_SECRET = os.getenv("GITHUB_CLIENT_SECRET", "") + GITHUB_PUBLIC_KEY = os.getenv("GITHUB_PUBLIC_KEY", "") GITHUB_INSTALLATION_ID: Final[int] = int(os.getenv("GITHUB_INSTALLATION_ID", 0)) - GITHUB_PRIVATE_KEY: str = base64.b64decode(os.getenv("GITHUB_PRIVATE_KEY_BASE64", "")).decode( - "utf-8", + GITHUB_PRIVATE_KEY: str = ( + base64.b64decode(os.getenv("GITHUB_PRIVATE_KEY_BASE64", "")).decode("utf-8") + if os.getenv("GITHUB_PRIVATE_KEY_BASE64") + else "" ) # Mailcow constants MAILCOW_API_KEY: Final[str] = os.getenv("MAILCOW_API_KEY", "") MAILCOW_API_URL: Final[str] = os.getenv("MAILCOW_API_URL", "") - # Channel constants - LOG_CHANNELS: Final[dict[str, int]] = config["LOG_CHANNELS"].copy() - - if DEV and DEV.lower() == "true": - for key in LOG_CHANNELS: - LOG_CHANNELS[key] = LOG_CHANNELS["DEV"] - # Temp VC constants TEMPVC_CATEGORY_ID: Final[str | None] = config["TEMPVC_CATEGORY_ID"] TEMPVC_CHANNEL_ID: Final[str | None] = config["TEMPVC_CHANNEL_ID"] From c3bb4443df54a0c7af97f941da1c36bf8ed9e2b7 Mon Sep 17 00:00:00 2001 From: kzndotsh Date: Thu, 22 Aug 2024 01:44:37 +0000 Subject: [PATCH 38/84] feat: add example settings.yml file to provide a template for configuration settings --- config/settings.yml.example | 42 +++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 config/settings.yml.example diff --git a/config/settings.yml.example b/config/settings.yml.example new file mode 100644 index 0000000..f947ebc --- /dev/null +++ b/config/settings.yml.example @@ -0,0 +1,42 @@ +# config/settings.yml + +PREFIX: + PROD: "$" + DEV: "~" + +USER_IDS: + SYSADMINS: + - 123456789012345679 + - 123456789012345679 + BOT_OWNER: 123456789012345679 + +TEMPVC_CATEGORY_ID: 123456789012345679 +TEMPVC_CHANNEL_ID: 123456789012345679 + +EMBED_COLORS: + DEFAULT: 16044058 + INFO: 12634869 + WARNING: 16634507 + ERROR: 16067173 + SUCCESS: 10407530 + POLL: 14724968 + CASE: 16217742 + NOTE: 16752228 + +EMBED_ICONS: + DEFAULT: "https://i.imgur.com/owW4EZk.png" + INFO: "https://i.imgur.com/8GRtR2G.png" + SUCCESS: "https://i.imgur.com/JsNbN7D.png" + ERROR: "https://i.imgur.com/zZjuWaU.png" + CASE: "https://i.imgur.com/c43cwnV.png" + NOTE: "https://i.imgur.com/VqPFbil.png" + POLL: "https://i.imgur.com/pkPeG5q.png" + ACTIVE_CASE: "https://github.com/allthingslinux/tux/blob/main/assets/embeds/active_case.png?raw=true" + INACTIVE_CASE: "https://github.com/allthingslinux/tux/blob/main/assets/embeds/inactive_case.png?raw=true" + ADD: "https://github.com/allthingslinux/tux/blob/main/assets/emojis/added.png?raw=true" + REMOVE: "https://github.com/allthingslinux/tux/blob/main/assets/emojis/removed.png?raw=true" + BAN: "https://github.com/allthingslinux/tux/blob/main/assets/emojis/ban.png?raw=true" + JAIL: "https://github.com/allthingslinux/tux/blob/main/assets/emojis/jail.png?raw=true" + KICK: "https://github.com/allthingslinux/tux/blob/main/assets/emojis/kick.png?raw=true" + TIMEOUT: "https://github.com/allthingslinux/tux/blob/main/assets/emojis/timeout.png?raw=true" + WARN: "https://github.com/allthingslinux/tux/blob/main/assets/emojis/warn.png?raw=true" From 66fc66247c1d296d298d4bc707e21e9fa9440da3 Mon Sep 17 00:00:00 2001 From: kzndotsh Date: Thu, 22 Aug 2024 01:45:57 +0000 Subject: [PATCH 39/84] docs(README.md, development.md): update file references from .json to .yml to reflect changes in configuration file format fix(development.md): correct .env.example to .env to accurately represent the environment file name --- README.md | 4 ++-- docs/development.md | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index a082e71..9c6aa33 100644 --- a/README.md +++ b/README.md @@ -110,10 +110,10 @@ Further detailed instructions can be found in the [development guide](docs/devel We offer dev tokens on request in our Discord server. -7. Copy the `config/settings.json.example` file to `config/settings.json` and fill in the required values. +7. Copy the `config/settings.yml.example` file to `config/settings.yml` and fill in the required values. ```bash - cp config/settings.json.example config/settings.json + cp config/settings.yml.example config/settings.yml ``` Be sure to add your Discord user ID to the `BOT_OWNER` key in the settings file. diff --git a/docs/development.md b/docs/development.md index 08c0537..20ba4e6 100644 --- a/docs/development.md +++ b/docs/development.md @@ -50,9 +50,9 @@ The project is structured as follows: ### Configuration -- `.env.example`: The example environment file containing the environment variables required for the bot. +- `.env`: The environment file containing the secret environment variables for the bot. - `config/`: The config directory containing the configuration files for the bot. - - `settings.json`: The settings file containing the bot settings and configuration. + - `settings.yml`: The settings file containing the bot settings and configuration. ### Documentation From 1b3fe5024d85765a760d12af85b2ec0e84115a24 Mon Sep 17 00:00:00 2001 From: kzndotsh Date: Thu, 22 Aug 2024 01:52:18 +0000 Subject: [PATCH 40/84] feat: add SECURITY.md to provide guidelines for reporting vulnerabilities and clarify support versions --- SECURITY.md => .github/SECURITY.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename SECURITY.md => .github/SECURITY.md (100%) diff --git a/SECURITY.md b/.github/SECURITY.md similarity index 100% rename from SECURITY.md rename to .github/SECURITY.md From 20680eb502ab33db428905f094db2652d95a26b7 Mon Sep 17 00:00:00 2001 From: kzndotsh Date: Wed, 21 Aug 2024 21:55:44 -0400 Subject: [PATCH 41/84] chore: move docs/resources to assets/branding --- {docs/resources => assets/branding}/tux.gif | Bin 1 file changed, 0 insertions(+), 0 deletions(-) rename {docs/resources => assets/branding}/tux.gif (100%) diff --git a/docs/resources/tux.gif b/assets/branding/tux.gif similarity index 100% rename from docs/resources/tux.gif rename to assets/branding/tux.gif From ea1df7797c32baccf78ccbd7c0a9111498c7c048 Mon Sep 17 00:00:00 2001 From: kzndotsh Date: Thu, 22 Aug 2024 02:55:57 +0000 Subject: [PATCH 42/84] chore(pyproject.toml): update dependencies and linting rules - feat(pyproject.toml): add pyyaml to main dependencies for YAML file handling - fix(pyproject.toml): move pyyaml from wrong section to main dependencies - refactor(pyproject.toml): add new linting rules "PLR0913", "PLR2004" to ignore list for better code quality - feat(pyproject.toml): add "FURB" and "PL" to select list for enhanced linting checks --- pyproject.toml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 9db141a..015edd9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,6 +27,7 @@ pynacl = "^1.5.0" pyright = "^1.1.358" python = ">=3.12,<4" python-dotenv = "^1.0.1" +pyyaml = "^6.0.2" reactionmenu = "^3.1.7" rsa = "^4.9" ruff = "^0.6.0" @@ -34,7 +35,6 @@ sentry-sdk = {extras = ["httpx", "loguru"], version = "^2.7.0"} types-aiofiles = "^24.1.0.20240626" types-psutil = "^6.0.0.20240621" typing-extensions = "^4.12.2" -pyyaml = "^6.0.2" [tool.poetry.group.docs.dependencies] mkdocs-material = "^9.5.30" @@ -87,8 +87,7 @@ target-version = "py312" [tool.ruff.lint] dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" fixable = ["ALL"] -ignore = ["E501", "N814"] - +ignore = ["E501", "N814", "PLR0913", "PLR2004"] select = [ "I", # isort "E", # pycodestyle-error @@ -97,6 +96,8 @@ select = [ "N", # pep8-naming "TRY", # tryceratops "UP", # pyupgrade + "FURB", # refurb + "PL", # pylint "B", # flake8-bugbear "SIM", # flake8-simplify "ASYNC", # flake8-async From c2cca276bdbc9d59354a3597e231a2327580dc11 Mon Sep 17 00:00:00 2001 From: kzndotsh Date: Thu, 22 Aug 2024 02:56:33 +0000 Subject: [PATCH 43/84] refactor(cases.py): simplify target conversion by using UserConverter only fix(cases.py): use ctx.author for moderator assignment to avoid unnecessary conversion feat(cases.py): add check for moderator type to ensure it's a discord.Member, convert if not --- tux/cogs/moderation/cases.py | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/tux/cogs/moderation/cases.py b/tux/cogs/moderation/cases.py index 3b831b1..eba325c 100644 --- a/tux/cogs/moderation/cases.py +++ b/tux/cogs/moderation/cases.py @@ -150,10 +150,7 @@ class Cases(ModerationCogBase): await ctx.send("Case not found.", delete_after=30) return - target = await commands.MemberConverter().convert( - ctx, - str(case.case_target_id), - ) or await commands.UserConverter().convert(ctx, str(case.case_target_id)) + target = await commands.UserConverter().convert(ctx, str(case.case_target_id)) await self._handle_case_response(ctx, case, "viewed", case.case_reason, target) @@ -233,10 +230,7 @@ class Cases(ModerationCogBase): await ctx.send("Failed to update case.", delete_after=30, ephemeral=True) return - target = await commands.MemberConverter().convert( - ctx, - str(case.case_target_id), - ) or await commands.UserConverter().convert(ctx, str(case.case_target_id)) + target = await commands.UserConverter().convert(ctx, str(case.case_target_id)) await self._handle_case_response(ctx, updated_case, "updated", updated_case.case_reason, target) @@ -266,10 +260,10 @@ class Cases(ModerationCogBase): """ if case is not None: - moderator = await commands.MemberConverter().convert( - ctx, - str(case.case_moderator_id), - ) or await commands.UserConverter().convert(ctx, str(case.case_moderator_id)) + moderator = ctx.author + + if not isinstance(moderator, discord.Member): + moderator = await commands.MemberConverter().convert(ctx, str(case.case_moderator_id)) fields = self._create_case_fields(moderator, target, reason) From 76593ac7b5afa28a855c9a7fa7c207b8a7c4828a Mon Sep 17 00:00:00 2001 From: kzndotsh Date: Thu, 22 Aug 2024 02:56:54 +0000 Subject: [PATCH 44/84] refactor(error.py, exceptions.py): move custom exceptions to a separate file for better code organization fix(error.py): update error_map to import exceptions from new location to maintain functionality --- tux/handlers/error.py | 19 +++---------------- tux/utils/exceptions.py | 14 ++++++++++++++ 2 files changed, 17 insertions(+), 16 deletions(-) create mode 100644 tux/utils/exceptions.py diff --git a/tux/handlers/error.py b/tux/handlers/error.py index e37a3f5..089c64d 100644 --- a/tux/handlers/error.py +++ b/tux/handlers/error.py @@ -6,21 +6,8 @@ from discord import app_commands from discord.ext import commands from loguru import logger -import tux.handlers.error as error from tux.utils.embeds import create_error_embed - - -class PermissionLevelError(commands.CheckFailure): - def __init__(self, permission: str) -> None: - self.permission = permission - super().__init__(f"User does not have the required permission: {permission}") - - -class AppCommandPermissionLevelError(app_commands.CheckFailure): - def __init__(self, permission: str) -> None: - self.permission = permission - super().__init__(f"User does not have the required permission: {permission}") - +from tux.utils.exceptions import AppCommandPermissionLevelError, PermissionLevelError error_map: dict[type[Exception], str] = { # app_commands @@ -51,8 +38,8 @@ error_map: dict[type[Exception], str] = { commands.NotOwner: "User not in sudoers file. This incident will be reported. (Not Owner)", commands.BotMissingPermissions: "User not in sudoers file. This incident will be reported. (Bot Missing Permissions)", # Custom errors - error.PermissionLevelError: "User not in sudoers file. This incident will be reported. (Missing required permission: {error.permission})", - error.AppCommandPermissionLevelError: "User not in sudoers file. This incident will be reported. (Missing required permission: {error.permission})", + PermissionLevelError: "User not in sudoers file. This incident will be reported. (Missing required permission: {error.permission})", + AppCommandPermissionLevelError: "User not in sudoers file. This incident will be reported. (Missing required permission: {error.permission})", } diff --git a/tux/utils/exceptions.py b/tux/utils/exceptions.py new file mode 100644 index 0000000..d84b4e5 --- /dev/null +++ b/tux/utils/exceptions.py @@ -0,0 +1,14 @@ +from discord import app_commands +from discord.ext import commands + + +class PermissionLevelError(commands.CheckFailure): + def __init__(self, permission: str) -> None: + self.permission = permission + super().__init__(f"User does not have the required permission: {permission}") + + +class AppCommandPermissionLevelError(app_commands.CheckFailure): + def __init__(self, permission: str) -> None: + self.permission = permission + super().__init__(f"User does not have the required permission: {permission}") From 817756c89428e281276661cca7e215a0bfb2c0fe Mon Sep 17 00:00:00 2001 From: kzndotsh Date: Thu, 22 Aug 2024 02:57:15 +0000 Subject: [PATCH 45/84] refactor(checks.py): split large function into smaller ones for better readability and maintainability fix(checks.py): improve error handling by adding try-except blocks around database operations docs(checks.py): update function docstrings to better reflect their purpose and parameters style(checks.py): improve code readability by removing unnecessary comments and improving variable names refactor(checks.py): simplify function comments and error handling for better readability fix(checks.py): change role_ids type from list[int] to list[Any] to handle non-integer role_ids style(checks.py): remove unnecessary comments and white spaces for cleaner code --- tux/utils/checks.py | 349 ++++++++++++++++++++++---------------------- 1 file changed, 174 insertions(+), 175 deletions(-) diff --git a/tux/utils/checks.py b/tux/utils/checks.py index f1ad2e8..89058ae 100644 --- a/tux/utils/checks.py +++ b/tux/utils/checks.py @@ -6,8 +6,8 @@ from discord.ext import commands from loguru import logger from tux.database.controllers import DatabaseController -from tux.handlers.error import AppCommandPermissionLevelError, PermissionLevelError from tux.utils.constants import CONST +from tux.utils.exceptions import AppCommandPermissionLevelError, PermissionLevelError db = DatabaseController().guild_config @@ -18,12 +18,12 @@ async def has_permission( higher_bound: int | None = None, ) -> bool: """ - Check if a user has a permission level. + Check if the source has the required permission level. Parameters ---------- source : commands.Context[commands.Bot] | discord.Interaction - The source object for the command. + The source of the command. lower_bound : int The lower bound of the permission level. higher_bound : int | None, optional @@ -32,82 +32,152 @@ async def has_permission( Returns ------- bool - Whether the user has the permission level. + Whether the source has the required permission level. """ - # If higher_bound is None, set it to lower_bound higher_bound = higher_bound or lower_bound - # If the source is a context object and the guild is None, return False if the lower bound is not 0 if source.guild is None: logger.debug("Guild is None, returning False if lower bound is not 0") return lower_bound == 0 - # If the source is a context object, set the context object to source ctx = source if isinstance(source, commands.Context) else None - # If the source is an interaction object, set the interaction object to source interaction = source if isinstance(source, discord.Interaction) else None - # Initialize the list of roles to an empty list to avoid type errors - roles: list[Any] = [] + roles = await get_roles_for_bounds(source, lower_bound, higher_bound) try: - if higher_bound == lower_bound: - # Get the role ID for the permission level - role_id = await get_perm_level_role_id(source, f"perm_level_{lower_bound}_role_id") + author = await get_author_from_source(ctx, interaction) - # If the role ID exists, append it to the list of roles - if role_id: - roles.append(role_id) - else: - logger.debug(f"No Role ID fetched for perm_level_{lower_bound}_role_id") - - else: - # Get the role IDs for the permission levels - fetched_roles = await get_perm_level_roles(source, lower_bound) - - # If the role IDs exist, extend the list of roles with the fetched roles - if fetched_roles: - roles.extend(fetched_roles) - - else: - logger.debug(f"No roles fetched for levels above and equal to {lower_bound}") - - # Set the author to None to avoid type errors - author = None - - # If the context object or interaction object exists and the guild is not None, fetch the author - if ctx and ctx.guild: - author = await ctx.guild.fetch_member(ctx.author.id) - elif interaction and interaction.guild: - author = await interaction.guild.fetch_member(interaction.user.id) - - # If the author is not None and the author has any of the roles in the list of roles, return True if author and any(role.id in roles for role in author.roles): return True except Exception as e: logger.error(f"Exception in permission check: {e}") - logger.debug("All checks failed, checking for sysadmin or bot owner status") + return await check_sysadmin_or_owner(ctx, interaction, lower_bound, higher_bound) + + +async def get_roles_for_bounds( + source: Any, + lower_bound: int, + higher_bound: int, +) -> list[int]: + """ + Get the roles for the given bounds. + + Parameters + ---------- + source : Any + The source of the command. + lower_bound : int + The lower bound of the permission level. + higher_bound : int + The higher bound of the permission level. + + Returns + ------- + list[int] + The list of role IDs for the given bounds. + """ + + roles: list[Any] = [] try: - # Get the user ID from the context object or interaction object - user_id = ctx.author.id if ctx else interaction.user.id if interaction else None + if higher_bound == lower_bound: + role_id = await get_perm_level_role_id(source, f"perm_level_{lower_bound}_role_id") + + if role_id: + roles.append(role_id) + else: + logger.debug(f"No Role ID fetched for perm_level_{lower_bound}_role_id") + + else: + fetched_roles = await get_perm_level_roles(source, lower_bound) + roles.extend(fetched_roles or []) + + except Exception as e: + logger.error(f"Error fetching roles: {e}") + + return roles + + +async def get_author_from_source( + ctx: Any, + interaction: Any, +) -> Any: + """ + Get the author from the source. + + Parameters + ---------- + ctx : Any + The context of the command. + interaction : Any + The interaction of the command. + + Returns + ------- + Any + The author of the command. + """ + + try: + if ctx and ctx.guild: + return await ctx.guild.fetch_member(ctx.author.id) + + if interaction and interaction.guild: + return await interaction.guild.fetch_member(interaction.user.id) + + except Exception as e: + logger.error(f"Error fetching author: {e}") + + return None + + +async def check_sysadmin_or_owner( + ctx: Any, + interaction: Any, + lower_bound: int, + higher_bound: int, +) -> bool: + """ + Check if the user is a sysadmin or bot owner. + + Parameters + ---------- + ctx : Any + The context of the command. + interaction : Any + The interaction of the command. + lower_bound : int + The lower bound of the permission level. + higher_bound : int + The higher bound of the permission level. + + Returns + ------- + bool + Whether the user is a sysadmin or bot owner. + """ + + try: + user_id = ctx.author.id if ctx else (interaction.user.id if interaction else None) if user_id: - # Check if the user is a sysadmin or the bot owner if 8 in range(lower_bound, higher_bound + 1) and user_id in CONST.SYSADMIN_IDS: logger.debug("User is a sysadmin") + return True + if 9 in range(lower_bound, higher_bound + 1) and user_id == CONST.BOT_OWNER_ID: logger.debug("User is the bot owner") + return True except Exception as e: logger.error(f"Exception while checking sysadmin or bot owner status: {e}") - logger.debug("All checks failed, returning False") return False @@ -117,14 +187,16 @@ async def level_to_name( or_higher: bool = False, ) -> str: """ - Convert a permission level to a name. + Get the name of the permission level. Parameters ---------- + source : commands.Context[commands.Bot] | discord.Interaction + The source of the command. level : int - The permission level to convert. + The permission level. or_higher : bool, optional - Whether the user should have the permission level or higher, by default False + Whether to include "or higher" in the name, by default False. Returns ------- @@ -132,34 +204,15 @@ async def level_to_name( The name of the permission level. """ - # Check if the level is 8 or 9 and return the corresponding name if level in {8, 9}: return "Sys Admin" if level == 8 else "Bot Owner" - # Check if the source is a context object and the guild is not None - if isinstance(source, commands.Context): - ctx = source - if ctx.guild is None: - return "Error" + role_name = await get_role_name_from_source(source, level) - # Get the role ID from the database for the guild and the role field - role_id = await db.get_perm_level_role(ctx.guild.id, f"perm_level_{level}_role_id") - if role_id and (role := ctx.guild.get_role(role_id)): - return f"{role.name} or higher" if or_higher else role.name + if role_name: + return f"{role_name} or higher" if or_higher else role_name - else: - # Get the interaction object and check if it exists and is in a guild - interaction = source - if not interaction or not interaction.guild: - return "Error" - - # Get the role ID from the database for the guild and the role field - role_id = await db.get_perm_level_role(interaction.guild.id, f"perm_level_{level}_role_id") - if role_id and (role := interaction.guild.get_role(role_id)): - return f"{role.name} or higher" if or_higher else role.name - - # Dictionary of permission levels with the level as the key and the name as the value - dictionary = { + default_names = { 0: "Member", 1: "Support", 2: "Junior Moderator", @@ -172,9 +225,35 @@ async def level_to_name( 9: "Bot Owner", } - # Return the name of the permission level from the dictionary - # or the name of the permission level with "or higher" appended if or_higher is True - return f"{dictionary[level]} or higher" if or_higher else dictionary[level] + return f"{default_names[level]} or higher" if or_higher else default_names[level] + + +async def get_role_name_from_source( + source: Any, + level: int, +) -> str | None: + """ + Get the name of the role for the given level from the source. + + Parameters + ---------- + source : Any + The source of the command. + level : int + The permission level. + + Returns + ------- + str | None + The name of the role for the given level. + """ + + role_id = await db.get_perm_level_role(source.guild.id, f"perm_level_{level}_role_id") + + if role_id and (role := source.guild.get_role(role_id)): + return role.name + + return None async def get_perm_level_role_id( @@ -182,47 +261,28 @@ async def get_perm_level_role_id( level: str, ) -> int | None: """ - Get the role ID for a permission level. + Get the role ID for the given permission level. Parameters ---------- source : commands.Context[commands.Bot] | discord.Interaction - The source object for the command. + The source of the command. level : str - The permission level to get the role ID for. + The permission level. Returns ------- int | None - The role ID for the permission level or None if it does not exist. + The role ID for the given permission level or None. """ - # Initialize the role ID to None to avoid type errors - role_id = None - try: - # Check if the source is a context object and if the guild is not None - if isinstance(source, commands.Context): - ctx = source - if ctx.guild is None: - return None - - # Get the role ID from the database for the guild and the role field - role_id = await db.get_perm_level_role(ctx.guild.id, level) - - else: - # Get the interaction object and check if it exists and is in a guild - interaction = source - if not interaction or not interaction.guild: - return None - - # Get the role ID from the database for the guild and the role field - role_id = await db.get_perm_level_role(interaction.guild.id, level) + guild = source.guild + return await db.get_perm_level_role(guild.id, level) if guild else None except Exception as e: logger.error(f"Error retrieving role ID for level {level}: {e}") - - return role_id + return None async def get_perm_level_roles( @@ -230,23 +290,22 @@ async def get_perm_level_roles( lower_bound: int, ) -> list[int] | None: """ - Get the role IDs for a range of permission levels. + Get the role IDs for the given permission levels. Parameters ---------- source : commands.Context[commands.Bot] | discord.Interaction - The source object for the command. + The source of the command. lower_bound : int The lower bound of the permission level. Returns ------- list[int] | None - The list of role IDs for the permission levels or None if they do not exist. + The role IDs for the given permission levels or None. """ - # Dictionary of permission level roles with the level as the key and the field name as the value - perm_level_roles: dict[int, str] = { + perm_level_roles = { 0: "perm_level_0_role_id", 1: "perm_level_1_role_id", 2: "perm_level_2_role_id", @@ -257,29 +316,21 @@ async def get_perm_level_roles( 7: "perm_level_7_role_id", } - # Initialize the list of role IDs to an empty list to avoid type errors - role_ids: list[int] = [] + role_ids: list[Any] = [] try: - # For each level in the range of the lower bound to 8 for level in range(lower_bound, 8): - # If the role field exists, get the role ID by the field name (e.g. perm_level_1_role_id) if role_field := perm_level_roles.get(level): - # Get the role ID from the database for the guild and the role field role_id = await db.get_guild_config_field_value(source.guild.id, role_field) # type: ignore - # If the role ID exists, append it to the list of role IDs + if role_id: role_ids.append(role_id) + else: logger.debug(f"No role ID found for {role_field}, skipping") - # Catch any exceptions that occur while getting the role IDs - except KeyError as e: - logger.error(f"Key error when accessing role field: {e}") - except AttributeError as e: - logger.error(f"Attribute error, likely due to accessing a wrong attribute: {e}") except Exception as e: - logger.error(f"General error getting perm level roles: {e}") + logger.error(f"Error getting perm level roles: {e}") return None return role_ids @@ -287,52 +338,26 @@ async def get_perm_level_roles( def has_pl(level: int, or_higher: bool = True): """ - Check if a user has a permission level via a decorator for prefix and hybrid commands. + Check if the source has the required permission level. This is a decorator for traditional "prefix" commands. Parameters ---------- level : int - The permission level to check. + The permission level required. or_higher : bool, optional - Whether the user should have the permission level or higher, by default True. - - Returns - ------- - commands.check - The check for the permission level + Whether to include "or higher" in the name, by default True. """ async def predicate(ctx: commands.Context[commands.Bot] | discord.Interaction) -> bool: - """ - Check if the user has the permission level. - - Parameters - ---------- - ctx : commands.Context[commands.Bot] | discord.Interaction - The context or interaction object for the command. - - Returns - ------- - bool - Whether the user has the permission level. - - Raises - ------ - PermissionLevelError - If the user does not have the permission level. - """ - if isinstance(ctx, discord.Interaction): logger.error("Incorrect checks decorator used. Please use ac_has_pl instead.") msg = "Incorrect checks decorator used. Please use ac_has_pl instead and report this as a issue." - raise PermissionLevelError(msg) if not await has_permission(ctx, level, 9 if or_higher else None): logger.error( f"{ctx.author} tried to run a command without perms. Command: {ctx.command}, Perm Level: {level} or higher: {or_higher}", ) - raise PermissionLevelError(await level_to_name(ctx, level, or_higher)) logger.info( @@ -346,52 +371,26 @@ def has_pl(level: int, or_higher: bool = True): def ac_has_pl(level: int, or_higher: bool = True): """ - Check if a user has a permission level via a decorator for app commands. + Check if the source has the required permission level. This is a decorator for application "slash" commands. Parameters ---------- level : int - The permission level to check. + The permission level required. or_higher : bool, optional - Whether the user should have the permission level or higher, by default True. - - Returns - ------- - app_commands.check - The check for the permission level + Whether to include "or higher" in the name, by default True. """ async def predicate(ctx: commands.Context[commands.Bot] | discord.Interaction) -> bool: - """ - Check if the user has the permission level. - - Parameters - ---------- - ctx : commands.Context[commands.Bot] | discord.Interaction - The context or interaction object for the command. - - Returns - ------- - bool - Whether the user has the permission level. - - Raises - ------ - AppCommandPermissionLevelError - If the user does not have the permission level. - """ - if isinstance(ctx, commands.Context): logger.error("Incorrect checks decorator used. Please use has_pl instead.") msg = "Incorrect checks decorator used. Please use has_pl instead and report this as a issue." - raise AppCommandPermissionLevelError(msg) if not await has_permission(ctx, level, 9 if or_higher else None): logger.error( f"{ctx.user} tried to run a command without perms. Command: {ctx.command}, Perm Level: {level} or higher: {or_higher}", ) - raise AppCommandPermissionLevelError(await level_to_name(ctx, level, or_higher)) logger.info( From 424310dda7f6cb3293b44a5fe9443438692c4eb5 Mon Sep 17 00:00:00 2001 From: kzndotsh Date: Thu, 22 Aug 2024 02:57:33 +0000 Subject: [PATCH 46/84] fix(constants.py): change default values for DEBUG, GITHUB_APP_ID, and GITHUB_INSTALLATION_ID to string to avoid type mismatch errors refactor(constants.py): change GITHUB_INSTALLATION_ID type from int to str for consistency with other GitHub related constants --- tux/utils/constants.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tux/utils/constants.py b/tux/utils/constants.py index da2d732..3a5df32 100644 --- a/tux/utils/constants.py +++ b/tux/utils/constants.py @@ -29,7 +29,7 @@ class Constants: DEV_COG_IGNORE_LIST: Final[set[str]] = set(os.getenv("DEV_COG_IGNORE_LIST", "").split(",")) # Debug env constants - DEBUG: Final[bool] = bool(os.getenv("DEBUG", True)) + DEBUG: Final[bool] = bool(os.getenv("DEBUG", "True")) # Final env constants TOKEN: Final[str] = DEV_TOKEN if DEV and DEV.lower() == "true" else PROD_TOKEN @@ -52,11 +52,11 @@ class Constants: GITHUB_REPO_OWNER: Final[str] = os.getenv("GITHUB_REPO_OWNER", "") GITHUB_REPO: Final[str] = os.getenv("GITHUB_REPO", "") GITHUB_TOKEN: Final[str] = os.getenv("GITHUB_TOKEN", "") - GITHUB_APP_ID: Final[int] = int(os.getenv("GITHUB_APP_ID", 0)) + GITHUB_APP_ID: Final[int] = int(os.getenv("GITHUB_APP_ID", "0")) GITHUB_CLIENT_ID = os.getenv("GITHUB_CLIENT_ID", "") GITHUB_CLIENT_SECRET = os.getenv("GITHUB_CLIENT_SECRET", "") GITHUB_PUBLIC_KEY = os.getenv("GITHUB_PUBLIC_KEY", "") - GITHUB_INSTALLATION_ID: Final[int] = int(os.getenv("GITHUB_INSTALLATION_ID", 0)) + GITHUB_INSTALLATION_ID: Final[str] = os.getenv("GITHUB_INSTALLATION_ID", "0") GITHUB_PRIVATE_KEY: str = ( base64.b64decode(os.getenv("GITHUB_PRIVATE_KEY_BASE64", "")).decode("utf-8") if os.getenv("GITHUB_PRIVATE_KEY_BASE64") From a8ce2999ab51801e24b47f7a80fc28a99123f58b Mon Sep 17 00:00:00 2001 From: kzndotsh Date: Thu, 22 Aug 2024 02:57:50 +0000 Subject: [PATCH 47/84] refactor(event.py): simplify conditional statements for better readability feat(event.py): add walrus operator (:=) to assign and check tags in a single line chore(event.py): add TODO comments for future database configuration tasks style(event.py): improve code formatting for better readability --- tux/handlers/event.py | 38 ++++++++++++++++++-------------------- 1 file changed, 18 insertions(+), 20 deletions(-) diff --git a/tux/handlers/event.py b/tux/handlers/event.py index 6422e72..5255538 100644 --- a/tux/handlers/event.py +++ b/tux/handlers/event.py @@ -43,9 +43,7 @@ class EventHandler(commands.Cog): flag_list = ["🏳️‍🌈", "🏳️‍⚧️"] user = self.bot.get_user(payload.user_id) - if user is None: - return - if user.bot: + if user is None or user.bot: return if payload.guild_id is None: @@ -59,44 +57,44 @@ class EventHandler(commands.Cog): return channel = self.bot.get_channel(payload.channel_id) - if channel is None: - return - if channel.id != 1172343581495795752: - return - if not isinstance(channel, discord.TextChannel): + if channel is None or channel.id != 1172343581495795752 or not isinstance(channel, discord.TextChannel): return message = await channel.fetch_message(payload.message_id) emoji = payload.emoji - if any(0x1F1E3 <= ord(char) <= 0x1F1FF for char in emoji.name): - await message.remove_reaction(emoji, member) - return - if "flag" in emoji.name.lower(): - await message.remove_reaction(emoji, member) - return - if emoji.name in flag_list: + if ( + any(0x1F1E3 <= ord(char) <= 0x1F1FF for char in emoji.name) + or "flag" in emoji.name.lower() + or emoji.name in flag_list + ): await message.remove_reaction(emoji, member) return @commands.Cog.listener() async def on_thread_create(self, thread: discord.Thread) -> None: - # Temporary hardcoded support forum ID and general chat ID + # TODO: Add database configuration for primmary support forum support_forum = 1172312653797007461 - general_chat = 1172245377395728467 - support_role = "<@&1274823545087590533>" if thread.parent_id == support_forum: owner_mention = thread.owner.mention if thread.owner else {thread.owner_id} - tags = [tag.name for tag in thread.applied_tags] - if tags: + + if tags := [tag.name for tag in thread.applied_tags]: tag_list = ", ".join(tags) msg = f"<:tux_notify:1274504953666474025> **New support thread created** - help is appreciated!\n{thread.mention} by {owner_mention}\n<:tux_tag:1274504955163709525> **Tags**: `{tag_list}`" + else: msg = f"<:tux_notify:1274504953666474025> **New support thread created** - help is appreciated!\n{thread.mention} by {owner_mention}" + embed = discord.Embed(description=msg, color=discord.Color.random()) + + general_chat = 1172245377395728467 channel = self.bot.get_channel(general_chat) + if channel is not None and isinstance(channel, discord.TextChannel): + # TODO: Add database configuration for primary support role + support_role = "<@&1274823545087590533>" + await channel.send(content=support_role, embed=embed, allowed_mentions=discord.AllowedMentions.none()) From 20e06f1e2b0de3ff8b5595476549be4a4cfd6e86 Mon Sep 17 00:00:00 2001 From: kzndotsh Date: Thu, 22 Aug 2024 03:01:57 +0000 Subject: [PATCH 48/84] refactor(checks.py): change logger level from error to info for unauthorized command attempts to better reflect the severity of the event --- tux/utils/checks.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tux/utils/checks.py b/tux/utils/checks.py index 89058ae..f910377 100644 --- a/tux/utils/checks.py +++ b/tux/utils/checks.py @@ -355,7 +355,7 @@ def has_pl(level: int, or_higher: bool = True): raise PermissionLevelError(msg) if not await has_permission(ctx, level, 9 if or_higher else None): - logger.error( + logger.info( f"{ctx.author} tried to run a command without perms. Command: {ctx.command}, Perm Level: {level} or higher: {or_higher}", ) raise PermissionLevelError(await level_to_name(ctx, level, or_higher)) @@ -388,7 +388,7 @@ def ac_has_pl(level: int, or_higher: bool = True): raise AppCommandPermissionLevelError(msg) if not await has_permission(ctx, level, 9 if or_higher else None): - logger.error( + logger.info( f"{ctx.user} tried to run a command without perms. Command: {ctx.command}, Perm Level: {level} or higher: {or_higher}", ) raise AppCommandPermissionLevelError(await level_to_name(ctx, level, or_higher)) From 54b46bc578e35b37d7c59bc39edca9ad4bed408a Mon Sep 17 00:00:00 2001 From: kzndotsh Date: Thu, 22 Aug 2024 03:09:09 +0000 Subject: [PATCH 49/84] docs(README.md): add HTTPX to list of features to reflect recent addition of HTTPX for request handling --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 9c6aa33..1443fd9 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,7 @@ It is designed to provide a variety of features to the server, including moderat - Justfile for easy CLI commands - Beautiful logging with Loguru - Exception handling with Sentry +- Request handling with HTTPX ## Bot Features From a107228fff5444e7bfc1f2c837cdd9df64563a3c Mon Sep 17 00:00:00 2001 From: kzndotsh Date: Thu, 22 Aug 2024 03:30:46 +0000 Subject: [PATCH 50/84] docs(README.md): update warning message to provide more accurate information and add support server link --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1443fd9..cbc7c20 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ > [!WARNING] -**This bot (without plenty of tweaking) is not ready for production use, we recommend against using it until it is more complete.** +**This bot (without plenty of tweaking) is not ready for production use, we suggest against using it until announced. Join our support server: [atl.dev](https://discord.gg/gpmSjcjQxg) for more info!** ## About From 1a1a310b28b81d471be5e326eff42e5ee706cdf4 Mon Sep 17 00:00:00 2001 From: kzndotsh Date: Thu, 22 Aug 2024 03:57:14 +0000 Subject: [PATCH 51/84] feat(pyproject.toml, bot.py): add jishaku library for debugging and load it as an extension in bot.py to enhance debugging capabilities --- poetry.lock | 84 +++++++++++++++++++++++++++++++++++++++++++++++++- pyproject.toml | 1 + tux/bot.py | 2 ++ 3 files changed, 86 insertions(+), 1 deletion(-) diff --git a/poetry.lock b/poetry.lock index 540ece8..2738485 100644 --- a/poetry.lock +++ b/poetry.lock @@ -190,6 +190,21 @@ doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphin test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] trio = ["trio (>=0.23)"] +[[package]] +name = "astunparse" +version = "1.6.3" +description = "An AST unparser for Python" +optional = false +python-versions = "*" +files = [ + {file = "astunparse-1.6.3-py2.py3-none-any.whl", hash = "sha256:c2652417f2c8b5bb325c885ae329bdf3f86424075c4fd1a128674bc6fba4b8e8"}, + {file = "astunparse-1.6.3.tar.gz", hash = "sha256:5ad93a8456f0d084c3456d059fd9a92cce667963232cbf763eac3bc5b7940872"}, +] + +[package.dependencies] +six = ">=1.6.1,<2.0" +wheel = ">=0.23.0,<1.0" + [[package]] name = "asynctempfile" version = "0.5.0" @@ -237,6 +252,17 @@ files = [ [package.extras] dev = ["freezegun (>=1.0,<2.0)", "pytest (>=6.0)", "pytest-cov"] +[[package]] +name = "braceexpand" +version = "0.1.7" +description = "Bash-style brace expansion for Python" +optional = false +python-versions = "*" +files = [ + {file = "braceexpand-0.1.7-py2.py3-none-any.whl", hash = "sha256:91332d53de7828103dcae5773fb43bc34950b0c8160e35e0f44c4427a3b85014"}, + {file = "braceexpand-0.1.7.tar.gz", hash = "sha256:e6e539bd20eaea53547472ff94f4fb5c3d3bf9d0a89388c4b56663aba765f705"}, +] + [[package]] name = "cairocffi" version = "1.7.1" @@ -881,6 +907,23 @@ files = [ {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"}, ] +[[package]] +name = "import-expression" +version = "1.1.5" +description = "Parses a superset of Python allowing for inline module import expressions" +optional = false +python-versions = "*" +files = [ + {file = "import_expression-1.1.5-py3-none-any.whl", hash = "sha256:f60c3765dbf2f41928b9c6ef79d632209b6705fc8f30e281ed1a492ed026b10f"}, + {file = "import_expression-1.1.5.tar.gz", hash = "sha256:9959588fcfc8dcb144a0725176cfef6c28c7db1fc2d683625025e687516d40c1"}, +] + +[package.dependencies] +astunparse = ">=1.6.3,<2.0.0" + +[package.extras] +test = ["pytest", "pytest-cov"] + [[package]] name = "jinja2" version = "3.1.4" @@ -898,6 +941,31 @@ MarkupSafe = ">=2.0" [package.extras] i18n = ["Babel (>=2.7)"] +[[package]] +name = "jishaku" +version = "2.5.2" +description = "A discord.py extension including useful tools for bot development and debugging." +optional = false +python-versions = ">=3.8.0" +files = [ + {file = "jishaku-2.5.2-py3-none-any.whl", hash = "sha256:87f34942ee44865f5ce08e36723b7c74a313d8a13a4db8a6b7cc12618cc3496c"}, + {file = "jishaku-2.5.2.tar.gz", hash = "sha256:56d38c333036e37481df5e3c9e81d6033b5097738f0d171a81e2752124f0df5c"}, +] + +[package.dependencies] +braceexpand = ">=0.1.7" +click = ">=8.0.1" +import-expression = ">=1.0.0,<2.0.0" + +[package.extras] +discordpy = ["discord.py (>=1.7.3)"] +docs = ["Sphinx (>=4.4.0)", "sphinxcontrib-trio (>=1.1.2)"] +procinfo = ["psutil (>=5.8.0)"] +profiling = ["line-profiler (>=3.5.1)"] +publish = ["Jinja2 (>=3.0.3)"] +test = ["coverage (>=6.3.2)", "flake8 (>=4.0.1)", "isort (>=5.10.1)", "pylint (>=2.11.1)", "pytest (>=7.0.1)", "pytest-asyncio (>=0.18.1)", "pytest-cov (>=3.0.0)", "pytest-mock (>=3.7.0)"] +voice = ["yt-dlp (>=2022.3.8)"] + [[package]] name = "loguru" version = "0.7.2" @@ -2204,6 +2272,20 @@ files = [ {file = "webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"}, ] +[[package]] +name = "wheel" +version = "0.44.0" +description = "A built-package format for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "wheel-0.44.0-py3-none-any.whl", hash = "sha256:2376a90c98cc337d18623527a97c31797bd02bad0033d41547043a1cbfbe448f"}, + {file = "wheel-0.44.0.tar.gz", hash = "sha256:a29c3f2817e95ab89aa4660681ad547c0e9547f20e75b0562fe7723c9a2a9d49"}, +] + +[package.extras] +test = ["pytest (>=6.0.0)", "setuptools (>=65)"] + [[package]] name = "win32-setctime" version = "1.1.0" @@ -2324,4 +2406,4 @@ multidict = ">=4.0" [metadata] lock-version = "2.0" python-versions = ">=3.12,<4" -content-hash = "b7e79ee5391b21488b2a71b8a447ffd2cf76aa9ced53f6ed96da383bf77c1aa7" +content-hash = "8b92b052826adf9c28d1314fc6058b2f6f24858025598aef6621c0e20441bd1e" diff --git a/pyproject.toml b/pyproject.toml index 015edd9..d55ec03 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,6 +35,7 @@ sentry-sdk = {extras = ["httpx", "loguru"], version = "^2.7.0"} types-aiofiles = "^24.1.0.20240626" types-psutil = "^6.0.0.20240621" typing-extensions = "^4.12.2" +jishaku = "^2.5.2" [tool.poetry.group.docs.dependencies] mkdocs-material = "^9.5.30" diff --git a/tux/bot.py b/tux/bot.py index f705ae1..f74cea7 100644 --- a/tux/bot.py +++ b/tux/bot.py @@ -32,6 +32,8 @@ class Tux(commands.Bot): logger.critical(f"An error occurred while connecting to the database: {e}") return + # Load Jishaku for debugging + await self.load_extension("jishaku") # Load cogs via CogLoader await self.load_cogs() From f07de586f4c075650d3fbca3db4a456750c6fcca Mon Sep 17 00:00:00 2001 From: kzndotsh Date: Thu, 22 Aug 2024 09:06:42 +0000 Subject: [PATCH 52/84] fix(github.py): cast GITHUB_INSTALLATION_ID to int to ensure compatibility with AppInstallationAuthStrategy constructor --- tux/wrappers/github.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tux/wrappers/github.py b/tux/wrappers/github.py index cfba44f..a9506b5 100644 --- a/tux/wrappers/github.py +++ b/tux/wrappers/github.py @@ -17,7 +17,7 @@ class GithubService: AppInstallationAuthStrategy( CONST.GITHUB_APP_ID, CONST.GITHUB_PRIVATE_KEY, - CONST.GITHUB_INSTALLATION_ID, + int(CONST.GITHUB_INSTALLATION_ID), CONST.GITHUB_CLIENT_ID, CONST.GITHUB_CLIENT_SECRET, ), From 61ce0b3c7d29c7603a7fbb38b76076f467ff4bcb Mon Sep 17 00:00:00 2001 From: Atmois Date: Thu, 22 Aug 2024 13:11:21 +0100 Subject: [PATCH 53/84] Add checks to ensure that user is not snippet banned or unbanned before executing the command --- tux/cogs/moderation/snippetban.py | 18 ++++++++++++------ tux/cogs/utility/snippets.py | 9 +++++++-- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/tux/cogs/moderation/snippetban.py b/tux/cogs/moderation/snippetban.py index bb32615..bb57977 100644 --- a/tux/cogs/moderation/snippetban.py +++ b/tux/cogs/moderation/snippetban.py @@ -47,12 +47,9 @@ class SnippetBan(ModerationCogBase): logger.warning("Snippet ban command used outside of a guild context.") return - # Check if the user is already snippet banned - cases = await self.case_controller.get_all_cases_by_type(ctx.guild.id, CaseType.SNIPPETBAN) - for case in cases: - if case.case_target_id == target.id: - await ctx.send(f"{target.mention} is already snippet banned.", delete_after=10) - return + if await self.is_snippetbanned(ctx.guild.id, target.id): + await ctx.send("User is already snippet banned.", delete_after=30) + return case = await self.db.case.insert_case( case_target_id=target.id, @@ -106,6 +103,15 @@ class SnippetBan(ModerationCogBase): await self.send_embed(ctx, embed, log_type="mod") await ctx.send(embed=embed, delete_after=30, ephemeral=True) + async def is_snippetbanned(self, guild_id: int, user_id: int) -> bool: + ban_cases = await self.case_controller.get_all_cases_by_type(guild_id, CaseType.SNIPPETBAN) + unban_cases = await self.case_controller.get_all_cases_by_type(guild_id, CaseType.SNIPPETUNBAN) + + ban_count = sum(1 for case in ban_cases if case.case_target_id == user_id) + unban_count = sum(1 for case in unban_cases if case.case_target_id == user_id) + + return ban_count > unban_count + async def setup(bot: commands.Bot) -> None: await bot.add_cog(SnippetBan(bot)) diff --git a/tux/cogs/utility/snippets.py b/tux/cogs/utility/snippets.py index 89f883b..ebee505 100644 --- a/tux/cogs/utility/snippets.py +++ b/tux/cogs/utility/snippets.py @@ -24,8 +24,13 @@ class Snippets(commands.Cog): self.case_controller = CaseController() async def is_snippetbanned(self, guild_id: int, user_id: int) -> bool: - cases = await self.case_controller.get_all_cases_by_type(guild_id, CaseType.SNIPPETBAN) - return any(case.case_target_id == user_id for case in cases) + ban_cases = await self.case_controller.get_all_cases_by_type(guild_id, CaseType.SNIPPETBAN) + unban_cases = await self.case_controller.get_all_cases_by_type(guild_id, CaseType.SNIPPETUNBAN) + + ban_count = sum(1 for case in ban_cases if case.case_target_id == user_id) + unban_count = sum(1 for case in unban_cases if case.case_target_id == user_id) + + return ban_count > unban_count @commands.command( name="snippets", From b3acb6d6b0d40b08ae43ccf932308f67ae5f94bd Mon Sep 17 00:00:00 2001 From: Atmois Date: Thu, 22 Aug 2024 13:11:39 +0100 Subject: [PATCH 54/84] Add snippet unban --- prisma/schema.prisma | 1 + tux/cogs/moderation/cases.py | 4 +- tux/cogs/moderation/snippetunban.py | 102 ++++++++++++++++++++++++++++ tux/utils/flags.py | 15 ++++ 4 files changed, 121 insertions(+), 1 deletion(-) create mode 100644 tux/cogs/moderation/snippetunban.py diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 9cab8c1..fb10a5a 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -34,6 +34,7 @@ enum CaseType { WARN JAIL UNJAIL + SNIPPETUNBAN } // Docs: https://www.prisma.io/docs/orm/prisma-schema/data-model/models#defining-models diff --git a/tux/cogs/moderation/cases.py b/tux/cogs/moderation/cases.py index 89f1ce0..f48a2cc 100644 --- a/tux/cogs/moderation/cases.py +++ b/tux/cogs/moderation/cases.py @@ -24,6 +24,7 @@ emojis: dict[str, int] = { "warn": 1268115764498399264, "jail": 1268115750392954880, "snippetban": 1275782294363312172, # Placeholder + "snippetunban": 1275782294363312172, # Placeholder } @@ -369,6 +370,7 @@ class Cases(ModerationCogBase): CaseType.JAIL: "jail", CaseType.UNJAIL: "jail", CaseType.SNIPPETBAN: "snippetban", + CaseType.SNIPPETUNBAN: "snippetunban", } emoji_name = emoji_map.get(case_type) if emoji_name is not None: @@ -383,7 +385,7 @@ class Cases(ModerationCogBase): if case_type in [CaseType.BAN, CaseType.KICK, CaseType.TIMEOUT, CaseType.WARN, CaseType.JAIL, CaseType.SNIPPETBAN] else "removed" - if case_type in [CaseType.UNBAN, CaseType.UNTIMEOUT, CaseType.UNJAIL] + if case_type in [CaseType.UNBAN, CaseType.UNTIMEOUT, CaseType.UNJAIL, CaseType.SNIPPETUNBAN] else None ) if action is not None: diff --git a/tux/cogs/moderation/snippetunban.py b/tux/cogs/moderation/snippetunban.py new file mode 100644 index 0000000..5c223fe --- /dev/null +++ b/tux/cogs/moderation/snippetunban.py @@ -0,0 +1,102 @@ +import discord +from discord.ext import commands +from loguru import logger + +from prisma.enums import CaseType +from prisma.models import Case +from tux.database.controllers.case import CaseController +from tux.utils import checks +from tux.utils.constants import Constants as CONST +from tux.utils.flags import SnippetUnbanFlags + +from . import ModerationCogBase + + +class SnippetUnban(ModerationCogBase): + def __init__(self, bot: commands.Bot) -> None: + super().__init__(bot) + self.case_controller = CaseController() + + @commands.hybrid_command( + name="snippetunban", + aliases=["sub"], + usage="snippetunban [target]", + ) + @commands.guild_only() + @checks.has_pl(3) + async def snippet_unban( + self, + ctx: commands.Context[commands.Bot], + target: discord.Member, + *, + flags: SnippetUnbanFlags, + ): + if ctx.guild is None: + logger.warning("Snippet ban command used outside of a guild context.") + return + + # Check if the user is already snippet banned + if not await self.is_snippetbanned(ctx.guild.id, target.id): + await ctx.send("User is not snippet banned.", delete_after=30) + return + + case = await self.db.case.insert_case( + case_target_id=target.id, + case_moderator_id=ctx.author.id, + case_type=CaseType.SNIPPETUNBAN, + case_reason=flags.reason, + guild_id=ctx.guild.id, + ) + + await self.send_dm(ctx, flags.silent, target, flags.reason, "Snippet Unbanned") + await self.handle_case_response(ctx, case, "created", flags.reason, target) + + async def handle_case_response( + self, + ctx: commands.Context[commands.Bot], + case: Case | None, + action: str, + reason: str, + target: discord.Member | discord.User, + previous_reason: str | None = None, + ) -> None: + moderator = ctx.author + + fields = [ + ("Moderator", f"__{moderator}__\n`{moderator.id}`", True), + ("Target", f"__{target}__\n`{target.id}`", True), + ("Reason", f"> {reason}", False), + ] + + if previous_reason: + fields.append(("Previous Reason", f"> {previous_reason}", False)) + + if case is not None: + embed = await self.create_embed( + ctx, + title=f"Case #{case.case_number} ({case.case_type}) {action}", + fields=fields, + color=CONST.EMBED_COLORS["CASE"], + icon_url=CONST.EMBED_ICONS["ACTIVE_CASE"], + ) + embed.set_thumbnail(url=target.avatar) + else: + embed = await self.create_embed( + ctx, + title=f"Case {action} ({CaseType.SNIPPETUNBAN})", + fields=fields, + color=CONST.EMBED_COLORS["CASE"], + icon_url=CONST.EMBED_ICONS["ACTIVE_CASE"], + ) + + await self.send_embed(ctx, embed, log_type="mod") + await ctx.send(embed=embed, delete_after=30, ephemeral=True) + + async def is_snippetbanned(self, guild_id: int, user_id: int) -> bool: + ban_cases = await self.case_controller.get_all_cases_by_type(guild_id, CaseType.SNIPPETBAN) + unban_cases = await self.case_controller.get_all_cases_by_type(guild_id, CaseType.SNIPPETUNBAN) + + ban_count = sum(1 for case in ban_cases if case.case_target_id == user_id) + unban_count = sum(1 for case in unban_cases if case.case_target_id == user_id) + + return ban_count > unban_count diff --git a/tux/utils/flags.py b/tux/utils/flags.py index 7afa75a..ae419ef 100644 --- a/tux/utils/flags.py +++ b/tux/utils/flags.py @@ -205,3 +205,18 @@ class SnippetBanFlags(commands.FlagConverter, delimiter=" ", prefix="-"): aliases=["s", "quiet"], default=False, ) + + +class SnippetUnbanFlags(commands.FlagConverter, delimiter=" ", prefix="-"): + reason: str = commands.flag( + name="reason", + description="The reason for the snippet unban.", + aliases=["r"], + default=MISSING, + ) + silent: bool = commands.flag( + name="silent", + description="Do not send a DM to the target.", + aliases=["s", "quiet"], + default=False, + ) From c8fc0628f1dc2e76fcd99f0e78ccf526e051ebf6 Mon Sep 17 00:00:00 2001 From: Atmois Date: Thu, 22 Aug 2024 15:08:45 +0100 Subject: [PATCH 55/84] Add role info subcommand --- tux/cogs/info/info.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/tux/cogs/info/info.py b/tux/cogs/info/info.py index 3f5383e..80f70a6 100644 --- a/tux/cogs/info/info.py +++ b/tux/cogs/info/info.py @@ -107,6 +107,32 @@ class Info(commands.Cog): await interaction.response.send_message(embed=embed) + @info.command(name="roles", description="Lists all roles in the server.") + async def roles(self, interaction: discord.Interaction) -> None: + """ + List all roles in the server. + + Parameters + ---------- + interaction : discord.Interaction + The discord interaction object. + """ + if not interaction.guild: + return + + guild = interaction.guild + roles = [role.mention for role in guild.roles] + + embed = EmbedCreator.create_info_embed( + title="Server Roles", + description=f"Role list for {guild.name}", + interaction=interaction, + ) + + embed.add_field(name="Roles", value=", ".join(roles), inline=False) + + await interaction.response.send_message(embed=embed) + async def setup(bot: commands.Bot) -> None: await bot.add_cog(Info(bot)) From f268d0e1733541d70b302b687bdbf28b69fa3744 Mon Sep 17 00:00:00 2001 From: Atmois Date: Thu, 22 Aug 2024 15:15:44 +0100 Subject: [PATCH 56/84] Add emote info subcommand --- tux/cogs/info/info.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/tux/cogs/info/info.py b/tux/cogs/info/info.py index 80f70a6..96999a7 100644 --- a/tux/cogs/info/info.py +++ b/tux/cogs/info/info.py @@ -133,6 +133,32 @@ class Info(commands.Cog): await interaction.response.send_message(embed=embed) + @info.command(name="emotes", description="Lists all emotes in the server.") + async def emotes(self, interaction: discord.Interaction) -> None: + """ + List all emotes in the server. + + Parameters + ---------- + interaction : discord.Interaction + The discord interaction object. + """ + if not interaction.guild: + return + + guild = interaction.guild + emotes = [str(emote) for emote in guild.emojis] + + embed = EmbedCreator.create_info_embed( + title="Server Emotes", + description=f"Emote list for {guild.name}", + interaction=interaction, + ) + + embed.add_field(name="Emotes", value=" ".join(emotes) if emotes else "No emotes available", inline=False) + + await interaction.response.send_message(embed=embed) + async def setup(bot: commands.Bot) -> None: await bot.add_cog(Info(bot)) From e12f195f3d601ec4db29cd5b9969778bce3aaba0 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 22 Aug 2024 16:17:09 +0000 Subject: [PATCH 57/84] fix(deps): update dependency ruff to v0.6.2 --- poetry.lock | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/poetry.lock b/poetry.lock index 2738485..e6833db 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1987,29 +1987,29 @@ pyasn1 = ">=0.1.3" [[package]] name = "ruff" -version = "0.6.1" +version = "0.6.2" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.6.1-py3-none-linux_armv6l.whl", hash = "sha256:b4bb7de6a24169dc023f992718a9417380301b0c2da0fe85919f47264fb8add9"}, - {file = "ruff-0.6.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:45efaae53b360c81043e311cdec8a7696420b3d3e8935202c2846e7a97d4edae"}, - {file = "ruff-0.6.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:bc60c7d71b732c8fa73cf995efc0c836a2fd8b9810e115be8babb24ae87e0850"}, - {file = "ruff-0.6.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c7477c3b9da822e2db0b4e0b59e61b8a23e87886e727b327e7dcaf06213c5cf"}, - {file = "ruff-0.6.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a0af7ab3f86e3dc9f157a928e08e26c4b40707d0612b01cd577cc84b8905cc9"}, - {file = "ruff-0.6.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:392688dbb50fecf1bf7126731c90c11a9df1c3a4cdc3f481b53e851da5634fa5"}, - {file = "ruff-0.6.1-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:5278d3e095ccc8c30430bcc9bc550f778790acc211865520f3041910a28d0024"}, - {file = "ruff-0.6.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fe6d5f65d6f276ee7a0fc50a0cecaccb362d30ef98a110f99cac1c7872df2f18"}, - {file = "ruff-0.6.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2e0dd11e2ae553ee5c92a81731d88a9883af8db7408db47fc81887c1f8b672e"}, - {file = "ruff-0.6.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d812615525a34ecfc07fd93f906ef5b93656be01dfae9a819e31caa6cfe758a1"}, - {file = "ruff-0.6.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:faaa4060f4064c3b7aaaa27328080c932fa142786f8142aff095b42b6a2eb631"}, - {file = "ruff-0.6.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:99d7ae0df47c62729d58765c593ea54c2546d5de213f2af2a19442d50a10cec9"}, - {file = "ruff-0.6.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:9eb18dfd7b613eec000e3738b3f0e4398bf0153cb80bfa3e351b3c1c2f6d7b15"}, - {file = "ruff-0.6.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:c62bc04c6723a81e25e71715aa59489f15034d69bf641df88cb38bdc32fd1dbb"}, - {file = "ruff-0.6.1-py3-none-win32.whl", hash = "sha256:9fb4c4e8b83f19c9477a8745e56d2eeef07a7ff50b68a6998f7d9e2e3887bdc4"}, - {file = "ruff-0.6.1-py3-none-win_amd64.whl", hash = "sha256:c2ebfc8f51ef4aca05dad4552bbcf6fe8d1f75b2f6af546cc47cc1c1ca916b5b"}, - {file = "ruff-0.6.1-py3-none-win_arm64.whl", hash = "sha256:3bc81074971b0ffad1bd0c52284b22411f02a11a012082a76ac6da153536e014"}, - {file = "ruff-0.6.1.tar.gz", hash = "sha256:af3ffd8c6563acb8848d33cd19a69b9bfe943667f0419ca083f8ebe4224a3436"}, + {file = "ruff-0.6.2-py3-none-linux_armv6l.whl", hash = "sha256:5c8cbc6252deb3ea840ad6a20b0f8583caab0c5ef4f9cca21adc5a92b8f79f3c"}, + {file = "ruff-0.6.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:17002fe241e76544448a8e1e6118abecbe8cd10cf68fde635dad480dba594570"}, + {file = "ruff-0.6.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3dbeac76ed13456f8158b8f4fe087bf87882e645c8e8b606dd17b0b66c2c1158"}, + {file = "ruff-0.6.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:094600ee88cda325988d3f54e3588c46de5c18dae09d683ace278b11f9d4d534"}, + {file = "ruff-0.6.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:316d418fe258c036ba05fbf7dfc1f7d3d4096db63431546163b472285668132b"}, + {file = "ruff-0.6.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d72b8b3abf8a2d51b7b9944a41307d2f442558ccb3859bbd87e6ae9be1694a5d"}, + {file = "ruff-0.6.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:2aed7e243be68487aa8982e91c6e260982d00da3f38955873aecd5a9204b1d66"}, + {file = "ruff-0.6.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d371f7fc9cec83497fe7cf5eaf5b76e22a8efce463de5f775a1826197feb9df8"}, + {file = "ruff-0.6.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8f310d63af08f583363dfb844ba8f9417b558199c58a5999215082036d795a1"}, + {file = "ruff-0.6.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7db6880c53c56addb8638fe444818183385ec85eeada1d48fc5abe045301b2f1"}, + {file = "ruff-0.6.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:1175d39faadd9a50718f478d23bfc1d4da5743f1ab56af81a2b6caf0a2394f23"}, + {file = "ruff-0.6.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:5b939f9c86d51635fe486585389f54582f0d65b8238e08c327c1534844b3bb9a"}, + {file = "ruff-0.6.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d0d62ca91219f906caf9b187dea50d17353f15ec9bb15aae4a606cd697b49b4c"}, + {file = "ruff-0.6.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:7438a7288f9d67ed3c8ce4d059e67f7ed65e9fe3aa2ab6f5b4b3610e57e3cb56"}, + {file = "ruff-0.6.2-py3-none-win32.whl", hash = "sha256:279d5f7d86696df5f9549b56b9b6a7f6c72961b619022b5b7999b15db392a4da"}, + {file = "ruff-0.6.2-py3-none-win_amd64.whl", hash = "sha256:d9f3469c7dd43cd22eb1c3fc16926fb8258d50cb1b216658a07be95dd117b0f2"}, + {file = "ruff-0.6.2-py3-none-win_arm64.whl", hash = "sha256:f28fcd2cd0e02bdf739297516d5643a945cc7caf09bd9bcb4d932540a5ea4fa9"}, + {file = "ruff-0.6.2.tar.gz", hash = "sha256:239ee6beb9e91feb8e0ec384204a763f36cb53fb895a1a364618c6abb076b3be"}, ] [[package]] From 630130bb64e96438c521a20d72176f3d336d41aa Mon Sep 17 00:00:00 2001 From: Atmois <130537361+Atmois@users.noreply.github.com> Date: Thu, 22 Aug 2024 17:37:36 +0100 Subject: [PATCH 58/84] Fix description for slowmode slash command --- tux/cogs/moderation/slowmode.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tux/cogs/moderation/slowmode.py b/tux/cogs/moderation/slowmode.py index b555b3c..2575237 100644 --- a/tux/cogs/moderation/slowmode.py +++ b/tux/cogs/moderation/slowmode.py @@ -23,14 +23,14 @@ class Slowmode(commands.Cog): channel: discord.TextChannel | discord.Thread | None = None, ) -> None: """ - Set or get the slowmode delay for a channel. + Set or get the slowmode for a channel. Parameters ---------- ctx : commands.Context[commands.Bot] The context of the command. - delay : int - The slowmode time in seconds, max is 21600. + action : str + Either 'get' to get the current slowmode or the slowmode time in seconds, max is 21600. channel : discord.TextChannel | discord.Thread | None The channel to set the slowmode in. """ From 9efbd07c2b4a1c486cafef0f3c52bc88b50d89e9 Mon Sep 17 00:00:00 2001 From: electron271 <66094410+electron271@users.noreply.github.com> Date: Thu, 22 Aug 2024 15:14:36 -0500 Subject: [PATCH 59/84] fix(main.py) change jishaku to use sysadmin and bot owner ids --- tux/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tux/main.py b/tux/main.py index 76b7ee1..69ef251 100644 --- a/tux/main.py +++ b/tux/main.py @@ -34,7 +34,7 @@ async def main() -> None: command_prefix=CONST.PREFIX, strip_after_prefix=True, intents=discord.Intents.all(), - owner_id=CONST.BOT_OWNER_ID, + owner_ids=[*CONST.SYSADMIN_IDS, CONST.BOT_OWNER_ID], allowed_mentions=discord.AllowedMentions(everyone=False), ) From b54697da3e02e25b397d21c8a6912b41f9ea7857 Mon Sep 17 00:00:00 2001 From: electron271 <66094410+electron271@users.noreply.github.com> Date: Thu, 22 Aug 2024 15:22:06 -0500 Subject: [PATCH 60/84] fix(eval.py) make eval.py use owner_ids (including sysadmin) --- tux/cogs/admin/eval.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/tux/cogs/admin/eval.py b/tux/cogs/admin/eval.py index d2b4ea3..2a889d1 100644 --- a/tux/cogs/admin/eval.py +++ b/tux/cogs/admin/eval.py @@ -5,7 +5,6 @@ from discord.ext import commands from loguru import logger from tux.utils import checks -from tux.utils.constants import Constants as CONST from tux.utils.embeds import EmbedCreator @@ -48,7 +47,7 @@ class Eval(commands.Cog): usage="eval [expression]", ) @commands.guild_only() - @checks.has_pl(9) + @checks.has_pl(8) # sysadmin or higher async def eval(self, ctx: commands.Context[commands.Bot], *, cmd: str) -> None: """ Evaluate a Python expression. (Owner only) @@ -61,10 +60,15 @@ class Eval(commands.Cog): The Python expression to evaluate. """ - # Check if the user is the bot owner - if ctx.author.id != CONST.BOT_OWNER_ID: + # Check if the user is in the discord.py owner_ids list in the bot instance + if self.bot.owner_ids is None: + logger.warning("Bot owner IDs are not set.") + await ctx.send("Bot owner IDs are not set. Better luck next time!", ephemeral=True, delete_after=30) + return + + if ctx.author.id not in self.bot.owner_ids: logger.warning( - f"{ctx.author} tried to run eval but is not the bot owner. (Owner ID: {self.bot.owner_id}, User ID: {ctx.author.id})", + f"{ctx.author} tried to run eval but is not the bot owner. (User ID: {ctx.author.id})", ) await ctx.send("You are not the bot owner. Better luck next time!", ephemeral=True, delete_after=30) return From 51c7d8c09492b036fb8d6840768f05d503abd209 Mon Sep 17 00:00:00 2001 From: kzndotsh Date: Fri, 23 Aug 2024 07:11:09 +0000 Subject: [PATCH 61/84] refactor(info.py): replace app_commands with commands for better compatibility feat(info.py): add aliases and usage instructions to commands for better user experience fix(info.py): replace interaction with ctx in command parameters for consistency with other commands style(info.py): improve formatting and readability of the code --- tux/cogs/info/info.py | 164 ++++++++++++++++++++++-------------------- 1 file changed, 85 insertions(+), 79 deletions(-) diff --git a/tux/cogs/info/info.py b/tux/cogs/info/info.py index 96999a7..04796c3 100644 --- a/tux/cogs/info/info.py +++ b/tux/cogs/info/info.py @@ -1,83 +1,83 @@ import discord -from discord import app_commands from discord.ext import commands -from tux.utils.embeds import EmbedCreator - class Info(commands.Cog): def __init__(self, bot: commands.Bot) -> None: self.bot = bot - info = app_commands.Group(name="info", description="Information commands.") + @commands.hybrid_group( + name="info", + aliases=["i"], + usage="info ", + ) + async def info(self, ctx: commands.Context[commands.Bot]) -> None: + """ + Information commands. - @info.command(name="server") - async def server(self, interaction: discord.Interaction) -> None: + Parameters + ---------- + ctx : commands.Context[commands.Bot] + The discord context object. + """ + if ctx.invoked_subcommand is None: + await ctx.send_help("info") + + @info.command( + name="server", + aliases=["s"], + usage="info server", + ) + async def server(self, ctx: commands.Context[commands.Bot]) -> None: """ Show information about the server. Parameters ---------- - interaction : discord.Interaction - The discord interaction object. + ctx : commands.Context[commands.Bot] + The discord context object. """ - if not interaction.guild: + + if not ctx.guild: return + guild = ctx.guild - guild = interaction.guild - owner = str(guild.owner) if guild.owner else "Unknown" - - embed = EmbedCreator.create_info_embed( - title=guild.name, - description="Here is some information about the server.", - interaction=interaction, + embed = discord.Embed( + title=ctx.guild.name, + description=guild.description or "No description available.", + color=discord.Color.blurple(), ) - embed.add_field(name="Members", value=str(guild.member_count)) - bots = sum(member.bot for member in guild.members if member.bot) - embed.add_field(name="Bots", value=str(bots)) - embed.add_field(name="Boosts", value=str(guild.premium_subscription_count)) - embed.add_field(name="Vanity URL", value=str(guild.vanity_url_code or "None")) - embed.add_field(name="Owner", value=owner) - embed.add_field(name="Created", value=guild.created_at.strftime("%d/%m/%Y")) - embed.add_field(name="ID", value=str(guild.id)) + embed.set_author(name="Server Information", icon_url=guild.icon) + embed.add_field(name="Owner", value=str(guild.owner.mention) if guild.owner else "Unknown") + embed.add_field(name="Vanity URL", value=guild.vanity_url_code or "None") + embed.add_field(name="Boosts", value=guild.premium_subscription_count) + embed.add_field(name="Text Channels", value=len(guild.text_channels)) + embed.add_field(name="Voice Channels", value=len(guild.voice_channels)) + embed.add_field(name="Forum Channels", value=len(guild.forums)) + embed.add_field(name="Emojis", value=f"{len(guild.emojis)}/{guild.emoji_limit}") + embed.add_field(name="Stickers", value=f"{len(guild.stickers)}/{guild.sticker_limit}") + embed.add_field(name="Roles", value=len(guild.roles)) + embed.add_field(name="Humans", value=sum(not member.bot for member in guild.members)) + embed.add_field(name="Bots", value=sum(member.bot for member in guild.members)) + embed.add_field(name="Bans", value=len([entry async for entry in guild.bans(limit=2000)])) + embed.set_footer(text=f"ID: {guild.id} | Created: {guild.created_at.strftime('%B %d, %Y')}") - embed.set_thumbnail(url=guild.icon) + await ctx.send(embed=embed) - await interaction.response.send_message(embed=embed) - - @info.command(name="tux", description="Shows information about Tux.") - async def tux(self, interaction: discord.Interaction) -> None: - """ - Show information about Tux. - - Parameters - ---------- - interaction : discord.Interaction - The discord interaction object. - """ - - embed = EmbedCreator.create_info_embed( - title="Tux", - description="Tux is a Discord bot written in Python using discord.py.", - interaction=interaction, - ) - embed.add_field( - name="GitHub", - value="[View the source code](https://github.com/allthingslinux/tux)", - ) - - await interaction.response.send_message(embed=embed) - - @info.command(name="member", description="Shows information about a member.") - async def member(self, interaction: discord.Interaction, member: discord.Member) -> None: + @info.command( + name="member", + aliases=["m", "user", "u"], + usage="info member [member]", + ) + async def member(self, ctx: commands.Context[commands.Bot], member: discord.Member) -> None: """ Show information about a member. Parameters ---------- - interaction : discord.Interaction - The discord interaction object. + ctx : commands.Context[commands.Bot] + The discord context object. member : discord.Member The member to get information about. """ @@ -86,15 +86,16 @@ class Info(commands.Cog): joined = discord.utils.format_dt(member.joined_at, "R") if member.joined_at else "Unknown" created = discord.utils.format_dt(member.created_at, "R") if member.created_at else "Unknown" roles = ", ".join(role.mention for role in member.roles[1:]) if member.roles[1:] else "No roles" - fetched_member = await self.bot.fetch_user(member.id) - embed = EmbedCreator.create_info_embed( + embed = discord.Embed( title=member.display_name, description="Here is some information about the member.", - interaction=interaction, + color=discord.Color.blurple(), ) + embed.set_thumbnail(url=member.display_avatar.url) + embed.set_image(url=fetched_member.banner) embed.add_field(name="Bot?", value=bot_status, inline=False) embed.add_field(name="Username", value=member.name, inline=False) embed.add_field(name="ID", value=str(member.id), inline=False) @@ -102,62 +103,67 @@ class Info(commands.Cog): embed.add_field(name="Registered", value=created, inline=False) embed.add_field(name="Roles", value=roles, inline=False) - embed.set_thumbnail(url=member.display_avatar.url) - embed.set_image(url=fetched_member.banner) + await ctx.send(embed=embed) - await interaction.response.send_message(embed=embed) - - @info.command(name="roles", description="Lists all roles in the server.") - async def roles(self, interaction: discord.Interaction) -> None: + @info.command( + name="roles", + aliases=["r"], + usage="info roles", + ) + async def roles(self, ctx: commands.Context[commands.Bot]) -> None: """ List all roles in the server. Parameters ---------- - interaction : discord.Interaction - The discord interaction object. + ctx : commands.Context[commands.Bot] + The discord context object. """ - if not interaction.guild: + if not ctx.guild: return - guild = interaction.guild + guild = ctx.guild roles = [role.mention for role in guild.roles] - embed = EmbedCreator.create_info_embed( + embed = discord.Embed( title="Server Roles", description=f"Role list for {guild.name}", - interaction=interaction, + color=discord.Color.blurple(), ) embed.add_field(name="Roles", value=", ".join(roles), inline=False) - await interaction.response.send_message(embed=embed) + await ctx.send(embed=embed) - @info.command(name="emotes", description="Lists all emotes in the server.") - async def emotes(self, interaction: discord.Interaction) -> None: + @info.command( + name="emotes", + aliases=["e"], + usage="info emotes", + ) + async def emotes(self, ctx: commands.Context[commands.Bot]) -> None: """ List all emotes in the server. Parameters ---------- - interaction : discord.Interaction - The discord interaction object. + ctx : commands.Context[commands.Bot] + The discord context object. """ - if not interaction.guild: + if not ctx.guild: return - guild = interaction.guild + guild = ctx.guild emotes = [str(emote) for emote in guild.emojis] - embed = EmbedCreator.create_info_embed( + embed = discord.Embed( title="Server Emotes", description=f"Emote list for {guild.name}", - interaction=interaction, + color=discord.Color.blurple(), ) embed.add_field(name="Emotes", value=" ".join(emotes) if emotes else "No emotes available", inline=False) - await interaction.response.send_message(embed=embed) + await ctx.send(embed=embed) async def setup(bot: commands.Bot) -> None: From 103374deba6621bb33a969f21be66bd4553b1dcf Mon Sep 17 00:00:00 2001 From: kzndotsh Date: Fri, 23 Aug 2024 07:14:38 +0000 Subject: [PATCH 62/84] chore(.pre-commit-config.yaml): update ruff-pre-commit version from v0.6.1 to v0.6.2 for latest features and bug fixes --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4d3da73..ac2deee 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -12,7 +12,7 @@ repos: - id: add-trailing-comma - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.6.1 + rev: v0.6.2 hooks: # Run the linter. - id: ruff From 985e2db177b9411847465daf9990ada6eddeb05a Mon Sep 17 00:00:00 2001 From: kzndotsh Date: Fri, 23 Aug 2024 09:49:31 +0000 Subject: [PATCH 63/84] fix(event.py): remove allowed_mentions parameter from channel.send method to allow mentions in the message --- tux/handlers/event.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tux/handlers/event.py b/tux/handlers/event.py index 5255538..26a5a70 100644 --- a/tux/handlers/event.py +++ b/tux/handlers/event.py @@ -95,7 +95,7 @@ class EventHandler(commands.Cog): # TODO: Add database configuration for primary support role support_role = "<@&1274823545087590533>" - await channel.send(content=support_role, embed=embed, allowed_mentions=discord.AllowedMentions.none()) + await channel.send(content=support_role, embed=embed) async def setup(bot: commands.Bot) -> None: From 51f2fc6186706ecdc265927dc468b5248dc4e3f7 Mon Sep 17 00:00:00 2001 From: kzndotsh Date: Fri, 23 Aug 2024 16:42:59 +0000 Subject: [PATCH 64/84] style(snippetban.py): add newline for better code readability style(snippetban.py): change "Snippet Banned" to "Snippet banned" for consistency in message case refactor(snippetban.py): simplify ban_count and unban_count calculation for better performance docs(snippetunban.py): add docstring to SnippetUnban class for better code documentation style(snippetunban.py): change "Snippet Unbanned" to "Snippet unbanned" for consistency in message case refactor(snippetunban.py): simplify ban_count and unban_count calculation for better performance feat(snippetunban.py): add setup function to add SnippetUnban cog to bot for enhanced functionality --- tux/cogs/moderation/snippetban.py | 7 ++++--- tux/cogs/moderation/snippetunban.py | 23 ++++++++++++++++++++--- 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/tux/cogs/moderation/snippetban.py b/tux/cogs/moderation/snippetban.py index bb57977..886c3ef 100644 --- a/tux/cogs/moderation/snippetban.py +++ b/tux/cogs/moderation/snippetban.py @@ -43,6 +43,7 @@ class SnippetBan(ModerationCogBase): flags : SnippetBanFlags The flags for the command. (reason: str, silent: bool) """ + if ctx.guild is None: logger.warning("Snippet ban command used outside of a guild context.") return @@ -59,7 +60,7 @@ class SnippetBan(ModerationCogBase): guild_id=ctx.guild.id, ) - await self.send_dm(ctx, flags.silent, target, flags.reason, "Snippet Banned") + await self.send_dm(ctx, flags.silent, target, flags.reason, "Snippet banned") await self.handle_case_response(ctx, case, "created", flags.reason, target) async def handle_case_response( @@ -107,8 +108,8 @@ class SnippetBan(ModerationCogBase): ban_cases = await self.case_controller.get_all_cases_by_type(guild_id, CaseType.SNIPPETBAN) unban_cases = await self.case_controller.get_all_cases_by_type(guild_id, CaseType.SNIPPETUNBAN) - ban_count = sum(1 for case in ban_cases if case.case_target_id == user_id) - unban_count = sum(1 for case in unban_cases if case.case_target_id == user_id) + ban_count = sum(case.case_target_id == user_id for case in ban_cases) + unban_count = sum(case.case_target_id == user_id for case in unban_cases) return ban_count > unban_count diff --git a/tux/cogs/moderation/snippetunban.py b/tux/cogs/moderation/snippetunban.py index 5c223fe..460388d 100644 --- a/tux/cogs/moderation/snippetunban.py +++ b/tux/cogs/moderation/snippetunban.py @@ -31,6 +31,19 @@ class SnippetUnban(ModerationCogBase): *, flags: SnippetUnbanFlags, ): + """ + Unban a user from creating snippets. + + Parameters + ---------- + ctx : commands.Context[commands.Bot] + The context object. + target : discord.Member + The member to snippet unban. + flags : SnippetUnbanFlags + The flags for the command. (reason: str, silent: bool) + """ + if ctx.guild is None: logger.warning("Snippet ban command used outside of a guild context.") return @@ -48,7 +61,7 @@ class SnippetUnban(ModerationCogBase): guild_id=ctx.guild.id, ) - await self.send_dm(ctx, flags.silent, target, flags.reason, "Snippet Unbanned") + await self.send_dm(ctx, flags.silent, target, flags.reason, "Snippet unbanned") await self.handle_case_response(ctx, case, "created", flags.reason, target) async def handle_case_response( @@ -96,7 +109,11 @@ class SnippetUnban(ModerationCogBase): ban_cases = await self.case_controller.get_all_cases_by_type(guild_id, CaseType.SNIPPETBAN) unban_cases = await self.case_controller.get_all_cases_by_type(guild_id, CaseType.SNIPPETUNBAN) - ban_count = sum(1 for case in ban_cases if case.case_target_id == user_id) - unban_count = sum(1 for case in unban_cases if case.case_target_id == user_id) + ban_count = sum(case.case_target_id == user_id for case in ban_cases) + unban_count = sum(case.case_target_id == user_id for case in unban_cases) return ban_count > unban_count + + +async def setup(bot: commands.Bot) -> None: + await bot.add_cog(SnippetUnban(bot)) From 1035a69494f57a19e4a997cdfd21b498f7796324 Mon Sep 17 00:00:00 2001 From: kzndotsh Date: Fri, 23 Aug 2024 16:44:49 +0000 Subject: [PATCH 65/84] docs(snippetban.py, snippetunban.py): add docstrings to is_snippetbanned method to provide clear explanation of its functionality and parameters --- tux/cogs/moderation/snippetban.py | 16 ++++++++++++++++ tux/cogs/moderation/snippetunban.py | 16 ++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/tux/cogs/moderation/snippetban.py b/tux/cogs/moderation/snippetban.py index 886c3ef..2555f4f 100644 --- a/tux/cogs/moderation/snippetban.py +++ b/tux/cogs/moderation/snippetban.py @@ -105,6 +105,22 @@ class SnippetBan(ModerationCogBase): await ctx.send(embed=embed, delete_after=30, ephemeral=True) async def is_snippetbanned(self, guild_id: int, user_id: int) -> bool: + """ + Check if a user is snippet banned. + + Parameters + ---------- + guild_id : int + The ID of the guild to check in. + user_id : int + The ID of the user to check. + + Returns + ------- + bool + True if the user is snippet banned, False otherwise. + """ + ban_cases = await self.case_controller.get_all_cases_by_type(guild_id, CaseType.SNIPPETBAN) unban_cases = await self.case_controller.get_all_cases_by_type(guild_id, CaseType.SNIPPETUNBAN) diff --git a/tux/cogs/moderation/snippetunban.py b/tux/cogs/moderation/snippetunban.py index 460388d..d44ee50 100644 --- a/tux/cogs/moderation/snippetunban.py +++ b/tux/cogs/moderation/snippetunban.py @@ -106,6 +106,22 @@ class SnippetUnban(ModerationCogBase): await ctx.send(embed=embed, delete_after=30, ephemeral=True) async def is_snippetbanned(self, guild_id: int, user_id: int) -> bool: + """ + Check if a user is snippet banned. + + Parameters + ---------- + guild_id : int + The ID of the guild to check in. + user_id : int + The ID of the user to check. + + Returns + ------- + bool + True if the user is snippet banned, False otherwise. + """ + ban_cases = await self.case_controller.get_all_cases_by_type(guild_id, CaseType.SNIPPETBAN) unban_cases = await self.case_controller.get_all_cases_by_type(guild_id, CaseType.SNIPPETUNBAN) From 66ec2bbb719796a7c77c407688456091775c9c81 Mon Sep 17 00:00:00 2001 From: kzndotsh Date: Sat, 24 Aug 2024 05:37:00 +0000 Subject: [PATCH 66/84] feat(pyproject.toml): add pytz library to manage timezone related operations --- poetry.lock | 2 +- pyproject.toml | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/poetry.lock b/poetry.lock index e6833db..3a85505 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2406,4 +2406,4 @@ multidict = ">=4.0" [metadata] lock-version = "2.0" python-versions = ">=3.12,<4" -content-hash = "8b92b052826adf9c28d1314fc6058b2f6f24858025598aef6621c0e20441bd1e" +content-hash = "e773f58b7548a102f8e9ebc152ec2390d1149a549fe9f77e9c248b37e896f56a" diff --git a/pyproject.toml b/pyproject.toml index d55ec03..98a5778 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,6 +36,7 @@ types-aiofiles = "^24.1.0.20240626" types-psutil = "^6.0.0.20240621" typing-extensions = "^4.12.2" jishaku = "^2.5.2" +pytz = "^2024.1" [tool.poetry.group.docs.dependencies] mkdocs-material = "^9.5.30" From 94abe651d280f734352421313bcf7eb8183a1838 Mon Sep 17 00:00:00 2001 From: kzndotsh Date: Sat, 24 Aug 2024 01:37:20 -0400 Subject: [PATCH 67/84] refactor: Remove unused timezones.json file --- tux/utils/data/countries.json | 4423 --------------------------------- tux/utils/data/timezones.json | 122 - 2 files changed, 4545 deletions(-) delete mode 100644 tux/utils/data/countries.json delete mode 100644 tux/utils/data/timezones.json diff --git a/tux/utils/data/countries.json b/tux/utils/data/countries.json deleted file mode 100644 index 05a645f..0000000 --- a/tux/utils/data/countries.json +++ /dev/null @@ -1,4423 +0,0 @@ -[ - { - "name": "Afghanistan", - "unicode": "U+1F1E6 U+1F1EB", - "emoji": "🇦🇫", - "alpha2": "AF", - "dialCode": "93", - "alpha3": "AFG", - "region": "Asia", - "capital": "Kabul", - "geo": { - "lat": 33, - "long": 33 - }, - "timezones": [ - "Asia/Kabul" - ] - }, - { - "name": "Albania", - "unicode": "U+1F1E6 U+1F1F1", - "emoji": "🇦🇱", - "alpha2": "AL", - "dialCode": "355", - "alpha3": "ALB", - "region": "Europe", - "capital": "Tirana", - "geo": { - "lat": 41, - "long": 41 - }, - "timezones": [ - "Europe/Tirane" - ] - }, - { - "name": "Algeria", - "unicode": "U+1F1E9 U+1F1FF", - "emoji": "🇩🇿", - "alpha2": "DZ", - "dialCode": "213", - "alpha3": "DZA", - "region": "Africa", - "capital": "Algiers", - "geo": { - "lat": 28, - "long": 28 - }, - "timezones": [ - "Africa/Algiers" - ] - }, - { - "name": "American Samoa", - "unicode": "U+1F1E6 U+1F1F8", - "emoji": "🇦🇸", - "alpha2": "AS", - "dialCode": "1 684", - "alpha3": "ASM", - "region": "Oceania", - "capital": "Pago Pago", - "geo": { - "lat": -14.33333333, - "long": -14.33333333 - }, - "timezones": [ - "Pacific/Pago_Pago" - ] - }, - { - "name": "Andorra", - "unicode": "U+1F1E6 U+1F1E9", - "emoji": "🇦🇩", - "alpha2": "AD", - "dialCode": "376", - "alpha3": "AND", - "region": "Europe", - "capital": "Andorra la Vella", - "geo": { - "lat": 42.5, - "long": 42.5 - }, - "timezones": [ - "Europe/Andorra" - ] - }, - { - "name": "Angola", - "unicode": "U+1F1E6 U+1F1F4", - "emoji": "🇦🇴", - "alpha2": "AO", - "dialCode": "244", - "alpha3": "AGO", - "region": "Africa", - "capital": "Luanda", - "geo": { - "lat": -12.5, - "long": -12.5 - }, - "timezones": [ - "Africa/Luanda" - ] - }, - { - "name": "Anguilla", - "unicode": "U+1F1E6 U+1F1EE", - "emoji": "🇦🇮", - "alpha2": "AI", - "dialCode": "1 264", - "alpha3": "AIA", - "region": "Americas", - "capital": "The Valley", - "geo": { - "lat": 18.25, - "long": 18.25 - }, - "timezones": [ - "America/Anguilla" - ] - }, - { - "name": "Antarctica", - "unicode": "U+1F1E6 U+1F1F6", - "emoji": "🇦🇶", - "alpha2": "AQ", - "dialCode": "", - "alpha3": "ATA", - "region": "", - "capital": null, - "geo": { - "lat": -90, - "long": -90 - }, - "timezones": [ - "Antarctica/McMurdo", - "Antarctica/Casey", - "Antarctica/Davis", - "Antarctica/DumontDUrville", - "Antarctica/Mawson", - "Antarctica/Palmer", - "Antarctica/Rothera", - "Antarctica/Syowa", - "Antarctica/Troll", - "Antarctica/Vostok" - ] - }, - { - "name": "Antigua and Barbuda", - "unicode": "U+1F1E6 U+1F1EC", - "emoji": "🇦🇬", - "alpha2": "AG", - "dialCode": "1268", - "alpha3": "ATG", - "region": "Americas", - "capital": "Saint John's", - "geo": { - "lat": 17.05, - "long": 17.05 - }, - "timezones": [ - "America/Antigua" - ] - }, - { - "name": "Argentina", - "unicode": "U+1F1E6 U+1F1F7", - "emoji": "🇦🇷", - "alpha2": "AR", - "dialCode": "54", - "alpha3": "ARG", - "region": "Americas", - "capital": "Buenos Aires", - "geo": { - "lat": -34, - "long": -34 - }, - "timezones": [ - "America/Argentina/Buenos_Aires", - "America/Argentina/Cordoba", - "America/Argentina/Salta", - "America/Argentina/Jujuy", - "America/Argentina/Tucuman", - "America/Argentina/Catamarca", - "America/Argentina/La_Rioja", - "America/Argentina/San_Juan", - "America/Argentina/Mendoza", - "America/Argentina/San_Luis", - "America/Argentina/Rio_Gallegos", - "America/Argentina/Ushuaia" - ] - }, - { - "name": "Armenia", - "unicode": "U+1F1E6 U+1F1F2", - "emoji": "🇦🇲", - "alpha2": "AM", - "dialCode": "374", - "alpha3": "ARM", - "region": "Asia", - "capital": "Yerevan", - "geo": { - "lat": 40, - "long": 40 - }, - "timezones": [ - "Asia/Yerevan" - ] - }, - { - "name": "Aruba", - "unicode": "U+1F1E6 U+1F1FC", - "emoji": "🇦🇼", - "alpha2": "AW", - "dialCode": "297", - "alpha3": "ABW", - "region": "Americas", - "capital": "Oranjestad", - "geo": { - "lat": 12.5, - "long": 12.5 - }, - "timezones": [ - "America/Aruba" - ] - }, - { - "name": "Australia", - "unicode": "U+1F1E6 U+1F1FA", - "emoji": "🇦🇺", - "alpha2": "AU", - "dialCode": "61", - "alpha3": "AUS", - "region": "Oceania", - "capital": "Canberra", - "geo": { - "lat": -27, - "long": -27 - }, - "timezones": [ - "Australia/Lord_Howe", - "Antarctica/Macquarie", - "Australia/Hobart", - "Australia/Currie", - "Australia/Melbourne", - "Australia/Sydney", - "Australia/Broken_Hill", - "Australia/Brisbane", - "Australia/Lindeman", - "Australia/Adelaide", - "Australia/Darwin", - "Australia/Perth", - "Australia/Eucla" - ] - }, - { - "name": "Austria", - "unicode": "U+1F1E6 U+1F1F9", - "emoji": "🇦🇹", - "alpha2": "AT", - "dialCode": "43", - "alpha3": "AUT", - "region": "Europe", - "capital": "Vienna", - "geo": { - "lat": 47.33333333, - "long": 47.33333333 - }, - "timezones": [ - "Europe/Vienna" - ] - }, - { - "name": "Azerbaijan", - "unicode": "U+1F1E6 U+1F1FF", - "emoji": "🇦🇿", - "alpha2": "AZ", - "dialCode": "994", - "alpha3": "AZE", - "region": "Asia", - "capital": "Baku", - "geo": { - "lat": 40.5, - "long": 40.5 - }, - "timezones": [ - "Asia/Baku" - ] - }, - { - "name": "Bahamas", - "unicode": "U+1F1E7 U+1F1F8", - "emoji": "🇧🇸", - "alpha2": "BS", - "dialCode": "1 242", - "alpha3": "BHS", - "region": "Americas", - "capital": "Nassau", - "geo": { - "lat": 24.25, - "long": 24.25 - }, - "timezones": [ - "America/Nassau" - ] - }, - { - "name": "Bahrain", - "unicode": "U+1F1E7 U+1F1ED", - "emoji": "🇧🇭", - "alpha2": "BH", - "dialCode": "973", - "alpha3": "BHR", - "region": "Asia", - "capital": "Manama", - "geo": { - "lat": 26, - "long": 26 - }, - "timezones": [ - "Asia/Bahrain" - ] - }, - { - "name": "Bangladesh", - "unicode": "U+1F1E7 U+1F1E9", - "emoji": "🇧🇩", - "alpha2": "BD", - "dialCode": "880", - "alpha3": "BGD", - "region": "Asia", - "capital": "Dhaka", - "geo": { - "lat": 24, - "long": 24 - }, - "timezones": [ - "Asia/Dhaka" - ] - }, - { - "name": "Barbados", - "unicode": "U+1F1E7 U+1F1E7", - "emoji": "🇧🇧", - "alpha2": "BB", - "dialCode": "1 246", - "alpha3": "BRB", - "region": "Americas", - "capital": "Bridgetown", - "geo": { - "lat": 13.16666666, - "long": 13.16666666 - }, - "timezones": [ - "America/Barbados" - ] - }, - { - "name": "Belarus", - "unicode": "U+1F1E7 U+1F1FE", - "emoji": "🇧🇾", - "alpha2": "BY", - "dialCode": "375", - "alpha3": "BLR", - "region": "Europe", - "capital": "Minsk", - "geo": { - "lat": 53, - "long": 53 - }, - "timezones": [ - "Europe/Minsk" - ] - }, - { - "name": "Belgium", - "unicode": "U+1F1E7 U+1F1EA", - "emoji": "🇧🇪", - "alpha2": "BE", - "dialCode": "32", - "alpha3": "BEL", - "region": "Europe", - "capital": "Brussels", - "geo": { - "lat": 50.83333333, - "long": 50.83333333 - }, - "timezones": [ - "Europe/Brussels" - ] - }, - { - "name": "Belize", - "unicode": "U+1F1E7 U+1F1FF", - "emoji": "🇧🇿", - "alpha2": "BZ", - "dialCode": "501", - "alpha3": "BLZ", - "region": "Americas", - "capital": "Belmopan", - "geo": { - "lat": 17.25, - "long": 17.25 - }, - "timezones": [ - "America/Belize" - ] - }, - { - "name": "Benin", - "unicode": "U+1F1E7 U+1F1EF", - "emoji": "🇧🇯", - "alpha2": "BJ", - "dialCode": "229", - "alpha3": "BEN", - "region": "Africa", - "capital": "Porto-Novo", - "geo": { - "lat": 9.5, - "long": 9.5 - }, - "timezones": [ - "Africa/Porto-Novo" - ] - }, - { - "name": "Bermuda", - "unicode": "U+1F1E7 U+1F1F2", - "emoji": "🇧🇲", - "alpha2": "BM", - "dialCode": "1 441", - "alpha3": "BMU", - "region": "Americas", - "capital": "Hamilton", - "geo": { - "lat": 32.33333333, - "long": 32.33333333 - }, - "timezones": [ - "Atlantic/Bermuda" - ] - }, - { - "name": "Bhutan", - "unicode": "U+1F1E7 U+1F1F9", - "emoji": "🇧🇹", - "alpha2": "BT", - "dialCode": "975", - "alpha3": "BTN", - "region": "Asia", - "capital": "Thimphu", - "geo": { - "lat": 27.5, - "long": 27.5 - }, - "timezones": [ - "Asia/Thimphu" - ] - }, - { - "name": "Bolivia", - "unicode": "U+1F1E7 U+1F1F4", - "emoji": "🇧🇴", - "alpha2": "BO", - "dialCode": "591", - "alpha3": "BOL", - "region": "Americas", - "capital": "Sucre", - "geo": { - "lat": -17, - "long": -17 - }, - "timezones": [ - "America/La_Paz" - ] - }, - { - "name": "Bonaire, Sint Eustatius and Saba", - "unicode": "U+1F1E7 U+1F1F6", - "emoji": "🇧🇶", - "alpha2": "BQ", - "dialCode": "", - "alpha3": "BES", - "region": "Americas", - "geo": {}, - "capital": "", - "timezones": [] - }, - { - "name": "Bosnia and Herzegovina", - "unicode": "U+1F1E7 U+1F1E6", - "emoji": "🇧🇦", - "alpha2": "BA", - "dialCode": "387", - "alpha3": "BIH", - "region": "Europe", - "capital": "Sarajevo", - "geo": { - "lat": 44, - "long": 44 - }, - "timezones": [ - "Europe/Sarajevo" - ] - }, - { - "name": "Botswana", - "unicode": "U+1F1E7 U+1F1FC", - "emoji": "🇧🇼", - "alpha2": "BW", - "dialCode": "267", - "alpha3": "BWA", - "region": "Africa", - "capital": "Gaborone", - "geo": { - "lat": -22, - "long": -22 - }, - "timezones": [ - "Africa/Gaborone" - ] - }, - { - "name": "Bouvet Island", - "unicode": "U+1F1E7 U+1F1FB", - "emoji": "🇧🇻", - "alpha2": "BV", - "dialCode": "", - "alpha3": "BVT", - "region": "Americas", - "capital": null, - "geo": { - "lat": -54.43333333, - "long": -54.43333333 - }, - "timezones": [ - "Europe/Oslo" - ] - }, - { - "name": "Brazil", - "unicode": "U+1F1E7 U+1F1F7", - "emoji": "🇧🇷", - "alpha2": "BR", - "dialCode": "55", - "alpha3": "BRA", - "region": "Americas", - "capital": "Brasília", - "geo": { - "lat": -10, - "long": -10 - }, - "timezones": [ - "America/Noronha", - "America/Belem", - "America/Fortaleza", - "America/Recife", - "America/Araguaina", - "America/Maceio", - "America/Bahia", - "America/Sao_Paulo", - "America/Campo_Grande", - "America/Cuiaba", - "America/Santarem", - "America/Porto_Velho", - "America/Boa_Vista", - "America/Manaus", - "America/Eirunepe", - "America/Rio_Branco" - ] - }, - { - "name": "British Indian Ocean Territory", - "unicode": "U+1F1EE U+1F1F4", - "emoji": "🇮🇴", - "alpha2": "IO", - "dialCode": "246", - "alpha3": "IOT", - "region": "Africa", - "capital": "Diego Garcia", - "geo": { - "lat": -6, - "long": -6 - }, - "timezones": [ - "Indian/Chagos" - ] - }, - { - "name": "Brunei Darussalam", - "unicode": "U+1F1E7 U+1F1F3", - "emoji": "🇧🇳", - "alpha2": "BN", - "dialCode": "673", - "alpha3": "BRN", - "region": "Asia", - "capital": "Bandar Seri Begawan", - "geo": { - "lat": 4.5, - "long": 4.5 - }, - "timezones": [ - "Asia/Brunei" - ] - }, - { - "name": "Bulgaria", - "unicode": "U+1F1E7 U+1F1EC", - "emoji": "🇧🇬", - "alpha2": "BG", - "dialCode": "359", - "alpha3": "BGR", - "region": "Europe", - "capital": "Sofia", - "geo": { - "lat": 43, - "long": 43 - }, - "timezones": [ - "Europe/Sofia" - ] - }, - { - "name": "Burkina Faso", - "unicode": "U+1F1E7 U+1F1EB", - "emoji": "🇧🇫", - "alpha2": "BF", - "dialCode": "226", - "alpha3": "BFA", - "region": "Africa", - "capital": "Ouagadougou", - "geo": { - "lat": 13, - "long": 13 - }, - "timezones": [ - "Africa/Ouagadougou" - ] - }, - { - "name": "Burundi", - "unicode": "U+1F1E7 U+1F1EE", - "emoji": "🇧🇮", - "alpha2": "BI", - "dialCode": "257", - "alpha3": "BDI", - "region": "Africa", - "capital": "Bujumbura", - "geo": { - "lat": -3.5, - "long": -3.5 - }, - "timezones": [ - "Africa/Bujumbura" - ] - }, - { - "name": "Cambodia", - "unicode": "U+1F1F0 U+1F1ED", - "emoji": "🇰🇭", - "alpha2": "KH", - "dialCode": "855", - "alpha3": "KHM", - "region": "Asia", - "capital": "Phnom Penh", - "geo": { - "lat": 13, - "long": 13 - }, - "timezones": [ - "Asia/Phnom_Penh" - ] - }, - { - "name": "Cameroon", - "unicode": "U+1F1E8 U+1F1F2", - "emoji": "🇨🇲", - "alpha2": "CM", - "dialCode": "237", - "alpha3": "CMR", - "region": "Africa", - "capital": "Yaoundé", - "geo": { - "lat": 6, - "long": 6 - }, - "timezones": [ - "Africa/Douala" - ] - }, - { - "name": "Canada", - "unicode": "U+1F1E8 U+1F1E6", - "emoji": "🇨🇦", - "alpha2": "CA", - "dialCode": "1", - "alpha3": "CAN", - "region": "Americas", - "capital": "Ottawa", - "geo": { - "lat": 60, - "long": 60 - }, - "timezones": [ - "America/St_Johns", - "America/Halifax", - "America/Glace_Bay", - "America/Moncton", - "America/Goose_Bay", - "America/Blanc-Sablon", - "America/Toronto", - "America/Nipigon", - "America/Thunder_Bay", - "America/Iqaluit", - "America/Pangnirtung", - "America/Atikokan", - "America/Winnipeg", - "America/Rainy_River", - "America/Resolute", - "America/Rankin_Inlet", - "America/Regina", - "America/Swift_Current", - "America/Edmonton", - "America/Cambridge_Bay", - "America/Yellowknife", - "America/Inuvik", - "America/Creston", - "America/Dawson_Creek", - "America/Fort_Nelson", - "America/Vancouver", - "America/Whitehorse", - "America/Dawson" - ] - }, - { - "name": "Cape Verde", - "unicode": "U+1F1E8 U+1F1FB", - "emoji": "🇨🇻", - "alpha2": "CV", - "dialCode": "238", - "alpha3": "CPV", - "region": "Africa", - "capital": "Praia", - "geo": { - "lat": 16, - "long": 16 - }, - "timezones": [ - "Atlantic/Cape_Verde" - ] - }, - { - "name": "Cayman Islands", - "unicode": "U+1F1F0 U+1F1FE", - "emoji": "🇰🇾", - "alpha2": "KY", - "dialCode": " 345", - "alpha3": "CYM", - "region": "Americas", - "capital": "George Town", - "geo": { - "lat": 19.5, - "long": 19.5 - }, - "timezones": [ - "America/Cayman" - ] - }, - { - "name": "Central African Republic", - "unicode": "U+1F1E8 U+1F1EB", - "emoji": "🇨🇫", - "alpha2": "CF", - "dialCode": "236", - "alpha3": "CAF", - "region": "Africa", - "capital": "Bangui", - "geo": { - "lat": 7, - "long": 7 - }, - "timezones": [ - "Africa/Bangui" - ] - }, - { - "name": "Chad", - "unicode": "U+1F1F9 U+1F1E9", - "emoji": "🇹🇩", - "alpha2": "TD", - "dialCode": "235", - "alpha3": "TCD", - "region": "Africa", - "capital": "N'Djamena", - "geo": { - "lat": 15, - "long": 15 - }, - "timezones": [ - "Africa/Ndjamena" - ] - }, - { - "name": "Chile", - "unicode": "U+1F1E8 U+1F1F1", - "emoji": "🇨🇱", - "alpha2": "CL", - "dialCode": "56", - "alpha3": "CHL", - "region": "Americas", - "capital": "Santiago", - "geo": { - "lat": -30, - "long": -30 - }, - "timezones": [ - "America/Santiago", - "Pacific/Easter" - ] - }, - { - "name": "China", - "unicode": "U+1F1E8 U+1F1F3", - "emoji": "🇨🇳", - "alpha2": "CN", - "dialCode": "86", - "alpha3": "CHN", - "region": "Asia", - "capital": "Beijing", - "geo": { - "lat": 35, - "long": 35 - }, - "timezones": [ - "Asia/Shanghai", - "Asia/Urumqi" - ] - }, - { - "name": "Christmas Island", - "unicode": "U+1F1E8 U+1F1FD", - "emoji": "🇨🇽", - "alpha2": "CX", - "dialCode": "61", - "alpha3": "CXR", - "region": "Oceania", - "capital": "Flying Fish Cove", - "geo": { - "lat": -10.5, - "long": -10.5 - }, - "timezones": [ - "Indian/Christmas" - ] - }, - { - "name": "Cocos (Keeling) Islands", - "unicode": "U+1F1E8 U+1F1E8", - "emoji": "🇨🇨", - "alpha2": "CC", - "dialCode": "61", - "alpha3": "CCK", - "region": "Oceania", - "capital": "West Island", - "geo": { - "lat": -12.5, - "long": -12.5 - }, - "timezones": [ - "Indian/Cocos" - ] - }, - { - "name": "Colombia", - "unicode": "U+1F1E8 U+1F1F4", - "emoji": "🇨🇴", - "alpha2": "CO", - "dialCode": "57", - "alpha3": "COL", - "region": "Americas", - "capital": "Bogotá", - "geo": { - "lat": 4, - "long": 4 - }, - "timezones": [ - "America/Bogota" - ] - }, - { - "name": "Comoros", - "unicode": "U+1F1F0 U+1F1F2", - "emoji": "🇰🇲", - "alpha2": "KM", - "dialCode": "269", - "alpha3": "COM", - "region": "Africa", - "capital": "Moroni", - "geo": { - "lat": -12.16666666, - "long": -12.16666666 - }, - "timezones": [ - "Indian/Comoro" - ] - }, - { - "name": "Congo", - "unicode": "U+1F1E8 U+1F1E9", - "emoji": "🇨🇩", - "alpha2": "CD", - "dialCode": "243", - "alpha3": "COD", - "region": "Africa", - "capital": "Kinshasa", - "geo": { - "lat": 0, - "long": 0 - }, - "timezones": [ - "Africa/Kinshasa", - "Africa/Lubumbashi" - ] - }, - { - "name": "Congo", - "unicode": "U+1F1E8 U+1F1EC", - "emoji": "🇨🇬", - "alpha2": "CG", - "dialCode": "242", - "alpha3": "COG", - "region": "Africa", - "capital": "Brazzaville", - "geo": { - "lat": -1, - "long": -1 - }, - "timezones": [ - "Africa/Brazzaville" - ] - }, - { - "name": "Cook Islands", - "unicode": "U+1F1E8 U+1F1F0", - "emoji": "🇨🇰", - "alpha2": "CK", - "dialCode": "682", - "alpha3": "COK", - "region": "Oceania", - "capital": "Avarua", - "geo": { - "lat": -21.23333333, - "long": -21.23333333 - }, - "timezones": [ - "Pacific/Rarotonga" - ] - }, - { - "name": "Costa Rica", - "unicode": "U+1F1E8 U+1F1F7", - "emoji": "🇨🇷", - "alpha2": "CR", - "dialCode": "506", - "alpha3": "CRI", - "region": "Americas", - "capital": "San José", - "geo": { - "lat": 10, - "long": 10 - }, - "timezones": [ - "America/Costa_Rica" - ] - }, - { - "name": "Croatia", - "unicode": "U+1F1ED U+1F1F7", - "emoji": "🇭🇷", - "alpha2": "HR", - "dialCode": "385", - "alpha3": "HRV", - "region": "Europe", - "capital": "Zagreb", - "geo": { - "lat": 45.16666666, - "long": 45.16666666 - }, - "timezones": [ - "Europe/Zagreb" - ] - }, - { - "name": "Cuba", - "unicode": "U+1F1E8 U+1F1FA", - "emoji": "🇨🇺", - "alpha2": "CU", - "dialCode": "53", - "alpha3": "CUB", - "region": "Americas", - "capital": "Havana", - "geo": { - "lat": 21.5, - "long": 21.5 - }, - "timezones": [ - "America/Havana" - ] - }, - { - "name": "Curaçao", - "unicode": "U+1F1E8 U+1F1FC", - "emoji": "🇨🇼", - "alpha2": "CW", - "dialCode": "", - "alpha3": "CUW", - "region": "Americas", - "capital": "Willemstad", - "geo": { - "lat": 12.116667, - "long": 12.116667 - }, - "timezones": [ - "America/Curacao" - ] - }, - { - "name": "Cyprus", - "unicode": "U+1F1E8 U+1F1FE", - "emoji": "🇨🇾", - "alpha2": "CY", - "dialCode": "537", - "alpha3": "CYP", - "region": "Asia", - "capital": "Nicosia", - "geo": { - "lat": 35, - "long": 35 - }, - "timezones": [ - "Asia/Nicosia" - ] - }, - { - "name": "Czech Republic", - "unicode": "U+1F1E8 U+1F1FF", - "emoji": "🇨🇿", - "alpha2": "CZ", - "dialCode": "420", - "alpha3": "CZE", - "region": "Europe", - "capital": "Prague", - "geo": { - "lat": 49.75, - "long": 49.75 - }, - "timezones": [ - "Europe/Prague" - ] - }, - { - "name": "Côte D'Ivoire", - "unicode": "U+1F1E8 U+1F1EE", - "emoji": "🇨🇮", - "alpha2": "CI", - "dialCode": "225", - "alpha3": "CIV", - "region": "Africa", - "capital": "Yamoussoukro", - "geo": { - "lat": 8, - "long": 8 - }, - "timezones": [ - "Africa/Abidjan" - ] - }, - { - "name": "Denmark", - "unicode": "U+1F1E9 U+1F1F0", - "emoji": "🇩🇰", - "alpha2": "DK", - "dialCode": "45", - "alpha3": "DNK", - "region": "Europe", - "capital": "Copenhagen", - "geo": { - "lat": 56, - "long": 56 - }, - "timezones": [ - "Europe/Copenhagen" - ] - }, - { - "name": "Djibouti", - "unicode": "U+1F1E9 U+1F1EF", - "emoji": "🇩🇯", - "alpha2": "DJ", - "dialCode": "253", - "alpha3": "DJI", - "region": "Africa", - "capital": "Djibouti", - "geo": { - "lat": 11.5, - "long": 11.5 - }, - "timezones": [ - "Africa/Djibouti" - ] - }, - { - "name": "Dominica", - "unicode": "U+1F1E9 U+1F1F2", - "emoji": "🇩🇲", - "alpha2": "DM", - "dialCode": "1 767", - "alpha3": "DMA", - "region": "Americas", - "capital": "Roseau", - "geo": { - "lat": 15.41666666, - "long": 15.41666666 - }, - "timezones": [ - "America/Dominica" - ] - }, - { - "name": "Dominican Republic", - "unicode": "U+1F1E9 U+1F1F4", - "emoji": "🇩🇴", - "alpha2": "DO", - "dialCode": "1 849", - "alpha3": "DOM", - "region": "Americas", - "capital": "Santo Domingo", - "geo": { - "lat": 19, - "long": 19 - }, - "timezones": [ - "America/Santo_Domingo" - ] - }, - { - "name": "Ecuador", - "unicode": "U+1F1EA U+1F1E8", - "emoji": "🇪🇨", - "alpha2": "EC", - "dialCode": "593", - "alpha3": "ECU", - "region": "Americas", - "capital": "Quito", - "geo": { - "lat": -2, - "long": -2 - }, - "timezones": [ - "America/Guayaquil", - "Pacific/Galapagos" - ] - }, - { - "name": "Egypt", - "unicode": "U+1F1EA U+1F1EC", - "emoji": "🇪🇬", - "alpha2": "EG", - "dialCode": "20", - "alpha3": "EGY", - "region": "Africa", - "capital": "Cairo", - "geo": { - "lat": 27, - "long": 27 - }, - "timezones": [ - "Africa/Cairo" - ] - }, - { - "name": "El Salvador", - "unicode": "U+1F1F8 U+1F1FB", - "emoji": "🇸🇻", - "alpha2": "SV", - "dialCode": "503", - "alpha3": "SLV", - "region": "Americas", - "capital": "San Salvador", - "geo": { - "lat": 13.83333333, - "long": 13.83333333 - }, - "timezones": [ - "America/El_Salvador" - ] - }, - { - "name": "Equatorial Guinea", - "unicode": "U+1F1EC U+1F1F6", - "emoji": "🇬🇶", - "alpha2": "GQ", - "dialCode": "240", - "alpha3": "GNQ", - "region": "Africa", - "capital": "Malabo", - "geo": { - "lat": 2, - "long": 2 - }, - "timezones": [ - "Africa/Malabo" - ] - }, - { - "name": "Eritrea", - "unicode": "U+1F1EA U+1F1F7", - "emoji": "🇪🇷", - "alpha2": "ER", - "dialCode": "291", - "alpha3": "ERI", - "region": "Africa", - "capital": "Asmara", - "geo": { - "lat": 15, - "long": 15 - }, - "timezones": [ - "Africa/Asmara" - ] - }, - { - "name": "Estonia", - "unicode": "U+1F1EA U+1F1EA", - "emoji": "🇪🇪", - "alpha2": "EE", - "dialCode": "372", - "alpha3": "EST", - "region": "Europe", - "capital": "Tallinn", - "geo": { - "lat": 59, - "long": 59 - }, - "timezones": [ - "Europe/Tallinn" - ] - }, - { - "name": "Ethiopia", - "unicode": "U+1F1EA U+1F1F9", - "emoji": "🇪🇹", - "alpha2": "ET", - "dialCode": "251", - "alpha3": "ETH", - "region": "Africa", - "capital": "Addis Ababa", - "geo": { - "lat": 8, - "long": 8 - }, - "timezones": [ - "Africa/Addis_Ababa" - ] - }, - { - "name": "European Union", - "unicode": "U+1F1EA U+1F1FA", - "emoji": "🇪🇺", - "alpha2": "EU", - "dialCode": "", - "alpha3": "", - "region": "", - "geo": {}, - "capital": "", - "timezones": [] - }, - { - "name": "Falkland Islands (Malvinas)", - "unicode": "U+1F1EB U+1F1F0", - "emoji": "🇫🇰", - "alpha2": "FK", - "dialCode": "500", - "alpha3": "FLK", - "region": "Americas", - "capital": "Stanley", - "geo": { - "lat": -51.75, - "long": -51.75 - }, - "timezones": [ - "Atlantic/Stanley" - ] - }, - { - "name": "Faroe Islands", - "unicode": "U+1F1EB U+1F1F4", - "emoji": "🇫🇴", - "alpha2": "FO", - "dialCode": "298", - "alpha3": "FRO", - "region": "Europe", - "capital": "Tórshavn", - "geo": { - "lat": 62, - "long": 62 - }, - "timezones": [ - "Atlantic/Faroe" - ] - }, - { - "name": "Fiji", - "unicode": "U+1F1EB U+1F1EF", - "emoji": "🇫🇯", - "alpha2": "FJ", - "dialCode": "679", - "alpha3": "FJI", - "region": "Oceania", - "capital": "Suva", - "geo": { - "lat": -18, - "long": -18 - }, - "timezones": [ - "Pacific/Fiji" - ] - }, - { - "name": "Finland", - "unicode": "U+1F1EB U+1F1EE", - "emoji": "🇫🇮", - "alpha2": "FI", - "dialCode": "358", - "alpha3": "FIN", - "region": "Europe", - "capital": "Helsinki", - "geo": { - "lat": 64, - "long": 64 - }, - "timezones": [ - "Europe/Helsinki" - ] - }, - { - "name": "France", - "unicode": "U+1F1EB U+1F1F7", - "emoji": "🇫🇷", - "alpha2": "FR", - "dialCode": "33", - "alpha3": "FRA", - "region": "Europe", - "capital": "Paris", - "geo": { - "lat": 46, - "long": 46 - }, - "timezones": [ - "Europe/Paris" - ] - }, - { - "name": "French Guiana", - "unicode": "U+1F1EC U+1F1EB", - "emoji": "🇬🇫", - "alpha2": "GF", - "dialCode": "594", - "alpha3": "GUF", - "region": "Americas", - "capital": "Cayenne", - "geo": { - "lat": 4, - "long": 4 - }, - "timezones": [ - "America/Cayenne" - ] - }, - { - "name": "French Polynesia", - "unicode": "U+1F1F5 U+1F1EB", - "emoji": "🇵🇫", - "alpha2": "PF", - "dialCode": "689", - "alpha3": "PYF", - "region": "Oceania", - "capital": "Papeetē", - "geo": { - "lat": -15, - "long": -15 - }, - "timezones": [ - "Pacific/Tahiti", - "Pacific/Marquesas", - "Pacific/Gambier" - ] - }, - { - "name": "French Southern Territories", - "unicode": "U+1F1F9 U+1F1EB", - "emoji": "🇹🇫", - "alpha2": "TF", - "dialCode": "", - "alpha3": "ATF", - "region": "Africa", - "capital": "Port-aux-Français", - "geo": { - "lat": -49.25, - "long": -49.25 - }, - "timezones": [ - "Indian/Kerguelen" - ] - }, - { - "name": "Gabon", - "unicode": "U+1F1EC U+1F1E6", - "emoji": "🇬🇦", - "alpha2": "GA", - "dialCode": "241", - "alpha3": "GAB", - "region": "Africa", - "capital": "Libreville", - "geo": { - "lat": -1, - "long": -1 - }, - "timezones": [ - "Africa/Libreville" - ] - }, - { - "name": "Gambia", - "unicode": "U+1F1EC U+1F1F2", - "emoji": "🇬🇲", - "alpha2": "GM", - "dialCode": "220", - "alpha3": "GMB", - "region": "Africa", - "capital": "Banjul", - "geo": { - "lat": 13.46666666, - "long": 13.46666666 - }, - "timezones": [ - "Africa/Banjul" - ] - }, - { - "name": "Georgia", - "unicode": "U+1F1EC U+1F1EA", - "emoji": "🇬🇪", - "alpha2": "GE", - "dialCode": "995", - "alpha3": "GEO", - "region": "Asia", - "capital": "Tbilisi", - "geo": { - "lat": 42, - "long": 42 - }, - "timezones": [ - "Asia/Tbilisi" - ] - }, - { - "name": "Germany", - "unicode": "U+1F1E9 U+1F1EA", - "emoji": "🇩🇪", - "alpha2": "DE", - "dialCode": "49", - "alpha3": "DEU", - "region": "Europe", - "capital": "Berlin", - "geo": { - "lat": 51, - "long": 51 - }, - "timezones": [ - "Europe/Berlin", - "Europe/Busingen" - ] - }, - { - "name": "Ghana", - "unicode": "U+1F1EC U+1F1ED", - "emoji": "🇬🇭", - "alpha2": "GH", - "dialCode": "233", - "alpha3": "GHA", - "region": "Africa", - "capital": "Accra", - "geo": { - "lat": 8, - "long": 8 - }, - "timezones": [ - "Africa/Accra" - ] - }, - { - "name": "Gibraltar", - "unicode": "U+1F1EC U+1F1EE", - "emoji": "🇬🇮", - "alpha2": "GI", - "dialCode": "350", - "alpha3": "GIB", - "region": "Europe", - "capital": "Gibraltar", - "geo": { - "lat": 36.13333333, - "long": 36.13333333 - }, - "timezones": [ - "Europe/Gibraltar" - ] - }, - { - "name": "Greece", - "unicode": "U+1F1EC U+1F1F7", - "emoji": "🇬🇷", - "alpha2": "GR", - "dialCode": "30", - "alpha3": "GRC", - "region": "Europe", - "capital": "Athens", - "geo": { - "lat": 39, - "long": 39 - }, - "timezones": [ - "Europe/Athens" - ] - }, - { - "name": "Greenland", - "unicode": "U+1F1EC U+1F1F1", - "emoji": "🇬🇱", - "alpha2": "GL", - "dialCode": "299", - "alpha3": "GRL", - "region": "Americas", - "capital": "Nuuk", - "geo": { - "lat": 72, - "long": 72 - }, - "timezones": [ - "America/Godthab", - "America/Danmarkshavn", - "America/Scoresbysund", - "America/Thule" - ] - }, - { - "name": "Grenada", - "unicode": "U+1F1EC U+1F1E9", - "emoji": "🇬🇩", - "alpha2": "GD", - "dialCode": "1 473", - "alpha3": "GRD", - "region": "Americas", - "capital": "St. George's", - "geo": { - "lat": 12.11666666, - "long": 12.11666666 - }, - "timezones": [ - "America/Grenada" - ] - }, - { - "name": "Guadeloupe", - "unicode": "U+1F1EC U+1F1F5", - "emoji": "🇬🇵", - "alpha2": "GP", - "dialCode": "590", - "alpha3": "GLP", - "region": "Americas", - "capital": "Basse-Terre", - "geo": { - "lat": 16.25, - "long": 16.25 - }, - "timezones": [ - "America/Guadeloupe" - ] - }, - { - "name": "Guam", - "unicode": "U+1F1EC U+1F1FA", - "emoji": "🇬🇺", - "alpha2": "GU", - "dialCode": "1 671", - "alpha3": "GUM", - "region": "Oceania", - "capital": "Hagåtña", - "geo": { - "lat": 13.46666666, - "long": 13.46666666 - }, - "timezones": [ - "Pacific/Guam" - ] - }, - { - "name": "Guatemala", - "unicode": "U+1F1EC U+1F1F9", - "emoji": "🇬🇹", - "alpha2": "GT", - "dialCode": "502", - "alpha3": "GTM", - "region": "Americas", - "capital": "Guatemala City", - "geo": { - "lat": 15.5, - "long": 15.5 - }, - "timezones": [ - "America/Guatemala" - ] - }, - { - "name": "Guernsey", - "unicode": "U+1F1EC U+1F1EC", - "emoji": "🇬🇬", - "alpha2": "GG", - "dialCode": "44", - "alpha3": "GGY", - "region": "Europe", - "capital": "St. Peter Port", - "geo": { - "lat": 49.46666666, - "long": 49.46666666 - }, - "timezones": [ - "Europe/Guernsey" - ] - }, - { - "name": "Guinea", - "unicode": "U+1F1EC U+1F1F3", - "emoji": "🇬🇳", - "alpha2": "GN", - "dialCode": "224", - "alpha3": "GIN", - "region": "Africa", - "capital": "Conakry", - "geo": { - "lat": 11, - "long": 11 - }, - "timezones": [ - "Africa/Conakry" - ] - }, - { - "name": "Guinea-Bissau", - "unicode": "U+1F1EC U+1F1FC", - "emoji": "🇬🇼", - "alpha2": "GW", - "dialCode": "245", - "alpha3": "GNB", - "region": "Africa", - "capital": "Bissau", - "geo": { - "lat": 12, - "long": 12 - }, - "timezones": [ - "Africa/Bissau" - ] - }, - { - "name": "Guyana", - "unicode": "U+1F1EC U+1F1FE", - "emoji": "🇬🇾", - "alpha2": "GY", - "dialCode": "595", - "alpha3": "GUY", - "region": "Americas", - "capital": "Georgetown", - "geo": { - "lat": 5, - "long": 5 - }, - "timezones": [ - "America/Guyana" - ] - }, - { - "name": "Haiti", - "unicode": "U+1F1ED U+1F1F9", - "emoji": "🇭🇹", - "alpha2": "HT", - "dialCode": "509", - "alpha3": "HTI", - "region": "Americas", - "capital": "Port-au-Prince", - "geo": { - "lat": 19, - "long": 19 - }, - "timezones": [ - "America/Port-au-Prince" - ] - }, - { - "name": "Heard Island and Mcdonald Islands", - "unicode": "U+1F1ED U+1F1F2", - "emoji": "🇭🇲", - "alpha2": "HM", - "dialCode": "", - "alpha3": "HMD", - "region": "Oceania", - "geo": {}, - "capital": "", - "timezones": [] - }, - { - "name": "Honduras", - "unicode": "U+1F1ED U+1F1F3", - "emoji": "🇭🇳", - "alpha2": "HN", - "dialCode": "504", - "alpha3": "HND", - "region": "Americas", - "capital": "Tegucigalpa", - "geo": { - "lat": 15, - "long": 15 - }, - "timezones": [ - "America/Tegucigalpa" - ] - }, - { - "name": "Hong Kong", - "unicode": "U+1F1ED U+1F1F0", - "emoji": "🇭🇰", - "alpha2": "HK", - "dialCode": "852", - "alpha3": "HKG", - "region": "Asia", - "capital": "City of Victoria", - "geo": { - "lat": 22.267, - "long": 22.267 - }, - "timezones": [ - "Asia/Hong_Kong" - ] - }, - { - "name": "Hungary", - "unicode": "U+1F1ED U+1F1FA", - "emoji": "🇭🇺", - "alpha2": "HU", - "dialCode": "36", - "alpha3": "HUN", - "region": "Europe", - "capital": "Budapest", - "geo": { - "lat": 47, - "long": 47 - }, - "timezones": [ - "Europe/Budapest" - ] - }, - { - "name": "Iceland", - "unicode": "U+1F1EE U+1F1F8", - "emoji": "🇮🇸", - "alpha2": "IS", - "dialCode": "354", - "alpha3": "ISL", - "region": "Europe", - "capital": "Reykjavik", - "geo": { - "lat": 65, - "long": 65 - }, - "timezones": [ - "Atlantic/Reykjavik" - ] - }, - { - "name": "India", - "unicode": "U+1F1EE U+1F1F3", - "emoji": "🇮🇳", - "alpha2": "IN", - "dialCode": "91", - "alpha3": "IND", - "region": "Asia", - "capital": "New Delhi", - "geo": { - "lat": 20, - "long": 20 - }, - "timezones": [ - "Asia/Kolkata" - ] - }, - { - "name": "Indonesia", - "unicode": "U+1F1EE U+1F1E9", - "emoji": "🇮🇩", - "alpha2": "ID", - "dialCode": "62", - "alpha3": "IDN", - "region": "Asia", - "capital": "Jakarta", - "geo": { - "lat": -5, - "long": -5 - }, - "timezones": [ - "Asia/Jakarta", - "Asia/Pontianak", - "Asia/Makassar", - "Asia/Jayapura" - ] - }, - { - "name": "Iran", - "unicode": "U+1F1EE U+1F1F7", - "emoji": "🇮🇷", - "alpha2": "IR", - "dialCode": "98", - "alpha3": "IRN", - "region": "Asia", - "capital": "Tehran", - "geo": { - "lat": 32, - "long": 32 - }, - "timezones": [ - "Asia/Tehran" - ] - }, - { - "name": "Iraq", - "unicode": "U+1F1EE U+1F1F6", - "emoji": "🇮🇶", - "alpha2": "IQ", - "dialCode": "964", - "alpha3": "IRQ", - "region": "Asia", - "capital": "Baghdad", - "geo": { - "lat": 33, - "long": 33 - }, - "timezones": [ - "Asia/Baghdad" - ] - }, - { - "name": "Ireland", - "unicode": "U+1F1EE U+1F1EA", - "emoji": "🇮🇪", - "alpha2": "IE", - "dialCode": "353", - "alpha3": "IRL", - "region": "Europe", - "capital": "Dublin", - "geo": { - "lat": 53, - "long": 53 - }, - "timezones": [ - "Europe/Dublin" - ] - }, - { - "name": "Isle of Man", - "unicode": "U+1F1EE U+1F1F2", - "emoji": "🇮🇲", - "alpha2": "IM", - "dialCode": "44", - "alpha3": "IMN", - "region": "Europe", - "capital": "Douglas", - "geo": { - "lat": 54.25, - "long": 54.25 - }, - "timezones": [ - "Europe/Isle_of_Man" - ] - }, - { - "name": "Israel", - "unicode": "U+1F1EE U+1F1F1", - "emoji": "🇮🇱", - "alpha2": "IL", - "dialCode": "972", - "alpha3": "ISR", - "region": "Asia", - "capital": "Jerusalem", - "geo": { - "lat": 31.47, - "long": 31.47 - }, - "timezones": [ - "Asia/Jerusalem" - ] - }, - { - "name": "Italy", - "unicode": "U+1F1EE U+1F1F9", - "emoji": "🇮🇹", - "alpha2": "IT", - "dialCode": "39", - "alpha3": "ITA", - "region": "Europe", - "capital": "Rome", - "geo": { - "lat": 42.83333333, - "long": 42.83333333 - }, - "timezones": [ - "Europe/Rome" - ] - }, - { - "name": "Jamaica", - "unicode": "U+1F1EF U+1F1F2", - "emoji": "🇯🇲", - "alpha2": "JM", - "dialCode": "1 876", - "alpha3": "JAM", - "region": "Americas", - "capital": "Kingston", - "geo": { - "lat": 18.25, - "long": 18.25 - }, - "timezones": [ - "America/Jamaica" - ] - }, - { - "name": "Japan", - "unicode": "U+1F1EF U+1F1F5", - "emoji": "🇯🇵", - "alpha2": "JP", - "dialCode": "81", - "alpha3": "JPN", - "region": "Asia", - "capital": "Tokyo", - "geo": { - "lat": 36, - "long": 36 - }, - "timezones": [ - "Asia/Tokyo" - ] - }, - { - "name": "Jersey", - "unicode": "U+1F1EF U+1F1EA", - "emoji": "🇯🇪", - "alpha2": "JE", - "dialCode": "44", - "alpha3": "JEY", - "region": "Europe", - "capital": "Saint Helier", - "geo": { - "lat": 49.25, - "long": 49.25 - }, - "timezones": [ - "Europe/Jersey" - ] - }, - { - "name": "Jordan", - "unicode": "U+1F1EF U+1F1F4", - "emoji": "🇯🇴", - "alpha2": "JO", - "dialCode": "962", - "alpha3": "JOR", - "region": "Asia", - "capital": "Amman", - "geo": { - "lat": 31, - "long": 31 - }, - "timezones": [ - "Asia/Amman" - ] - }, - { - "name": "Kazakhstan", - "unicode": "U+1F1F0 U+1F1FF", - "emoji": "🇰🇿", - "alpha2": "KZ", - "dialCode": "7 7", - "alpha3": "KAZ", - "region": "Asia", - "capital": "Astana", - "geo": { - "lat": 48, - "long": 48 - }, - "timezones": [ - "Asia/Almaty", - "Asia/Qyzylorda", - "Asia/Aqtobe", - "Asia/Aqtau", - "Asia/Oral" - ] - }, - { - "name": "Kenya", - "unicode": "U+1F1F0 U+1F1EA", - "emoji": "🇰🇪", - "alpha2": "KE", - "dialCode": "254", - "alpha3": "KEN", - "region": "Africa", - "capital": "Nairobi", - "geo": { - "lat": 1, - "long": 1 - }, - "timezones": [ - "Africa/Nairobi" - ] - }, - { - "name": "Kiribati", - "unicode": "U+1F1F0 U+1F1EE", - "emoji": "🇰🇮", - "alpha2": "KI", - "dialCode": "686", - "alpha3": "KIR", - "region": "Oceania", - "capital": "South Tarawa", - "geo": { - "lat": 1.41666666, - "long": 1.41666666 - }, - "timezones": [ - "Pacific/Tarawa", - "Pacific/Enderbury", - "Pacific/Kiritimati" - ] - }, - { - "name": "Kosovo", - "unicode": "U+1F1FD U+1F1F0", - "emoji": "🇽🇰", - "alpha2": "XK", - "dialCode": "383", - "alpha3": "", - "region": "", - "capital": "Pristina", - "geo": { - "lat": 42.666667, - "long": 42.666667 - }, - "timezones": [ - "Europe/Belgrade" - ] - }, - { - "name": "Kuwait", - "unicode": "U+1F1F0 U+1F1FC", - "emoji": "🇰🇼", - "alpha2": "KW", - "dialCode": "965", - "alpha3": "KWT", - "region": "Asia", - "capital": "Kuwait City", - "geo": { - "lat": 29.5, - "long": 29.5 - }, - "timezones": [ - "Asia/Kuwait" - ] - }, - { - "name": "Kyrgyzstan", - "unicode": "U+1F1F0 U+1F1EC", - "emoji": "🇰🇬", - "alpha2": "KG", - "dialCode": "996", - "alpha3": "KGZ", - "region": "Asia", - "capital": "Bishkek", - "geo": { - "lat": 41, - "long": 41 - }, - "timezones": [ - "Asia/Bishkek" - ] - }, - { - "name": "Lao People's Democratic Republic", - "unicode": "U+1F1F1 U+1F1E6", - "emoji": "🇱🇦", - "alpha2": "LA", - "dialCode": "856", - "alpha3": "LAO", - "region": "Asia", - "capital": "Vientiane", - "geo": { - "lat": 18, - "long": 18 - }, - "timezones": [ - "Asia/Vientiane" - ] - }, - { - "name": "Latvia", - "unicode": "U+1F1F1 U+1F1FB", - "emoji": "🇱🇻", - "alpha2": "LV", - "dialCode": "371", - "alpha3": "LVA", - "region": "Europe", - "capital": "Riga", - "geo": { - "lat": 57, - "long": 57 - }, - "timezones": [ - "Europe/Riga" - ] - }, - { - "name": "Lebanon", - "unicode": "U+1F1F1 U+1F1E7", - "emoji": "🇱🇧", - "alpha2": "LB", - "dialCode": "961", - "alpha3": "LBN", - "region": "Asia", - "capital": "Beirut", - "geo": { - "lat": 33.83333333, - "long": 33.83333333 - }, - "timezones": [ - "Asia/Beirut" - ] - }, - { - "name": "Lesotho", - "unicode": "U+1F1F1 U+1F1F8", - "emoji": "🇱🇸", - "alpha2": "LS", - "dialCode": "266", - "alpha3": "LSO", - "region": "Africa", - "capital": "Maseru", - "geo": { - "lat": -29.5, - "long": -29.5 - }, - "timezones": [ - "Africa/Maseru" - ] - }, - { - "name": "Liberia", - "unicode": "U+1F1F1 U+1F1F7", - "emoji": "🇱🇷", - "alpha2": "LR", - "dialCode": "231", - "alpha3": "LBR", - "region": "Africa", - "capital": "Monrovia", - "geo": { - "lat": 6.5, - "long": 6.5 - }, - "timezones": [ - "Africa/Monrovia" - ] - }, - { - "name": "Libya", - "unicode": "U+1F1F1 U+1F1FE", - "emoji": "🇱🇾", - "alpha2": "LY", - "dialCode": "218", - "alpha3": "LBY", - "region": "Africa", - "capital": "Tripoli", - "geo": { - "lat": 25, - "long": 25 - }, - "timezones": [ - "Africa/Tripoli" - ] - }, - { - "name": "Liechtenstein", - "unicode": "U+1F1F1 U+1F1EE", - "emoji": "🇱🇮", - "alpha2": "LI", - "dialCode": "423", - "alpha3": "LIE", - "region": "Europe", - "capital": "Vaduz", - "geo": { - "lat": 47.26666666, - "long": 47.26666666 - }, - "timezones": [ - "Europe/Vaduz" - ] - }, - { - "name": "Lithuania", - "unicode": "U+1F1F1 U+1F1F9", - "emoji": "🇱🇹", - "alpha2": "LT", - "dialCode": "370", - "alpha3": "LTU", - "region": "Europe", - "capital": "Vilnius", - "geo": { - "lat": 56, - "long": 56 - }, - "timezones": [ - "Europe/Vilnius" - ] - }, - { - "name": "Luxembourg", - "unicode": "U+1F1F1 U+1F1FA", - "emoji": "🇱🇺", - "alpha2": "LU", - "dialCode": "352", - "alpha3": "LUX", - "region": "Europe", - "capital": "Luxembourg", - "geo": { - "lat": 49.75, - "long": 49.75 - }, - "timezones": [ - "Europe/Luxembourg" - ] - }, - { - "name": "Macao", - "unicode": "U+1F1F2 U+1F1F4", - "emoji": "🇲🇴", - "alpha2": "MO", - "dialCode": "853", - "alpha3": "MAC", - "region": "Asia", - "capital": null, - "geo": { - "lat": 22.16666666, - "long": 22.16666666 - }, - "timezones": [ - "Asia/Macau" - ] - }, - { - "name": "Macedonia", - "unicode": "U+1F1F2 U+1F1F0", - "emoji": "🇲🇰", - "alpha2": "MK", - "dialCode": "389", - "alpha3": "MKD", - "region": "Europe", - "capital": "Skopje", - "geo": { - "lat": 41.83333333, - "long": 41.83333333 - }, - "timezones": [ - "Europe/Skopje" - ] - }, - { - "name": "Madagascar", - "unicode": "U+1F1F2 U+1F1EC", - "emoji": "🇲🇬", - "alpha2": "MG", - "dialCode": "261", - "alpha3": "MDG", - "region": "Africa", - "capital": "Antananarivo", - "geo": { - "lat": -20, - "long": -20 - }, - "timezones": [ - "Indian/Antananarivo" - ] - }, - { - "name": "Malawi", - "unicode": "U+1F1F2 U+1F1FC", - "emoji": "🇲🇼", - "alpha2": "MW", - "dialCode": "265", - "alpha3": "MWI", - "region": "Africa", - "capital": "Lilongwe", - "geo": { - "lat": -13.5, - "long": -13.5 - }, - "timezones": [ - "Africa/Blantyre" - ] - }, - { - "name": "Malaysia", - "unicode": "U+1F1F2 U+1F1FE", - "emoji": "🇲🇾", - "alpha2": "MY", - "dialCode": "60", - "alpha3": "MYS", - "region": "Asia", - "capital": "Kuala Lumpur", - "geo": { - "lat": 2.5, - "long": 2.5 - }, - "timezones": [ - "Asia/Kuala_Lumpur", - "Asia/Kuching" - ] - }, - { - "name": "Maldives", - "unicode": "U+1F1F2 U+1F1FB", - "emoji": "🇲🇻", - "alpha2": "MV", - "dialCode": "960", - "alpha3": "MDV", - "region": "Asia", - "capital": "Malé", - "geo": { - "lat": 3.25, - "long": 3.25 - }, - "timezones": [ - "Indian/Maldives" - ] - }, - { - "name": "Mali", - "unicode": "U+1F1F2 U+1F1F1", - "emoji": "🇲🇱", - "alpha2": "ML", - "dialCode": "223", - "alpha3": "MLI", - "region": "Africa", - "capital": "Bamako", - "geo": { - "lat": 17, - "long": 17 - }, - "timezones": [ - "Africa/Bamako" - ] - }, - { - "name": "Malta", - "unicode": "U+1F1F2 U+1F1F9", - "emoji": "🇲🇹", - "alpha2": "MT", - "dialCode": "356", - "alpha3": "MLT", - "region": "Europe", - "capital": "Valletta", - "geo": { - "lat": 35.83333333, - "long": 35.83333333 - }, - "timezones": [ - "Europe/Malta" - ] - }, - { - "name": "Marshall Islands", - "unicode": "U+1F1F2 U+1F1ED", - "emoji": "🇲🇭", - "alpha2": "MH", - "dialCode": "692", - "alpha3": "MHL", - "region": "Oceania", - "capital": "Majuro", - "geo": { - "lat": 9, - "long": 9 - }, - "timezones": [ - "Pacific/Majuro", - "Pacific/Kwajalein" - ] - }, - { - "name": "Martinique", - "unicode": "U+1F1F2 U+1F1F6", - "emoji": "🇲🇶", - "alpha2": "MQ", - "dialCode": "596", - "alpha3": "MTQ", - "region": "Americas", - "capital": "Fort-de-France", - "geo": { - "lat": 14.666667, - "long": 14.666667 - }, - "timezones": [ - "America/Martinique" - ] - }, - { - "name": "Mauritania", - "unicode": "U+1F1F2 U+1F1F7", - "emoji": "🇲🇷", - "alpha2": "MR", - "dialCode": "222", - "alpha3": "MRT", - "region": "Africa", - "capital": "Nouakchott", - "geo": { - "lat": 20, - "long": 20 - }, - "timezones": [ - "Africa/Nouakchott" - ] - }, - { - "name": "Mauritius", - "unicode": "U+1F1F2 U+1F1FA", - "emoji": "🇲🇺", - "alpha2": "MU", - "dialCode": "230", - "alpha3": "MUS", - "region": "Africa", - "capital": "Port Louis", - "geo": { - "lat": -20.28333333, - "long": -20.28333333 - }, - "timezones": [ - "Indian/Mauritius" - ] - }, - { - "name": "Mayotte", - "unicode": "U+1F1FE U+1F1F9", - "emoji": "🇾🇹", - "alpha2": "YT", - "dialCode": "262", - "alpha3": "MYT", - "region": "Africa", - "capital": "Mamoudzou", - "geo": { - "lat": -12.83333333, - "long": -12.83333333 - }, - "timezones": [ - "Indian/Mayotte" - ] - }, - { - "name": "Mexico", - "unicode": "U+1F1F2 U+1F1FD", - "emoji": "🇲🇽", - "alpha2": "MX", - "dialCode": "52", - "alpha3": "MEX", - "region": "Americas", - "capital": "Mexico City", - "geo": { - "lat": 23, - "long": 23 - }, - "timezones": [ - "America/Mexico_City", - "America/Cancun", - "America/Merida", - "America/Monterrey", - "America/Matamoros", - "America/Mazatlan", - "America/Chihuahua", - "America/Ojinaga", - "America/Hermosillo", - "America/Tijuana", - "America/Bahia_Banderas" - ] - }, - { - "name": "Micronesia", - "unicode": "U+1F1EB U+1F1F2", - "emoji": "🇫🇲", - "alpha2": "FM", - "dialCode": "691", - "alpha3": "FSM", - "region": "Oceania", - "capital": "Palikir", - "geo": { - "lat": 6.91666666, - "long": 6.91666666 - }, - "timezones": [ - "Pacific/Chuuk", - "Pacific/Pohnpei", - "Pacific/Kosrae" - ] - }, - { - "name": "Moldova", - "unicode": "U+1F1F2 U+1F1E9", - "emoji": "🇲🇩", - "alpha2": "MD", - "dialCode": "373", - "alpha3": "MDA", - "region": "Europe", - "capital": "Chișinău", - "geo": { - "lat": 47, - "long": 47 - }, - "timezones": [ - "Europe/Chisinau" - ] - }, - { - "name": "Monaco", - "unicode": "U+1F1F2 U+1F1E8", - "emoji": "🇲🇨", - "alpha2": "MC", - "dialCode": "377", - "alpha3": "MCO", - "region": "Europe", - "capital": "Monaco", - "geo": { - "lat": 43.73333333, - "long": 43.73333333 - }, - "timezones": [ - "Europe/Monaco" - ] - }, - { - "name": "Mongolia", - "unicode": "U+1F1F2 U+1F1F3", - "emoji": "🇲🇳", - "alpha2": "MN", - "dialCode": "976", - "alpha3": "MNG", - "region": "Asia", - "capital": "Ulan Bator", - "geo": { - "lat": 46, - "long": 46 - }, - "timezones": [ - "Asia/Ulaanbaatar", - "Asia/Hovd", - "Asia/Choibalsan" - ] - }, - { - "name": "Montenegro", - "unicode": "U+1F1F2 U+1F1EA", - "emoji": "🇲🇪", - "alpha2": "ME", - "dialCode": "382", - "alpha3": "MNE", - "region": "Europe", - "capital": "Podgorica", - "geo": { - "lat": 42.5, - "long": 42.5 - }, - "timezones": [ - "Europe/Podgorica" - ] - }, - { - "name": "Montserrat", - "unicode": "U+1F1F2 U+1F1F8", - "emoji": "🇲🇸", - "alpha2": "MS", - "dialCode": "1664", - "alpha3": "MSR", - "region": "Americas", - "capital": "Plymouth", - "geo": { - "lat": 16.75, - "long": 16.75 - }, - "timezones": [ - "America/Montserrat" - ] - }, - { - "name": "Morocco", - "unicode": "U+1F1F2 U+1F1E6", - "emoji": "🇲🇦", - "alpha2": "MA", - "dialCode": "212", - "alpha3": "MAR", - "region": "Africa", - "capital": "Rabat", - "geo": { - "lat": 32, - "long": 32 - }, - "timezones": [ - "Africa/Casablanca" - ] - }, - { - "name": "Mozambique", - "unicode": "U+1F1F2 U+1F1FF", - "emoji": "🇲🇿", - "alpha2": "MZ", - "dialCode": "258", - "alpha3": "MOZ", - "region": "Africa", - "capital": "Maputo", - "geo": { - "lat": -18.25, - "long": -18.25 - }, - "timezones": [ - "Africa/Maputo" - ] - }, - { - "name": "Myanmar", - "unicode": "U+1F1F2 U+1F1F2", - "emoji": "🇲🇲", - "alpha2": "MM", - "dialCode": "95", - "alpha3": "MMR", - "region": "Asia", - "capital": "Naypyidaw", - "geo": { - "lat": 22, - "long": 22 - }, - "timezones": [ - "Asia/Rangoon" - ] - }, - { - "name": "Namibia", - "unicode": "U+1F1F3 U+1F1E6", - "emoji": "🇳🇦", - "alpha2": "NA", - "dialCode": "264", - "alpha3": "NAM", - "region": "Africa", - "capital": "Windhoek", - "geo": { - "lat": -22, - "long": -22 - }, - "timezones": [ - "Africa/Windhoek" - ] - }, - { - "name": "Nauru", - "unicode": "U+1F1F3 U+1F1F7", - "emoji": "🇳🇷", - "alpha2": "NR", - "dialCode": "674", - "alpha3": "NRU", - "region": "Oceania", - "capital": "Yaren", - "geo": { - "lat": -0.53333333, - "long": -0.53333333 - }, - "timezones": [ - "Pacific/Nauru" - ] - }, - { - "name": "Nepal", - "unicode": "U+1F1F3 U+1F1F5", - "emoji": "🇳🇵", - "alpha2": "NP", - "dialCode": "977", - "alpha3": "NPL", - "region": "Asia", - "capital": "Kathmandu", - "geo": { - "lat": 28, - "long": 28 - }, - "timezones": [ - "Asia/Kathmandu" - ] - }, - { - "name": "Netherlands", - "unicode": "U+1F1F3 U+1F1F1", - "emoji": "🇳🇱", - "alpha2": "NL", - "dialCode": "31", - "alpha3": "NLD", - "region": "Europe", - "capital": "Amsterdam", - "geo": { - "lat": 52.5, - "long": 52.5 - }, - "timezones": [ - "Europe/Amsterdam" - ] - }, - { - "name": "New Caledonia", - "unicode": "U+1F1F3 U+1F1E8", - "emoji": "🇳🇨", - "alpha2": "NC", - "dialCode": "687", - "alpha3": "NCL", - "region": "Oceania", - "capital": "Nouméa", - "geo": { - "lat": -21.5, - "long": -21.5 - }, - "timezones": [ - "Pacific/Noumea" - ] - }, - { - "name": "New Zealand", - "unicode": "U+1F1F3 U+1F1FF", - "emoji": "🇳🇿", - "alpha2": "NZ", - "dialCode": "64", - "alpha3": "NZL", - "region": "Oceania", - "capital": "Wellington", - "geo": { - "lat": -41, - "long": -41 - }, - "timezones": [ - "Pacific/Auckland", - "Pacific/Chatham" - ] - }, - { - "name": "Nicaragua", - "unicode": "U+1F1F3 U+1F1EE", - "emoji": "🇳🇮", - "alpha2": "NI", - "dialCode": "505", - "alpha3": "NIC", - "region": "Americas", - "capital": "Managua", - "geo": { - "lat": 13, - "long": 13 - }, - "timezones": [ - "America/Managua" - ] - }, - { - "name": "Niger", - "unicode": "U+1F1F3 U+1F1EA", - "emoji": "🇳🇪", - "alpha2": "NE", - "dialCode": "227", - "alpha3": "NER", - "region": "Africa", - "capital": "Niamey", - "geo": { - "lat": 16, - "long": 16 - }, - "timezones": [ - "Africa/Niamey" - ] - }, - { - "name": "Nigeria", - "unicode": "U+1F1F3 U+1F1EC", - "emoji": "🇳🇬", - "alpha2": "NG", - "dialCode": "234", - "alpha3": "NGA", - "region": "Africa", - "capital": "Abuja", - "geo": { - "lat": 10, - "long": 10 - }, - "timezones": [ - "Africa/Lagos" - ] - }, - { - "name": "Niue", - "unicode": "U+1F1F3 U+1F1FA", - "emoji": "🇳🇺", - "alpha2": "NU", - "dialCode": "683", - "alpha3": "NIU", - "region": "Oceania", - "capital": "Alofi", - "geo": { - "lat": -19.03333333, - "long": -19.03333333 - }, - "timezones": [ - "Pacific/Niue" - ] - }, - { - "name": "Norfolk Island", - "unicode": "U+1F1F3 U+1F1EB", - "emoji": "🇳🇫", - "alpha2": "NF", - "dialCode": "672", - "alpha3": "NFK", - "region": "Oceania", - "capital": "Kingston", - "geo": { - "lat": -29.03333333, - "long": -29.03333333 - }, - "timezones": [ - "Pacific/Norfolk" - ] - }, - { - "name": "North Korea", - "unicode": "U+1F1F0 U+1F1F5", - "emoji": "🇰🇵", - "alpha2": "KP", - "dialCode": "850", - "alpha3": "PRK", - "region": "Asia", - "capital": "Pyongyang", - "geo": { - "lat": 40, - "long": 40 - }, - "timezones": [ - "Asia/Pyongyang" - ] - }, - { - "name": "Northern Mariana Islands", - "unicode": "U+1F1F2 U+1F1F5", - "emoji": "🇲🇵", - "alpha2": "MP", - "dialCode": "1 670", - "alpha3": "MNP", - "region": "Oceania", - "capital": "Saipan", - "geo": { - "lat": 15.2, - "long": 15.2 - }, - "timezones": [ - "Pacific/Saipan" - ] - }, - { - "name": "Norway", - "unicode": "U+1F1F3 U+1F1F4", - "emoji": "🇳🇴", - "alpha2": "NO", - "dialCode": "47", - "alpha3": "NOR", - "region": "Europe", - "capital": "Oslo", - "geo": { - "lat": 62, - "long": 62 - }, - "timezones": [ - "Europe/Oslo" - ] - }, - { - "name": "Oman", - "unicode": "U+1F1F4 U+1F1F2", - "emoji": "🇴🇲", - "alpha2": "OM", - "dialCode": "968", - "alpha3": "OMN", - "region": "Asia", - "capital": "Muscat", - "geo": { - "lat": 21, - "long": 21 - }, - "timezones": [ - "Asia/Muscat" - ] - }, - { - "name": "Pakistan", - "unicode": "U+1F1F5 U+1F1F0", - "emoji": "🇵🇰", - "alpha2": "PK", - "dialCode": "92", - "alpha3": "PAK", - "region": "Asia", - "capital": "Islamabad", - "geo": { - "lat": 30, - "long": 30 - }, - "timezones": [ - "Asia/Karachi" - ] - }, - { - "name": "Palau", - "unicode": "U+1F1F5 U+1F1FC", - "emoji": "🇵🇼", - "alpha2": "PW", - "dialCode": "680", - "alpha3": "PLW", - "region": "Oceania", - "capital": "Ngerulmud", - "geo": { - "lat": 7.5, - "long": 7.5 - }, - "timezones": [ - "Pacific/Palau" - ] - }, - { - "name": "Palestinian Territory", - "unicode": "U+1F1F5 U+1F1F8", - "emoji": "🇵🇸", - "alpha2": "PS", - "dialCode": "970", - "alpha3": "PSE", - "region": "Asia", - "capital": "Ramallah", - "geo": { - "lat": 31.9, - "long": 31.9 - }, - "timezones": [ - "Asia/Gaza", - "Asia/Hebron" - ] - }, - { - "name": "Panama", - "unicode": "U+1F1F5 U+1F1E6", - "emoji": "🇵🇦", - "alpha2": "PA", - "dialCode": "507", - "alpha3": "PAN", - "region": "Americas", - "capital": "Panama City", - "geo": { - "lat": 9, - "long": 9 - }, - "timezones": [ - "America/Panama" - ] - }, - { - "name": "Papua New Guinea", - "unicode": "U+1F1F5 U+1F1EC", - "emoji": "🇵🇬", - "alpha2": "PG", - "dialCode": "675", - "alpha3": "PNG", - "region": "Oceania", - "capital": "Port Moresby", - "geo": { - "lat": -6, - "long": -6 - }, - "timezones": [ - "Pacific/Port_Moresby", - "Pacific/Bougainville" - ] - }, - { - "name": "Paraguay", - "unicode": "U+1F1F5 U+1F1FE", - "emoji": "🇵🇾", - "alpha2": "PY", - "dialCode": "595", - "alpha3": "PRY", - "region": "Americas", - "capital": "Asunción", - "geo": { - "lat": -23, - "long": -23 - }, - "timezones": [ - "America/Asuncion" - ] - }, - { - "name": "Peru", - "unicode": "U+1F1F5 U+1F1EA", - "emoji": "🇵🇪", - "alpha2": "PE", - "dialCode": "51", - "alpha3": "PER", - "region": "Americas", - "capital": "Lima", - "geo": { - "lat": -10, - "long": -10 - }, - "timezones": [ - "America/Lima" - ] - }, - { - "name": "Philippines", - "unicode": "U+1F1F5 U+1F1ED", - "emoji": "🇵🇭", - "alpha2": "PH", - "dialCode": "63", - "alpha3": "PHL", - "region": "Asia", - "capital": "Manila", - "geo": { - "lat": 13, - "long": 13 - }, - "timezones": [ - "Asia/Manila" - ] - }, - { - "name": "Pitcairn", - "unicode": "U+1F1F5 U+1F1F3", - "emoji": "🇵🇳", - "alpha2": "PN", - "dialCode": "872", - "alpha3": "PCN", - "region": "Oceania", - "capital": "Adamstown", - "geo": { - "lat": -25.06666666, - "long": -25.06666666 - }, - "timezones": [ - "Pacific/Pitcairn" - ] - }, - { - "name": "Poland", - "unicode": "U+1F1F5 U+1F1F1", - "emoji": "🇵🇱", - "alpha2": "PL", - "dialCode": "48", - "alpha3": "POL", - "region": "Europe", - "capital": "Warsaw", - "geo": { - "lat": 52, - "long": 52 - }, - "timezones": [ - "Europe/Warsaw" - ] - }, - { - "name": "Portugal", - "unicode": "U+1F1F5 U+1F1F9", - "emoji": "🇵🇹", - "alpha2": "PT", - "dialCode": "351", - "alpha3": "PRT", - "region": "Europe", - "capital": "Lisbon", - "geo": { - "lat": 39.5, - "long": 39.5 - }, - "timezones": [ - "Europe/Lisbon", - "Atlantic/Madeira", - "Atlantic/Azores" - ] - }, - { - "name": "Puerto Rico", - "unicode": "U+1F1F5 U+1F1F7", - "emoji": "🇵🇷", - "alpha2": "PR", - "dialCode": "1 939", - "alpha3": "PRI", - "region": "Americas", - "capital": "San Juan", - "geo": { - "lat": 18.25, - "long": 18.25 - }, - "timezones": [ - "America/Puerto_Rico" - ] - }, - { - "name": "Qatar", - "unicode": "U+1F1F6 U+1F1E6", - "emoji": "🇶🇦", - "alpha2": "QA", - "dialCode": "974", - "alpha3": "QAT", - "region": "Asia", - "capital": "Doha", - "geo": { - "lat": 25.5, - "long": 25.5 - }, - "timezones": [ - "Asia/Qatar" - ] - }, - { - "name": "Romania", - "unicode": "U+1F1F7 U+1F1F4", - "emoji": "🇷🇴", - "alpha2": "RO", - "dialCode": "40", - "alpha3": "ROU", - "region": "Europe", - "capital": "Bucharest", - "geo": { - "lat": 46, - "long": 46 - }, - "timezones": [ - "Europe/Bucharest" - ] - }, - { - "name": "Russia", - "unicode": "U+1F1F7 U+1F1FA", - "emoji": "🇷🇺", - "alpha2": "RU", - "dialCode": "7", - "alpha3": "RUS", - "region": "Europe", - "capital": "Moscow", - "geo": { - "lat": 60, - "long": 60 - }, - "timezones": [ - "Europe/Kaliningrad", - "Europe/Moscow", - "Europe/Simferopol", - "Europe/Volgograd", - "Europe/Kirov", - "Europe/Astrakhan", - "Europe/Samara", - "Europe/Ulyanovsk", - "Asia/Yekaterinburg", - "Asia/Omsk", - "Asia/Novosibirsk", - "Asia/Barnaul", - "Asia/Tomsk", - "Asia/Novokuznetsk", - "Asia/Krasnoyarsk", - "Asia/Irkutsk", - "Asia/Chita", - "Asia/Yakutsk", - "Asia/Khandyga", - "Asia/Vladivostok", - "Asia/Ust-Nera", - "Asia/Magadan", - "Asia/Sakhalin", - "Asia/Srednekolymsk", - "Asia/Kamchatka", - "Asia/Anadyr" - ] - }, - { - "name": "Rwanda", - "unicode": "U+1F1F7 U+1F1FC", - "emoji": "🇷🇼", - "alpha2": "RW", - "dialCode": "250", - "alpha3": "RWA", - "region": "Africa", - "capital": "Kigali", - "geo": { - "lat": -2, - "long": -2 - }, - "timezones": [ - "Africa/Kigali" - ] - }, - { - "name": "Réunion", - "unicode": "U+1F1F7 U+1F1EA", - "emoji": "🇷🇪", - "alpha2": "RE", - "dialCode": "262", - "alpha3": "REU", - "region": "Africa", - "capital": "Saint-Denis", - "geo": { - "lat": -21.15, - "long": -21.15 - }, - "timezones": [ - "Indian/Reunion" - ] - }, - { - "name": "Saint Barthélemy", - "unicode": "U+1F1E7 U+1F1F1", - "emoji": "🇧🇱", - "alpha2": "BL", - "dialCode": "590", - "alpha3": "BLM", - "region": "Americas", - "capital": "Gustavia", - "geo": { - "lat": 18.5, - "long": 18.5 - }, - "timezones": [ - "America/St_Barthelemy" - ] - }, - { - "name": "Saint Helena, Ascension and Tristan Da Cunha", - "unicode": "U+1F1F8 U+1F1ED", - "emoji": "🇸🇭", - "alpha2": "SH", - "dialCode": "290", - "alpha3": "SHN", - "region": "Africa", - "geo": {}, - "capital": "", - "timezones": [] - }, - { - "name": "Saint Kitts and Nevis", - "unicode": "U+1F1F0 U+1F1F3", - "emoji": "🇰🇳", - "alpha2": "KN", - "dialCode": "1 869", - "alpha3": "KNA", - "region": "Americas", - "capital": "Basseterre", - "geo": { - "lat": 17.33333333, - "long": 17.33333333 - }, - "timezones": [ - "America/St_Kitts" - ] - }, - { - "name": "Saint Lucia", - "unicode": "U+1F1F1 U+1F1E8", - "emoji": "🇱🇨", - "alpha2": "LC", - "dialCode": "1 758", - "alpha3": "LCA", - "region": "Americas", - "capital": "Castries", - "geo": { - "lat": 13.88333333, - "long": 13.88333333 - }, - "timezones": [ - "America/St_Lucia" - ] - }, - { - "name": "Saint Martin (French Part)", - "unicode": "U+1F1F2 U+1F1EB", - "emoji": "🇲🇫", - "alpha2": "MF", - "dialCode": "590", - "alpha3": "MAF", - "region": "Americas", - "capital": "Marigot", - "geo": { - "lat": 18.08333333, - "long": 18.08333333 - }, - "timezones": [ - "America/Marigot" - ] - }, - { - "name": "Saint Pierre and Miquelon", - "unicode": "U+1F1F5 U+1F1F2", - "emoji": "🇵🇲", - "alpha2": "PM", - "dialCode": "508", - "alpha3": "SPM", - "region": "Americas", - "capital": "Saint-Pierre", - "geo": { - "lat": 46.83333333, - "long": 46.83333333 - }, - "timezones": [ - "America/Miquelon" - ] - }, - { - "name": "Saint Vincent and The Grenadines", - "unicode": "U+1F1FB U+1F1E8", - "emoji": "🇻🇨", - "alpha2": "VC", - "dialCode": "1 784", - "alpha3": "VCT", - "region": "Americas", - "capital": "Kingstown", - "geo": { - "lat": 13.25, - "long": 13.25 - }, - "timezones": [ - "America/St_Vincent" - ] - }, - { - "name": "Samoa", - "unicode": "U+1F1FC U+1F1F8", - "emoji": "🇼🇸", - "alpha2": "WS", - "dialCode": "685", - "alpha3": "WSM", - "region": "Oceania", - "capital": "Apia", - "geo": { - "lat": -13.58333333, - "long": -13.58333333 - }, - "timezones": [ - "Pacific/Apia" - ] - }, - { - "name": "San Marino", - "unicode": "U+1F1F8 U+1F1F2", - "emoji": "🇸🇲", - "alpha2": "SM", - "dialCode": "378", - "alpha3": "SMR", - "region": "Europe", - "capital": "City of San Marino", - "geo": { - "lat": 43.76666666, - "long": 43.76666666 - }, - "timezones": [ - "Europe/San_Marino" - ] - }, - { - "name": "Sao Tome and Principe", - "unicode": "U+1F1F8 U+1F1F9", - "emoji": "🇸🇹", - "alpha2": "ST", - "dialCode": "239", - "alpha3": "STP", - "region": "Africa", - "capital": "São Tomé", - "geo": { - "lat": 1, - "long": 1 - }, - "timezones": [ - "Africa/Sao_Tome" - ] - }, - { - "name": "Saudi Arabia", - "unicode": "U+1F1F8 U+1F1E6", - "emoji": "🇸🇦", - "alpha2": "SA", - "dialCode": "966", - "alpha3": "SAU", - "region": "Asia", - "capital": "Riyadh", - "geo": { - "lat": 25, - "long": 25 - }, - "timezones": [ - "Asia/Riyadh" - ] - }, - { - "name": "Senegal", - "unicode": "U+1F1F8 U+1F1F3", - "emoji": "🇸🇳", - "alpha2": "SN", - "dialCode": "221", - "alpha3": "SEN", - "region": "Africa", - "capital": "Dakar", - "geo": { - "lat": 14, - "long": 14 - }, - "timezones": [ - "Africa/Dakar" - ] - }, - { - "name": "Serbia", - "unicode": "U+1F1F7 U+1F1F8", - "emoji": "🇷🇸", - "alpha2": "RS", - "dialCode": "381", - "alpha3": "SRB", - "region": "Europe", - "capital": "Belgrade", - "geo": { - "lat": 44, - "long": 44 - }, - "timezones": [ - "Europe/Belgrade" - ] - }, - { - "name": "Seychelles", - "unicode": "U+1F1F8 U+1F1E8", - "emoji": "🇸🇨", - "alpha2": "SC", - "dialCode": "248", - "alpha3": "SYC", - "region": "Africa", - "capital": "Victoria", - "geo": { - "lat": -4.58333333, - "long": -4.58333333 - }, - "timezones": [ - "Indian/Mahe" - ] - }, - { - "name": "Sierra Leone", - "unicode": "U+1F1F8 U+1F1F1", - "emoji": "🇸🇱", - "alpha2": "SL", - "dialCode": "232", - "alpha3": "SLE", - "region": "Africa", - "capital": "Freetown", - "geo": { - "lat": 8.5, - "long": 8.5 - }, - "timezones": [ - "Africa/Freetown" - ] - }, - { - "name": "Singapore", - "unicode": "U+1F1F8 U+1F1EC", - "emoji": "🇸🇬", - "alpha2": "SG", - "dialCode": "65", - "alpha3": "SGP", - "region": "Asia", - "capital": "Singapore", - "geo": { - "lat": 1.36666666, - "long": 1.36666666 - }, - "timezones": [ - "Asia/Singapore" - ] - }, - { - "name": "Sint Maarten (Dutch Part)", - "unicode": "U+1F1F8 U+1F1FD", - "emoji": "🇸🇽", - "alpha2": "SX", - "dialCode": "", - "alpha3": "SXM", - "region": "Americas", - "capital": "Philipsburg", - "geo": { - "lat": 18.033333, - "long": 18.033333 - }, - "timezones": [ - "America/Lower_Princes" - ] - }, - { - "name": "Slovakia", - "unicode": "U+1F1F8 U+1F1F0", - "emoji": "🇸🇰", - "alpha2": "SK", - "dialCode": "421", - "alpha3": "SVK", - "region": "Europe", - "capital": "Bratislava", - "geo": { - "lat": 48.66666666, - "long": 48.66666666 - }, - "timezones": [ - "Europe/Bratislava" - ] - }, - { - "name": "Slovenia", - "unicode": "U+1F1F8 U+1F1EE", - "emoji": "🇸🇮", - "alpha2": "SI", - "dialCode": "386", - "alpha3": "SVN", - "region": "Europe", - "capital": "Ljubljana", - "geo": { - "lat": 46.11666666, - "long": 46.11666666 - }, - "timezones": [ - "Europe/Ljubljana" - ] - }, - { - "name": "Solomon Islands", - "unicode": "U+1F1F8 U+1F1E7", - "emoji": "🇸🇧", - "alpha2": "SB", - "dialCode": "677", - "alpha3": "SLB", - "region": "Oceania", - "capital": "Honiara", - "geo": { - "lat": -8, - "long": -8 - }, - "timezones": [ - "Pacific/Guadalcanal" - ] - }, - { - "name": "Somalia", - "unicode": "U+1F1F8 U+1F1F4", - "emoji": "🇸🇴", - "alpha2": "SO", - "dialCode": "252", - "alpha3": "SOM", - "region": "Africa", - "capital": "Mogadishu", - "geo": { - "lat": 10, - "long": 10 - }, - "timezones": [ - "Africa/Mogadishu" - ] - }, - { - "name": "South Africa", - "unicode": "U+1F1FF U+1F1E6", - "emoji": "🇿🇦", - "alpha2": "ZA", - "dialCode": "27", - "alpha3": "ZAF", - "region": "Africa", - "capital": "Pretoria", - "geo": { - "lat": -29, - "long": -29 - }, - "timezones": [ - "Africa/Johannesburg" - ] - }, - { - "name": "South Georgia", - "unicode": "U+1F1EC U+1F1F8", - "emoji": "🇬🇸", - "alpha2": "GS", - "dialCode": "500", - "alpha3": "SGS", - "region": "Americas", - "capital": "King Edward Point", - "geo": { - "lat": -54.5, - "long": -54.5 - }, - "timezones": [ - "Atlantic/South_Georgia" - ] - }, - { - "name": "South Korea", - "unicode": "U+1F1F0 U+1F1F7", - "emoji": "🇰🇷", - "alpha2": "KR", - "dialCode": "82", - "alpha3": "KOR", - "region": "Asia", - "capital": "Seoul", - "geo": { - "lat": 37, - "long": 37 - }, - "timezones": [ - "Asia/Seoul" - ] - }, - { - "name": "South Sudan", - "unicode": "U+1F1F8 U+1F1F8", - "emoji": "🇸🇸", - "alpha2": "SS", - "dialCode": "", - "alpha3": "SSD", - "region": "Africa", - "capital": "Juba", - "geo": { - "lat": 7, - "long": 7 - }, - "timezones": [ - "Africa/Juba" - ] - }, - { - "name": "Spain", - "unicode": "U+1F1EA U+1F1F8", - "emoji": "🇪🇸", - "alpha2": "ES", - "dialCode": "34", - "alpha3": "ESP", - "region": "Europe", - "capital": "Madrid", - "geo": { - "lat": 40, - "long": 40 - }, - "timezones": [ - "Europe/Madrid", - "Africa/Ceuta", - "Atlantic/Canary" - ] - }, - { - "name": "Sri Lanka", - "unicode": "U+1F1F1 U+1F1F0", - "emoji": "🇱🇰", - "alpha2": "LK", - "dialCode": "94", - "alpha3": "LKA", - "region": "Asia", - "capital": "Colombo", - "geo": { - "lat": 7, - "long": 7 - }, - "timezones": [ - "Asia/Colombo" - ] - }, - { - "name": "Sudan", - "unicode": "U+1F1F8 U+1F1E9", - "emoji": "🇸🇩", - "alpha2": "SD", - "dialCode": "249", - "alpha3": "SDN", - "region": "Africa", - "capital": "Khartoum", - "geo": { - "lat": 15, - "long": 15 - }, - "timezones": [ - "Africa/Khartoum" - ] - }, - { - "name": "Suriname", - "unicode": "U+1F1F8 U+1F1F7", - "emoji": "🇸🇷", - "alpha2": "SR", - "dialCode": "597", - "alpha3": "SUR", - "region": "Americas", - "capital": "Paramaribo", - "geo": { - "lat": 4, - "long": 4 - }, - "timezones": [ - "America/Paramaribo" - ] - }, - { - "name": "Svalbard and Jan Mayen", - "unicode": "U+1F1F8 U+1F1EF", - "emoji": "🇸🇯", - "alpha2": "SJ", - "dialCode": "47", - "alpha3": "SJM", - "region": "Europe", - "capital": "Longyearbyen", - "geo": { - "lat": 78, - "long": 78 - }, - "timezones": [ - "Arctic/Longyearbyen" - ] - }, - { - "name": "Swaziland", - "unicode": "U+1F1F8 U+1F1FF", - "emoji": "🇸🇿", - "alpha2": "SZ", - "dialCode": "268", - "alpha3": "SWZ", - "region": "Africa", - "capital": "Lobamba", - "geo": { - "lat": -26.5, - "long": -26.5 - }, - "timezones": [ - "Africa/Mbabane" - ] - }, - { - "name": "Sweden", - "unicode": "U+1F1F8 U+1F1EA", - "emoji": "🇸🇪", - "alpha2": "SE", - "dialCode": "46", - "alpha3": "SWE", - "region": "Europe", - "capital": "Stockholm", - "geo": { - "lat": 62, - "long": 62 - }, - "timezones": [ - "Europe/Stockholm" - ] - }, - { - "name": "Switzerland", - "unicode": "U+1F1E8 U+1F1ED", - "emoji": "🇨🇭", - "alpha2": "CH", - "dialCode": "41", - "alpha3": "CHE", - "region": "Europe", - "capital": "Bern", - "geo": { - "lat": 47, - "long": 47 - }, - "timezones": [ - "Europe/Zurich" - ] - }, - { - "name": "Syrian Arab Republic", - "unicode": "U+1F1F8 U+1F1FE", - "emoji": "🇸🇾", - "alpha2": "SY", - "dialCode": "963", - "alpha3": "SYR", - "region": "Asia", - "capital": "Damascus", - "geo": { - "lat": 35, - "long": 35 - }, - "timezones": [ - "Asia/Damascus" - ] - }, - { - "name": "Taiwan", - "unicode": "U+1F1F9 U+1F1FC", - "emoji": "🇹🇼", - "alpha2": "TW", - "dialCode": "886", - "alpha3": "TWN", - "region": "Asia", - "capital": "Taipei", - "geo": { - "lat": 23.5, - "long": 23.5 - }, - "timezones": [ - "Asia/Taipei" - ] - }, - { - "name": "Tajikistan", - "unicode": "U+1F1F9 U+1F1EF", - "emoji": "🇹🇯", - "alpha2": "TJ", - "dialCode": "992", - "alpha3": "TJK", - "region": "Asia", - "capital": "Dushanbe", - "geo": { - "lat": 39, - "long": 39 - }, - "timezones": [ - "Asia/Dushanbe" - ] - }, - { - "name": "Tanzania", - "unicode": "U+1F1F9 U+1F1FF", - "emoji": "🇹🇿", - "alpha2": "TZ", - "dialCode": "255", - "alpha3": "TZA", - "region": "Africa", - "capital": "Dodoma", - "geo": { - "lat": -6, - "long": -6 - }, - "timezones": [ - "Africa/Dar_es_Salaam" - ] - }, - { - "name": "Thailand", - "unicode": "U+1F1F9 U+1F1ED", - "emoji": "🇹🇭", - "alpha2": "TH", - "dialCode": "66", - "alpha3": "THA", - "region": "Asia", - "capital": "Bangkok", - "geo": { - "lat": 15, - "long": 15 - }, - "timezones": [ - "Asia/Bangkok" - ] - }, - { - "name": "Timor-Leste", - "unicode": "U+1F1F9 U+1F1F1", - "emoji": "🇹🇱", - "alpha2": "TL", - "dialCode": "670", - "alpha3": "TLS", - "region": "Asia", - "capital": "Dili", - "geo": { - "lat": -8.83333333, - "long": -8.83333333 - }, - "timezones": [ - "Asia/Dili" - ] - }, - { - "name": "Togo", - "unicode": "U+1F1F9 U+1F1EC", - "emoji": "🇹🇬", - "alpha2": "TG", - "dialCode": "228", - "alpha3": "TGO", - "region": "Africa", - "capital": "Lomé", - "geo": { - "lat": 8, - "long": 8 - }, - "timezones": [ - "Africa/Lome" - ] - }, - { - "name": "Tokelau", - "unicode": "U+1F1F9 U+1F1F0", - "emoji": "🇹🇰", - "alpha2": "TK", - "dialCode": "690", - "alpha3": "TKL", - "region": "Oceania", - "capital": "Fakaofo", - "geo": { - "lat": -9, - "long": -9 - }, - "timezones": [ - "Pacific/Fakaofo" - ] - }, - { - "name": "Tonga", - "unicode": "U+1F1F9 U+1F1F4", - "emoji": "🇹🇴", - "alpha2": "TO", - "dialCode": "676", - "alpha3": "TON", - "region": "Oceania", - "capital": "Nuku'alofa", - "geo": { - "lat": -20, - "long": -20 - }, - "timezones": [ - "Pacific/Tongatapu" - ] - }, - { - "name": "Trinidad and Tobago", - "unicode": "U+1F1F9 U+1F1F9", - "emoji": "🇹🇹", - "alpha2": "TT", - "dialCode": "1 868", - "alpha3": "TTO", - "region": "Americas", - "capital": "Port of Spain", - "geo": { - "lat": 11, - "long": 11 - }, - "timezones": [ - "America/Port_of_Spain" - ] - }, - { - "name": "Tunisia", - "unicode": "U+1F1F9 U+1F1F3", - "emoji": "🇹🇳", - "alpha2": "TN", - "dialCode": "216", - "alpha3": "TUN", - "region": "Africa", - "capital": "Tunis", - "geo": { - "lat": 34, - "long": 34 - }, - "timezones": [ - "Africa/Tunis" - ] - }, - { - "name": "Turkey", - "unicode": "U+1F1F9 U+1F1F7", - "emoji": "🇹🇷", - "alpha2": "TR", - "dialCode": "90", - "alpha3": "TUR", - "region": "Asia", - "capital": "Ankara", - "geo": { - "lat": 39, - "long": 39 - }, - "timezones": [ - "Europe/Istanbul" - ] - }, - { - "name": "Turkmenistan", - "unicode": "U+1F1F9 U+1F1F2", - "emoji": "🇹🇲", - "alpha2": "TM", - "dialCode": "993", - "alpha3": "TKM", - "region": "Asia", - "capital": "Ashgabat", - "geo": { - "lat": 40, - "long": 40 - }, - "timezones": [ - "Asia/Ashgabat" - ] - }, - { - "name": "Turks and Caicos Islands", - "unicode": "U+1F1F9 U+1F1E8", - "emoji": "🇹🇨", - "alpha2": "TC", - "dialCode": "1 649", - "alpha3": "TCA", - "region": "Americas", - "capital": "Cockburn Town", - "geo": { - "lat": 21.75, - "long": 21.75 - }, - "timezones": [ - "America/Grand_Turk" - ] - }, - { - "name": "Tuvalu", - "unicode": "U+1F1F9 U+1F1FB", - "emoji": "🇹🇻", - "alpha2": "TV", - "dialCode": "688", - "alpha3": "TUV", - "region": "Oceania", - "capital": "Funafuti", - "geo": { - "lat": -8, - "long": -8 - }, - "timezones": [ - "Pacific/Funafuti" - ] - }, - { - "name": "Uganda", - "unicode": "U+1F1FA U+1F1EC", - "emoji": "🇺🇬", - "alpha2": "UG", - "dialCode": "256", - "alpha3": "UGA", - "region": "Africa", - "capital": "Kampala", - "geo": { - "lat": 1, - "long": 1 - }, - "timezones": [ - "Africa/Kampala" - ] - }, - { - "name": "Ukraine", - "unicode": "U+1F1FA U+1F1E6", - "emoji": "🇺🇦", - "alpha2": "UA", - "dialCode": "380", - "alpha3": "UKR", - "region": "Europe", - "capital": "Kiev", - "geo": { - "lat": 49, - "long": 49 - }, - "timezones": [ - "Europe/Kiev", - "Europe/Uzhgorod", - "Europe/Zaporozhye" - ] - }, - { - "name": "United Arab Emirates", - "unicode": "U+1F1E6 U+1F1EA", - "emoji": "🇦🇪", - "alpha2": "AE", - "dialCode": "971", - "alpha3": "ARE", - "region": "Asia", - "capital": "Abu Dhabi", - "geo": { - "lat": 24, - "long": 24 - }, - "timezones": [ - "Asia/Dubai" - ] - }, - { - "name": "United Kingdom", - "unicode": "U+1F1EC U+1F1E7", - "emoji": "🇬🇧", - "alpha2": "GB", - "dialCode": "44", - "alpha3": "GBR", - "region": "Europe", - "capital": "London", - "geo": { - "lat": 54, - "long": 54 - }, - "timezones": [ - "Europe/London" - ] - }, - { - "name": "United States", - "unicode": "U+1F1FA U+1F1F8", - "emoji": "🇺🇸", - "alpha2": "US", - "dialCode": "1", - "alpha3": "USA", - "region": "Americas", - "capital": "Washington D.C.", - "geo": { - "lat": 38, - "long": 38 - }, - "timezones": [ - "America/New_York", - "America/Detroit", - "America/Kentucky/Louisville", - "America/Kentucky/Monticello", - "America/Indiana/Indianapolis", - "America/Indiana/Vincennes", - "America/Indiana/Winamac", - "America/Indiana/Marengo", - "America/Indiana/Petersburg", - "America/Indiana/Vevay", - "America/Chicago", - "America/Indiana/Tell_City", - "America/Indiana/Knox", - "America/Menominee", - "America/North_Dakota/Center", - "America/North_Dakota/New_Salem", - "America/North_Dakota/Beulah", - "America/Denver", - "America/Boise", - "America/Phoenix", - "America/Los_Angeles", - "America/Anchorage", - "America/Juneau", - "America/Sitka", - "America/Metlakatla", - "America/Yakutat", - "America/Nome", - "America/Adak", - "Pacific/Honolulu" - ] - }, - { - "name": "United States Minor Outlying Islands", - "unicode": "U+1F1FA U+1F1F2", - "emoji": "🇺🇲", - "alpha2": "UM", - "dialCode": "", - "alpha3": "UMI", - "region": "Oceania", - "capital": null, - "geo": { - "lat": 19.2911437, - "long": 19.2911437 - }, - "timezones": [ - "Pacific/Johnston", - "Pacific/Midway", - "Pacific/Wake" - ] - }, - { - "name": "Uruguay", - "unicode": "U+1F1FA U+1F1FE", - "emoji": "🇺🇾", - "alpha2": "UY", - "dialCode": "598", - "alpha3": "URY", - "region": "Americas", - "capital": "Montevideo", - "geo": { - "lat": -33, - "long": -33 - }, - "timezones": [ - "America/Montevideo" - ] - }, - { - "name": "Uzbekistan", - "unicode": "U+1F1FA U+1F1FF", - "emoji": "🇺🇿", - "alpha2": "UZ", - "dialCode": "998", - "alpha3": "UZB", - "region": "Asia", - "capital": "Tashkent", - "geo": { - "lat": 41, - "long": 41 - }, - "timezones": [ - "Asia/Samarkand", - "Asia/Tashkent" - ] - }, - { - "name": "Vanuatu", - "unicode": "U+1F1FB U+1F1FA", - "emoji": "🇻🇺", - "alpha2": "VU", - "dialCode": "678", - "alpha3": "VUT", - "region": "Oceania", - "capital": "Port Vila", - "geo": { - "lat": -16, - "long": -16 - }, - "timezones": [ - "Pacific/Efate" - ] - }, - { - "name": "Vatican City", - "unicode": "U+1F1FB U+1F1E6", - "emoji": "🇻🇦", - "alpha2": "VA", - "dialCode": "379", - "alpha3": "VAT", - "region": "Europe", - "capital": "Vatican City", - "geo": { - "lat": 41.9, - "long": 41.9 - }, - "timezones": [ - "Europe/Vatican" - ] - }, - { - "name": "Venezuela", - "unicode": "U+1F1FB U+1F1EA", - "emoji": "🇻🇪", - "alpha2": "VE", - "dialCode": "58", - "alpha3": "VEN", - "region": "Americas", - "capital": "Caracas", - "geo": { - "lat": 8, - "long": 8 - }, - "timezones": [ - "America/Caracas" - ] - }, - { - "name": "Viet Nam", - "unicode": "U+1F1FB U+1F1F3", - "emoji": "🇻🇳", - "alpha2": "VN", - "dialCode": "84", - "alpha3": "VNM", - "region": "Asia", - "capital": "Hanoi", - "geo": { - "lat": 16.16666666, - "long": 16.16666666 - }, - "timezones": [ - "Asia/Ho_Chi_Minh" - ] - }, - { - "name": "Virgin Islands, British", - "unicode": "U+1F1FB U+1F1EC", - "emoji": "🇻🇬", - "alpha2": "VG", - "dialCode": "1 284", - "alpha3": "VGB", - "region": "Americas", - "capital": "Road Town", - "geo": { - "lat": 18.431383, - "long": 18.431383 - }, - "timezones": [ - "America/Tortola" - ] - }, - { - "name": "Virgin Islands, U.S.", - "unicode": "U+1F1FB U+1F1EE", - "emoji": "🇻🇮", - "alpha2": "VI", - "dialCode": "1 340", - "alpha3": "VIR", - "region": "Americas", - "capital": "Charlotte Amalie", - "geo": { - "lat": 18.35, - "long": 18.35 - }, - "timezones": [ - "America/St_Thomas" - ] - }, - { - "name": "Wallis and Futuna", - "unicode": "U+1F1FC U+1F1EB", - "emoji": "🇼🇫", - "alpha2": "WF", - "dialCode": "681", - "alpha3": "WLF", - "region": "Oceania", - "capital": "Mata-Utu", - "geo": { - "lat": -13.3, - "long": -13.3 - }, - "timezones": [ - "Pacific/Wallis" - ] - }, - { - "name": "Western Sahara", - "unicode": "U+1F1EA U+1F1ED", - "emoji": "🇪🇭", - "alpha2": "EH", - "dialCode": "", - "alpha3": "ESH", - "region": "Africa", - "capital": "El Aaiún", - "geo": { - "lat": 24.5, - "long": 24.5 - }, - "timezones": [ - "Africa/El_Aaiun" - ] - }, - { - "name": "Yemen", - "unicode": "U+1F1FE U+1F1EA", - "emoji": "🇾🇪", - "alpha2": "YE", - "dialCode": "967", - "alpha3": "YEM", - "region": "Asia", - "capital": "Sana'a", - "geo": { - "lat": 15, - "long": 15 - }, - "timezones": [ - "Asia/Aden" - ] - }, - { - "name": "Zambia", - "unicode": "U+1F1FF U+1F1F2", - "emoji": "🇿🇲", - "alpha2": "ZM", - "dialCode": "260", - "alpha3": "ZMB", - "region": "Africa", - "capital": "Lusaka", - "geo": { - "lat": -15, - "long": -15 - }, - "timezones": [ - "Africa/Lusaka" - ] - }, - { - "name": "Zimbabwe", - "unicode": "U+1F1FF U+1F1FC", - "emoji": "🇿🇼", - "alpha2": "ZW", - "dialCode": "263", - "alpha3": "ZWE", - "region": "Africa", - "capital": "Harare", - "geo": { - "lat": -20, - "long": -20 - }, - "timezones": [ - "Africa/Harare" - ] - }, - { - "name": "Åland Islands", - "unicode": "U+1F1E6 U+1F1FD", - "emoji": "🇦🇽", - "alpha2": "AX", - "dialCode": "", - "alpha3": "ALA", - "region": "Europe", - "capital": "Mariehamn", - "geo": { - "lat": 60.116667, - "long": 60.116667 - }, - "timezones": [ - "Europe/Mariehamn" - ] - } -] \ No newline at end of file diff --git a/tux/utils/data/timezones.json b/tux/utils/data/timezones.json deleted file mode 100644 index 6f5ab73..0000000 --- a/tux/utils/data/timezones.json +++ /dev/null @@ -1,122 +0,0 @@ -[ - { - "offset": "-10:00", - "timezone": "HST", - "full_timezone": "Pacific/Honolulu", - "discord_emoji": ":flag_hn:" - }, - { - "offset": "-09:00", - "timezone": "AKST", - "full_timezone": "America/Anchorage", - "discord_emoji": ":flag_us:" - }, - { - "offset": "-08:00", - "timezone": "PST", - "full_timezone": "America/Los_Angeles", - "discord_emoji": ":flag_us:" - }, - { - "offset": "-07:00", - "timezone": "MST", - "full_timezone": "America/Denver", - "discord_emoji": ":flag_us:" - }, - { - "offset": "-06:00", - "timezone": "CST", - "full_timezone": "America/Chicago", - "discord_emoji": ":flag_us:" - }, - { - "offset": "-05:00", - "timezone": "EST", - "full_timezone": "America/New_York", - "discord_emoji": ":flag_us:" - }, - { - "offset": "-04:00", - "timezone": "AST", - "full_timezone": "America/Puerto_Rico", - "discord_emoji": ":flag_pr:" - }, - { - "offset": "-03:00", - "timezone": "ART", - "full_timezone": "America/Argentina/Buenos_Aires", - "discord_emoji": ":flag_ar:" - }, - { - "offset": "-02:00", - "timezone": "GST", - "full_timezone": "Atlantic/South_Georgia", - "discord_emoji": ":flag_gs:" - }, - { - "offset": "-01:00", - "timezone": "CVT", - "full_timezone": "Atlantic/Cape_Verde", - "discord_emoji": ":flag_cv:" - }, - { - "offset": "+00:00", - "timezone": "GMT", - "full_timezone": "Europe/London", - "discord_emoji": ":flag_gb:" - }, - { - "offset": "+01:00", - "timezone": "CET", - "full_timezone": "Europe/Berlin", - "discord_emoji": ":flag_de:" - }, - { - "offset": "+02:00", - "timezone": "EET", - "full_timezone": "Europe/Athens", - "discord_emoji": ":flag_gr:" - }, - { - "offset": "+03:00", - "timezone": "MSK", - "full_timezone": "Europe/Moscow", - "discord_emoji": ":flag_ru:" - }, - { - "offset": "+04:00", - "timezone": "GST", - "full_timezone": "Asia/Dubai", - "discord_emoji": ":flag_ae:" - }, - { - "offset": "+05:00", - "timezone": "PKT", - "full_timezone": "Asia/Karachi", - "discord_emoji": ":flag_pk:" - }, - { - "offset": "+06:00", - "timezone": "BST", - "full_timezone": "Asia/Dhaka", - "discord_emoji": ":flag_bd:" - }, - { - "offset": "+07:00", - "timezone": "THA", - "full_timezone": "Asia/Bangkok", - "discord_emoji": ":flag_th:" - }, - { - "offset": "+08:00", - "timezone": "CST", - "full_timezone": "Asia/Singapore", - "discord_emoji": ":flag_sg:" - }, - { - "offset": "+09:00", - "timezone": "JST", - "full_timezone": "Asia/Tokyo", - "discord_emoji": ":flag_jp:" - } -] From bcf4ef1551e11254e97442ca977a0075b18f54e8 Mon Sep 17 00:00:00 2001 From: kzndotsh Date: Sat, 24 Aug 2024 05:37:32 +0000 Subject: [PATCH 68/84] refactor(timezones.py): replace json file with hardcoded timezones for better performance feat(timezones.py): add support for reactionmenu to improve user interaction style(timezones.py): improve code readability by removing unnecessary comments and adding more descriptive variable names --- tux/cogs/utility/timezones.py | 158 +++++++++++++++++++++++++--------- 1 file changed, 115 insertions(+), 43 deletions(-) diff --git a/tux/cogs/utility/timezones.py b/tux/cogs/utility/timezones.py index 2f579c7..64d6ca7 100644 --- a/tux/cogs/utility/timezones.py +++ b/tux/cogs/utility/timezones.py @@ -1,67 +1,139 @@ -import json -from datetime import datetime -from pathlib import Path +from datetime import UTC, datetime +import discord import pytz from discord.ext import commands +from reactionmenu import Page, ViewButton, ViewMenu, ViewSelect -from tux.utils.embeds import EmbedCreator +timezones = { + "North America": [ + ("🇺🇸", "US", "Pacific/Honolulu", "HST", -10), + ("🇺🇸", "US", "America/Anchorage", "AKST", -9), + ("🇺🇸", "US", "America/Los_Angeles", "PST", -8), + ("🇺🇸", "US", "America/Denver", "MST", -7), + ("🇺🇸", "US", "America/Chicago", "CST", -6), + ("🇺🇸", "US", "America/New_York", "EST", -5), + ("🇲🇽", "MX", "America/Mexico_City", "CST", -6), + ("🇨🇦", "CA", "America/Toronto", "EST", -5), + ("🇨🇦", "CA", "America/Vancouver", "PST", -8), + ], + "South America": [ + ("🇧🇷", "BR", "America/Sao_Paulo", "BRT", -3), + ("🇦🇷", "AR", "America/Argentina/Buenos_Aires", "ART", -3), + ("🇨🇱", "CL", "America/Santiago", "CLT", -3), + ("🇵🇪", "PE", "America/Lima", "PET", -5), + ("🇨🇴", "CO", "America/Bogota", "COT", -5), + ("🇻🇪", "VE", "America/Caracas", "VET", -4), + ("🇧🇴", "BO", "America/La_Paz", "BOT", -4), + ("🇵🇾", "PY", "America/Asuncion", "PYT", -4), + ("🇺🇾", "UY", "America/Montevideo", "UYT", -3), + ], + "Africa": [ + ("🇬🇭", "GH", "Africa/Accra", "GMT", 0), + ("🇳🇬", "NG", "Africa/Lagos", "WAT", 1), + ("🇿🇦", "ZA", "Africa/Johannesburg", "SAST", 2), + ("🇪🇬", "EG", "Africa/Cairo", "EET", 2), + ("🇰🇪", "KE", "Africa/Nairobi", "EAT", 3), + ("🇲🇦", "MA", "Africa/Casablanca", "WET", 0), + ("🇹🇿", "TZ", "Africa/Dar_es_Salaam", "EAT", 3), + ("🇩🇿", "DZ", "Africa/Algiers", "CET", 1), + ("🇳🇦", "NA", "Africa/Windhoek", "CAT", 2), + ], + "Europe": [ + ("🇬🇧", "GB", "Europe/London", "GMT", 0), + ("🇩🇪", "DE", "Europe/Berlin", "CET", 1), + ("🇫🇷", "FR", "Europe/Paris", "CET", 1), + ("🇮🇹", "IT", "Europe/Rome", "CET", 1), + ("🇪🇸", "ES", "Europe/Madrid", "CET", 1), + ("🇳🇱", "NL", "Europe/Amsterdam", "CET", 1), + ("🇧🇪", "BE", "Europe/Brussels", "CET", 1), + ("🇷🇺", "RU", "Europe/Moscow", "MSK", 3), + ("🇬🇷", "GR", "Europe/Athens", "EET", 2), + ], + "Asia": [ + ("🇦🇪", "AE", "Asia/Dubai", "GST", 4), + ("🇮🇳", "IN", "Asia/Kolkata", "IST", 5.5), + ("🇧🇩", "BD", "Asia/Dhaka", "BST", 6), + ("🇲🇲", "MM", "Asia/Yangon", "MMT", 6.5), + ("🇹🇭", "TH", "Asia/Bangkok", "ICT", 7), + ("🇻🇳", "VN", "Asia/Ho_Chi_Minh", "ICT", 7), + ("🇨🇳", "CN", "Asia/Shanghai", "CST", 8), + ("🇭🇰", "HK", "Asia/Hong_Kong", "HKT", 8), + ("🇯🇵", "JP", "Asia/Tokyo", "JST", 9), + ], + "Australia/Oceania": [ + ("🇦🇺", "AU", "Australia/Perth", "AWST", 8), + ("🇦🇺", "AU", "Australia/Sydney", "AEST", 10), + ("🇫🇯", "FJ", "Pacific/Fiji", "FJT", 12), + ("🇳🇿", "NZ", "Pacific/Auckland", "NZDT", 13), + ("🇵🇬", "PG", "Pacific/Port_Moresby", "PGT", 10), + ("🇼🇸", "WS", "Pacific/Apia", "WSST", 13), + ("🇸🇧", "SB", "Pacific/Guadalcanal", "SBT", 11), + ("🇻🇺", "VU", "Pacific/Efate", "VUT", 11), + ("🇵🇫", "PF", "Pacific/Tahiti", "THAT", -10), + ], +} + +continent_emojis = { + "North America": "🌎", + "South America": "🌎", + "Africa": "🌍", + "Europe": "🌍", + "Asia": "🌏", + "Australia/Oceania": "🌏", +} class Timezones(commands.Cog): def __init__(self, bot: commands.Bot) -> None: self.bot = bot - def loadjson(self, json_file: str) -> dict: - """ - Opens the JSON file and returns a dictionary + @commands.hybrid_command( + name="timezones", + aliases=["tz"], + usage="timezones", + ) + async def timezones(self, ctx: commands.Context[commands.Bot]) -> None: + utc_now = datetime.now(UTC) - Parameters - ---------- - json_file : str - The path to the json file - """ + menu = ViewMenu(ctx, menu_type=ViewMenu.TypeEmbed) - with Path.open(json_file) as file: - return json.load(file) + default_embeds: list[discord.Embed] = [] + options: dict[discord.SelectOption, list[Page]] = {} - async def buildtzstring(self, json_file: str) -> str: - """ - Formats the timezone data within the timezones.json file into a string. + for continent, tz_list in timezones.items(): + embeds: list[discord.Embed] = [] + pages = [tz_list[i : i + 9] for i in range(0, len(tz_list), 9)] - Parameters - ---------- - json_file : str - The path to the json file - """ + for page in pages: + embed = discord.Embed(title=f"Timezones in {continent}", color=discord.Color.blurple()) - timezone_data = self.loadjson(json_file) + for flag, _country, tz_name, abbr, utc_offset in page: + tz = pytz.timezone(tz_name) + local_time = utc_now.astimezone(tz) + time_24hr = local_time.strftime("%H:%M") + time_12hr = local_time.strftime("%I:%M %p") - formatted_lines = [] - utc_now = datetime.now(pytz.utc) + embed.add_field( + name=f"{flag} {abbr} (UTC{utc_offset:+.2f})", + value=f"`{time_24hr} | {time_12hr}`", + inline=True, + ) - for entry in timezone_data: - entry_tz = pytz.timezone(f'{entry["full_timezone"]}') - entry_time_now = utc_now.astimezone(entry_tz) - formatted_time = entry_time_now.strftime("%H:%M") - line = f'{entry["discord_emoji"]} `{entry["offset"]} {entry["timezone"]}` | **{formatted_time}**' - formatted_lines.append(line) + embeds.append(embed) - return "\n".join(formatted_lines) + default_embeds.extend(embeds) - @commands.hybrid_command(name="timezones") - async def timezones(self, ctx: commands.Context) -> None: - """ - Presents a list of the top 20 timezones in the world. - """ + options[discord.SelectOption(label=continent, emoji=continent_emojis[continent])] = Page.from_embeds(embeds) - embed = EmbedCreator.create_info_embed( - title="List of timezones", - description=await self.buildtzstring("./tux/utils/data/timezones.json"), - ctx=ctx, - ) + for embed in default_embeds: + menu.add_page(embed) - await ctx.send(embed=embed) + select = ViewSelect(title="Select Continent", options=options) + menu.add_select(select) + menu.add_button(ViewButton.end_session()) + + await menu.start() async def setup(bot: commands.Bot) -> None: From 67bbf33af488263b9585f6b927b6bce6c81f8918 Mon Sep 17 00:00:00 2001 From: kzndotsh Date: Sat, 24 Aug 2024 06:07:51 +0000 Subject: [PATCH 69/84] fix(help.py): correct flag example in help message from '--reason' to '-reason' for better command understanding --- tux/help.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tux/help.py b/tux/help.py index 51e6f90..84082c4 100644 --- a/tux/help.py +++ b/tux/help.py @@ -225,7 +225,7 @@ class TuxHelp(commands.HelpCommand): ) embed.add_field( name="Flag Help", - value=f"Flags in `[]` are required and `<>` are optional. Most flags have aliases that can be used.\n> e.g. `{prefix}ban @user --reason spamming` or `{prefix}b @user -r spamming`", + value=f"Flags in `[]` are required and `<>` are optional. Most flags have aliases that can be used.\n> e.g. `{prefix}ban @user -reason spamming` or `{prefix}b @user -r spamming`", inline=False, ) embed.add_field( From c5164575074a6a057136b3d412844d866a5e4c5c Mon Sep 17 00:00:00 2001 From: kzndotsh Date: Sat, 24 Aug 2024 09:26:44 +0000 Subject: [PATCH 70/84] feat(main.py): add case_insensitive=True to Tux bot initialization to allow case-insensitive command inputs --- tux/main.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tux/main.py b/tux/main.py index 69ef251..a1a3849 100644 --- a/tux/main.py +++ b/tux/main.py @@ -33,6 +33,7 @@ async def main() -> None: bot = Tux( command_prefix=CONST.PREFIX, strip_after_prefix=True, + case_insensitive=True, intents=discord.Intents.all(), owner_ids=[*CONST.SYSADMIN_IDS, CONST.BOT_OWNER_ID], allowed_mentions=discord.AllowedMentions(everyone=False), From c7eec38dc5b5c8f0e22c686280aa7adce11e68d1 Mon Sep 17 00:00:00 2001 From: kzndotsh Date: Sat, 24 Aug 2024 09:26:55 +0000 Subject: [PATCH 71/84] feat(converters.py): add CaseTypeConverter class to handle conversion of case types in discord commands --- tux/utils/converters.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 tux/utils/converters.py diff --git a/tux/utils/converters.py b/tux/utils/converters.py new file mode 100644 index 0000000..6bf3e30 --- /dev/null +++ b/tux/utils/converters.py @@ -0,0 +1,14 @@ +from typing import Any + +from discord.ext import commands + +from prisma.enums import CaseType + + +class CaseTypeConverter(commands.Converter[CaseType]): + async def convert(self, ctx: commands.Context[Any], argument: str) -> CaseType: + try: + return CaseType[argument.upper()] + except KeyError as e: + msg = f"Invalid CaseType: {argument}" + raise commands.BadArgument(msg) from e From b67c5425ad877a454430b648a8f96a9bdb3cfa80 Mon Sep 17 00:00:00 2001 From: kzndotsh Date: Sat, 24 Aug 2024 09:28:03 +0000 Subject: [PATCH 72/84] refactor(flags.py): make all flag classes case insensitive for better user experience style(flags.py): update descriptions for clarity and consistency feat(flags.py): add 'silent' flag to TempBanFlags for optional DM to target feat(flags.py): add more aliases to 'expires_at' flag in TempBanFlags for better usability feat(flags.py): add CaseTypeConverter to 'type' flag in CasesViewFlags for proper type conversion refactor(flags.py): change 'target' and 'moderator' flags in CasesViewFlags from Member to User for broader applicability --- tux/utils/flags.py | 68 ++++++++++++++++++++++++++-------------------- 1 file changed, 38 insertions(+), 30 deletions(-) diff --git a/tux/utils/flags.py b/tux/utils/flags.py index ae419ef..eecac80 100644 --- a/tux/utils/flags.py +++ b/tux/utils/flags.py @@ -3,18 +3,19 @@ from discord.ext import commands from discord.utils import MISSING from prisma.enums import CaseType +from tux.utils.converters import CaseTypeConverter -class BanFlags(commands.FlagConverter, delimiter=" ", prefix="-"): +class BanFlags(commands.FlagConverter, case_insensitive=True, delimiter=" ", prefix="-"): reason: str = commands.flag( name="reason", - description="The reason for the member ban.", + description="The reason for the ban.", aliases=["r"], default=MISSING, ) purge_days: int = commands.flag( name="purge_days", - description="Number of days in messages", + description="The number of days (< 7) to purge in messages.", aliases=["p", "purge"], default=0, ) @@ -26,17 +27,17 @@ class BanFlags(commands.FlagConverter, delimiter=" ", prefix="-"): ) -class TempBanFlags(commands.FlagConverter, delimiter=" ", prefix="-"): +class TempBanFlags(commands.FlagConverter, case_insensitive=True, delimiter=" ", prefix="-"): reason: str = commands.flag( name="reason", - description="The reason for the member temp ban.", + description="The reason for the temp ban.", aliases=["r"], default=MISSING, ) expires_at: int = commands.flag( name="expires_at", description="The time in days the ban will last for.", - aliases=["t", "d", "e"], + aliases=["t", "d", "e", "duration", "expires", "time"], ) purge_days: int = commands.flag( name="purge_days", @@ -44,12 +45,18 @@ class TempBanFlags(commands.FlagConverter, delimiter=" ", prefix="-"): aliases=["p"], default=0, ) + silent: bool = commands.flag( + name="silent", + description="Do not send a DM to the target.", + aliases=["s", "quiet"], + default=False, + ) -class KickFlags(commands.FlagConverter, delimiter=" ", prefix="-"): +class KickFlags(commands.FlagConverter, case_insensitive=True, delimiter=" ", prefix="-"): reason: str = commands.flag( name="reason", - description="The reason for the member kick.", + description="The reason for the kick.", aliases=["r"], default=MISSING, ) @@ -61,16 +68,16 @@ class KickFlags(commands.FlagConverter, delimiter=" ", prefix="-"): ) -class TimeoutFlags(commands.FlagConverter, delimiter=" ", prefix="-"): +class TimeoutFlags(commands.FlagConverter, case_insensitive=True, delimiter=" ", prefix="-"): duration: str = commands.flag( name="duration", - description="The duration of the timeout.", + description="The duration of the timeout. (e.g. 1d, 1h, 1m)", aliases=["d"], default=MISSING, ) reason: str = commands.flag( name="reason", - description="The reason for the member ban. (e.g. 1d, 1h, 1m)", + description="The reason for the timeout.", aliases=["r"], default=MISSING, ) @@ -82,10 +89,10 @@ class TimeoutFlags(commands.FlagConverter, delimiter=" ", prefix="-"): ) -class UntimeoutFlags(commands.FlagConverter, delimiter=" ", prefix="-"): +class UntimeoutFlags(commands.FlagConverter, case_insensitive=True, delimiter=" ", prefix="-"): reason: str = commands.flag( name="reason", - description="The reason for the member ban.", + description="The reason for the untimeout.", aliases=["r"], default=MISSING, ) @@ -97,26 +104,26 @@ class UntimeoutFlags(commands.FlagConverter, delimiter=" ", prefix="-"): ) -class UnbanFlags(commands.FlagConverter, delimiter=" ", prefix="-"): +class UnbanFlags(commands.FlagConverter, case_insensitive=True, delimiter=" ", prefix="-"): username_or_id: str = commands.flag( name="username_or_id", - description="The username or ID of the user to ban.", + description="The username or ID of the user.", aliases=["u"], default=MISSING, positional=True, ) reason: str = commands.flag( name="reason", - description="The reason for the member ban.", + description="The reason for the unban.", aliases=["r"], default=MISSING, ) -class JailFlags(commands.FlagConverter, delimiter=" ", prefix="-"): +class JailFlags(commands.FlagConverter, case_insensitive=True, delimiter=" ", prefix="-"): reason: str = commands.flag( name="reason", - description="The reason for the member jail.", + description="The reason for the jail.", aliases=["r"], default=MISSING, ) @@ -128,10 +135,10 @@ class JailFlags(commands.FlagConverter, delimiter=" ", prefix="-"): ) -class UnjailFlags(commands.FlagConverter, delimiter=" ", prefix="-"): +class UnjailFlags(commands.FlagConverter, case_insensitive=True, delimiter=" ", prefix="-"): reason: str = commands.flag( name="reason", - description="The reason for the member unjail.", + description="The reason for the unjail.", aliases=["r"], default=MISSING, ) @@ -143,20 +150,21 @@ class UnjailFlags(commands.FlagConverter, delimiter=" ", prefix="-"): ) -class CasesViewFlags(commands.FlagConverter, delimiter=" ", prefix="-"): +class CasesViewFlags(commands.FlagConverter, case_insensitive=True, delimiter=" ", prefix="-"): type: CaseType = commands.flag( name="case_type", description="The case type to view.", aliases=["t"], default=None, + converter=CaseTypeConverter, ) - target: discord.Member = commands.flag( + target: discord.User = commands.flag( name="case_target", - description="The member to view cases for.", - aliases=["memb", "m", "user", "u"], + description="The user to view cases for.", + aliases=["user", "u", "member", "memb", "m"], default=None, ) - moderator: discord.Member = commands.flag( + moderator: discord.User = commands.flag( name="case_moderator", description="The moderator to view cases for.", aliases=["mod"], @@ -164,7 +172,7 @@ class CasesViewFlags(commands.FlagConverter, delimiter=" ", prefix="-"): ) -class CaseModifyFlags(commands.FlagConverter, delimiter=" ", prefix="-"): +class CaseModifyFlags(commands.FlagConverter, case_insensitive=True, delimiter=" ", prefix="-"): status: bool | None = commands.flag( name="case_status", description="The status of the case.", @@ -177,10 +185,10 @@ class CaseModifyFlags(commands.FlagConverter, delimiter=" ", prefix="-"): ) -class WarnFlags(commands.FlagConverter, delimiter=" ", prefix="-"): +class WarnFlags(commands.FlagConverter, case_insensitive=True, delimiter=" ", prefix="-"): reason: str = commands.flag( name="reason", - description="The reason for the member warn.", + description="The reason for the warn.", aliases=["r"], default=MISSING, ) @@ -192,7 +200,7 @@ class WarnFlags(commands.FlagConverter, delimiter=" ", prefix="-"): ) -class SnippetBanFlags(commands.FlagConverter, delimiter=" ", prefix="-"): +class SnippetBanFlags(commands.FlagConverter, case_insensitive=True, delimiter=" ", prefix="-"): reason: str = commands.flag( name="reason", description="The reason for the snippet ban.", @@ -207,7 +215,7 @@ class SnippetBanFlags(commands.FlagConverter, delimiter=" ", prefix="-"): ) -class SnippetUnbanFlags(commands.FlagConverter, delimiter=" ", prefix="-"): +class SnippetUnbanFlags(commands.FlagConverter, case_insensitive=True, delimiter=" ", prefix="-"): reason: str = commands.flag( name="reason", description="The reason for the snippet unban.", From 5dded916b479c4c4db43a7513b9e64e2f9314075 Mon Sep 17 00:00:00 2001 From: kzndotsh Date: Sat, 24 Aug 2024 09:28:24 +0000 Subject: [PATCH 73/84] refactor(warn.py): change 'user' to 'member' in warn function docstring for better clarity fix(warn.py): modify warn command usage to include 'reason' and change 'flags' to 'silent' for better command understanding --- tux/cogs/moderation/warn.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tux/cogs/moderation/warn.py b/tux/cogs/moderation/warn.py index d83c35e..975d86c 100644 --- a/tux/cogs/moderation/warn.py +++ b/tux/cogs/moderation/warn.py @@ -18,7 +18,7 @@ class Warn(ModerationCogBase): @commands.hybrid_command( name="warn", aliases=["w"], - usage="warn [target] ", + usage="warn [target] [reason] ", ) @commands.guild_only() @checks.has_pl(2) @@ -30,7 +30,7 @@ class Warn(ModerationCogBase): flags: WarnFlags, ) -> None: """ - Warn a user from the server. + Warn a member from the server. Parameters ---------- From 3064ae565b2e11a7edf980d31e08da088f6707bd Mon Sep 17 00:00:00 2001 From: kzndotsh Date: Sat, 24 Aug 2024 09:28:40 +0000 Subject: [PATCH 74/84] refactor(untimeout.py): change 'user' to 'member' for better context understanding feat(untimeout.py): add 'silent' flag to untimeout command for silent operation docs(untimeout.py): update command documentation to reflect new 'silent' flag and 'member' terminology --- tux/cogs/moderation/untimeout.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tux/cogs/moderation/untimeout.py b/tux/cogs/moderation/untimeout.py index 634b957..60587cf 100644 --- a/tux/cogs/moderation/untimeout.py +++ b/tux/cogs/moderation/untimeout.py @@ -18,7 +18,7 @@ class Untimeout(ModerationCogBase): @commands.hybrid_command( name="untimeout", aliases=["ut", "uto", "unmute"], - usage="untimeout [target] [reason]", + usage="untimeout [target] [reason] ", ) @commands.guild_only() @checks.has_pl(2) @@ -30,16 +30,16 @@ class Untimeout(ModerationCogBase): flags: UntimeoutFlags, ) -> None: """ - Untimeout a user from the server. + Untimeout a member from the server. Parameters ---------- ctx : commands.Context[commands.Bot] The context in which the command is being invoked. target : discord.Member - The user to timeout. + The member to untimeout. flags : UntimeoutFlags - The flags for the command. + The flags for the command (reason: str, silent: bool). Raises ------ From 13ce367cb200a7ab3cf7a20dfd2d44174a2c32cd Mon Sep 17 00:00:00 2001 From: kzndotsh Date: Sat, 24 Aug 2024 09:29:04 +0000 Subject: [PATCH 75/84] refactor(unjail.py): replace 'user' with 'member' for better context understanding fix(unjail.py): update error messages to reflect 'member' terminology for consistency --- tux/cogs/moderation/unjail.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/tux/cogs/moderation/unjail.py b/tux/cogs/moderation/unjail.py index de41a80..ff4b191 100644 --- a/tux/cogs/moderation/unjail.py +++ b/tux/cogs/moderation/unjail.py @@ -30,7 +30,7 @@ class Unjail(ModerationCogBase): flags: UnjailFlags, ) -> None: """ - Unjail a user in the server. + Unjail a member in the server. Parameters ---------- @@ -41,6 +41,7 @@ class Unjail(ModerationCogBase): flags : UnjailFlags The flags for the command. (reason: str, silent: bool) """ + if not ctx.guild: logger.warning("Unjail command used outside of a guild context.") return @@ -55,7 +56,7 @@ class Unjail(ModerationCogBase): return if jail_role not in target.roles: - await ctx.send("The user is not jailed.", delete_after=30, ephemeral=True) + await ctx.send("The member is not jailed.", delete_after=30, ephemeral=True) return if not await self._check_jail_channel(ctx): @@ -63,7 +64,7 @@ class Unjail(ModerationCogBase): case = await self.db.case.get_last_jail_case_by_target_id(ctx.guild.id, target.id) if not case: - await ctx.send("No jail case found for the user.", delete_after=30, ephemeral=True) + await ctx.send("No jail case found for this member.", delete_after=30, ephemeral=True) return await self._unjail_user(ctx, target, jail_role, case, flags.reason) @@ -121,11 +122,11 @@ class Unjail(ModerationCogBase): if previous_roles: await target.add_roles(*previous_roles, reason=reason, atomic=False) else: - await ctx.send("No previous roles found for the user.", delete_after=30, ephemeral=True) + await ctx.send("No previous roles found for the member.", delete_after=30, ephemeral=True) except (discord.Forbidden, discord.HTTPException) as e: - logger.error(f"Failed to unjail user {target}. {e}") - await ctx.send(f"Failed to unjail user {target}. {e}", delete_after=30, ephemeral=True) + logger.error(f"Failed to unjail member {target}. {e}") + await ctx.send(f"Failed to unjail member {target}. {e}", delete_after=30, ephemeral=True) async def _insert_unjail_case( self, From 52b5c3ab6bc4eceabdfbe71900b38b7be36a14c2 Mon Sep 17 00:00:00 2001 From: kzndotsh Date: Sat, 24 Aug 2024 09:29:23 +0000 Subject: [PATCH 76/84] refactor(unban.py): modify usage instruction to specify username_or_id instead of target for clarity fix(unban.py): rearrange parameters in insert_case function call to match function definition docs(unban.py): update flags parameter description to include specific flag details for better understanding --- tux/cogs/moderation/unban.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tux/cogs/moderation/unban.py b/tux/cogs/moderation/unban.py index 27b6025..0eadebb 100644 --- a/tux/cogs/moderation/unban.py +++ b/tux/cogs/moderation/unban.py @@ -18,7 +18,7 @@ class Unban(ModerationCogBase): @commands.hybrid_command( name="unban", aliases=["ub"], - usage="unban [target] [reason]", + usage="unban [username_or_id] [reason]", ) @commands.guild_only() @checks.has_pl(3) @@ -38,7 +38,7 @@ class Unban(ModerationCogBase): target : discord.Member The member to unban. flags : UnbanFlags - The flags for the command. + The flags for the command (username_or_id: str, reason: str). Raises ------ @@ -69,11 +69,11 @@ class Unban(ModerationCogBase): return case = await self.db.case.insert_case( + guild_id=ctx.guild.id, case_target_id=user.id, case_moderator_id=ctx.author.id, case_type=CaseType.UNBAN, case_reason=flags.reason, - guild_id=ctx.guild.id, ) await self.handle_case_response(ctx, case, "created", flags.reason, user) From 81d06cbb2c6e97934d65d06a638d82eb33af7cd4 Mon Sep 17 00:00:00 2001 From: kzndotsh Date: Sat, 24 Aug 2024 09:29:37 +0000 Subject: [PATCH 77/84] refactor(timeout.py): replace 'user' with 'member' for better context understanding style(timeout.py): add a blank line for better code readability --- tux/cogs/moderation/timeout.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tux/cogs/moderation/timeout.py b/tux/cogs/moderation/timeout.py index 3fb05c9..fd4620f 100644 --- a/tux/cogs/moderation/timeout.py +++ b/tux/cogs/moderation/timeout.py @@ -73,14 +73,14 @@ class Timeout(ModerationCogBase): flags: TimeoutFlags, ) -> None: """ - Timeout a user from the server. + Timeout a member from the server. Parameters ---------- ctx : commands.Context[commands.Bot] The context in which the command is being invoked. target : discord.Member - The user to timeout. + The member to timeout. flags : TimeoutFlags The flags for the command. @@ -89,6 +89,7 @@ class Timeout(ModerationCogBase): discord.DiscordException If an error occurs while timing out the user. """ + if ctx.guild is None: logger.warning("Timeout command used outside of a guild context.") return From 07b4e67fad42137edf3739b6679d0afa4b449d65 Mon Sep 17 00:00:00 2001 From: kzndotsh Date: Sat, 24 Aug 2024 09:29:53 +0000 Subject: [PATCH 78/84] refactor(ban.py): change 'user' to 'member' in command usage and docstring for better context fix(ban.py): add action parameter in send_dm method call to correctly specify the action taken docs(ban.py): update docstring to specify that purge_days should be less than 7 --- tux/cogs/moderation/ban.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tux/cogs/moderation/ban.py b/tux/cogs/moderation/ban.py index 8ceb161..747bba9 100644 --- a/tux/cogs/moderation/ban.py +++ b/tux/cogs/moderation/ban.py @@ -18,7 +18,7 @@ class Ban(ModerationCogBase): @commands.hybrid_command( name="ban", aliases=["b"], - usage="ban [target] ", + usage="ban [target] [reason] ", ) @commands.guild_only() @checks.has_pl(3) @@ -30,7 +30,7 @@ class Ban(ModerationCogBase): flags: BanFlags, ) -> None: """ - Ban a user from the server. + Ban a member from the server. Parameters ---------- @@ -39,7 +39,7 @@ class Ban(ModerationCogBase): target : discord.Member The member to ban. flags : BanFlags - The flags for the command. (reason: str, purge_days: int, silent: bool) + The flags for the command. (reason: str, purge_days: int (< 7), silent: bool) Raises ------ @@ -58,7 +58,7 @@ class Ban(ModerationCogBase): return try: - await self.send_dm(ctx, flags.silent, target, flags.reason, "banned") + await self.send_dm(ctx, flags.silent, target, flags.reason, action="banned") await ctx.guild.ban(target, reason=flags.reason, delete_message_days=flags.purge_days) except (discord.Forbidden, discord.HTTPException) as e: From 1d1c1bf003d097e0b2eb875ffa98c0dee2fe2254 Mon Sep 17 00:00:00 2001 From: kzndotsh Date: Sat, 24 Aug 2024 09:30:28 +0000 Subject: [PATCH 79/84] refactor(cases.py): update command usage details for better clarity feat(cases.py): add all_can_click and delete_on_timeout options to ViewMenu for improved user interaction style(cases.py): replace menu buttons with more descriptive ones for better user experience fix(cases.py): replace "?" with ":interrobang:" for missing case details for better error visibility style(cases.py): change case date formatting from italic to underline for better readability --- tux/cogs/moderation/cases.py | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/tux/cogs/moderation/cases.py b/tux/cogs/moderation/cases.py index f48a2cc..a53e89a 100644 --- a/tux/cogs/moderation/cases.py +++ b/tux/cogs/moderation/cases.py @@ -49,7 +49,7 @@ class Cases(ModerationCogBase): @cases.command( name="view", aliases=["v", "ls", "list"], - usage="cases view ", + usage="cases view ", ) @commands.guild_only() @checks.has_pl(2) @@ -85,7 +85,7 @@ class Cases(ModerationCogBase): @cases.command( name="modify", aliases=["m", "edit"], - usage="cases modify [case_number] ", + usage="cases modify [case_number] ", ) @commands.guild_only() @checks.has_pl(2) @@ -294,7 +294,7 @@ class Cases(ModerationCogBase): cases: list[Case], total_cases: int, ) -> None: - menu = ViewMenu(ctx, menu_type=ViewMenu.TypeEmbed) + menu = ViewMenu(ctx, menu_type=ViewMenu.TypeEmbed, all_can_click=True, delete_on_timeout=True) if not cases: embed = discord.Embed( @@ -310,9 +310,16 @@ class Cases(ModerationCogBase): embed = self._create_case_list_embed(ctx, cases[i : i + cases_per_page], total_cases) menu.add_page(embed) - menu.add_button(ViewButton.back()) - menu.add_button(ViewButton.next()) - menu.add_button(ViewButton.end_session()) + menu.add_button( + ViewButton(style=discord.ButtonStyle.secondary, custom_id=ViewButton.ID_GO_TO_FIRST_PAGE, emoji="⏮️"), + ) + menu.add_button( + ViewButton(style=discord.ButtonStyle.secondary, custom_id=ViewButton.ID_PREVIOUS_PAGE, emoji="⏪"), + ) + menu.add_button(ViewButton(style=discord.ButtonStyle.secondary, custom_id=ViewButton.ID_NEXT_PAGE, emoji="⏩")) + menu.add_button( + ViewButton(style=discord.ButtonStyle.secondary, custom_id=ViewButton.ID_GO_TO_LAST_PAGE, emoji="⏭️"), + ) await menu.start() @@ -402,12 +409,14 @@ class Cases(ModerationCogBase): case_action_emoji: str, ) -> str: case_type_and_action = ( - f"{case_action_emoji} {case_type_emoji}" if case_action_emoji and case_type_emoji else "?" + f"{case_action_emoji} {case_type_emoji}" + if case_action_emoji and case_type_emoji + else ":interrobang: :interrobang:" ) - case_date = discord.utils.format_dt(case.case_created_at, "R") if case.case_created_at else "?" + case_date = discord.utils.format_dt(case.case_created_at, "R") if case.case_created_at else ":interrobang:" case_number = f"{case.case_number:04d}" - return f"{case_status_emoji} `{case_number}`\u2002\u2002 {case_type_and_action} \u2002\u2002*{case_date}*\n" + return f"{case_status_emoji} `{case_number}`\u2002\u2002 {case_type_and_action} \u2002\u2002__{case_date}__\n" def _add_case_to_embed(self, embed: discord.Embed, case: Case) -> None: case_status_emoji = self._format_emoji(self._get_case_status_emoji(case.case_status)) From d2607f5774408bc7ecf5068e81c5545e855f05eb Mon Sep 17 00:00:00 2001 From: kzndotsh Date: Sat, 24 Aug 2024 19:53:39 +0000 Subject: [PATCH 80/84] feat: add pull request template to guide contributors on PR submission --- .github/PULL_REQUEST_TEMPLATE.md | 40 ++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 .github/PULL_REQUEST_TEMPLATE.md diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..de4ddeb --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,40 @@ +# Pull Request Template + +## Description + +Please include a summary of the changes and the related issue. Please also include relevant motivation and context. List any dependencies that are required for this change. + +Fixes # (issue) + +## Type of Change + +- [ ] Bug fix (non-breaking change which fixes an issue) +- [ ] New feature (non-breaking change which adds functionality) +- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) +- [ ] Documentation update + +## Checklist + +- [ ] My code follows the style guidelines of this project +- [ ] I have performed a self-review of my own code +- [ ] I have commented my code, particularly in hard-to-understand areas +- [ ] I have made corresponding changes to the documentation +- [ ] My changes generate no new warnings +- [ ] I have added tests that prove my fix is effective or that my feature works +- [ ] New and existing unit tests pass locally with my changes +- [ ] Any dependent changes have been merged and published in downstream modules + +## How Has This Been Tested? + +Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration. + +- [ ] Test A +- [ ] Test B + +## Screenshots (if applicable) + +Please add screenshots to help explain your changes. + +## Additional Information + +Please add any other information that is important to this PR. From b99d04c2097818a9edbe29d325b0c7e6bbd32688 Mon Sep 17 00:00:00 2001 From: kzndotsh Date: Sat, 24 Aug 2024 15:54:13 -0400 Subject: [PATCH 81/84] Update issue templates --- .github/ISSUE_TEMPLATE/bug_report.md | 38 +++++++++++++++++++++++ .github/ISSUE_TEMPLATE/feature_request.md | 20 ++++++++++++ 2 files changed, 58 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..dd84ea7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,38 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '' +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Desktop (please complete the following information):** + - OS: [e.g. iOS] + - Browser [e.g. chrome, safari] + - Version [e.g. 22] + +**Smartphone (please complete the following information):** + - Device: [e.g. iPhone6] + - OS: [e.g. iOS8.1] + - Browser [e.g. stock browser, safari] + - Version [e.g. 22] + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..bbcbbe7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: '' +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. From fd19185f3f4796b20db654e73c4fe118aac7dbbd Mon Sep 17 00:00:00 2001 From: kzndotsh Date: Sat, 24 Aug 2024 19:56:07 +0000 Subject: [PATCH 82/84] docs: update bug report template for better issue reporting - Change the title and labels to be more descriptive and automatically label bugs - Improve the structure and clarity of the issue template - Add a section for code snippets or screenshots - Update the environment section to be more relevant to the project - Add a section for additional context to provide more information about the issue --- .github/ISSUE_TEMPLATE/bug_report.md | 40 +++++++++++++++------------- 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index dd84ea7..b01c646 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -1,38 +1,42 @@ --- -name: Bug report +name: Bug Report about: Create a report to help us improve -title: '' -labels: '' +title: "[BUG] - " +labels: type: bug assignees: '' --- -**Describe the bug** +## Describe the Bug + A clear and concise description of what the bug is. -**To Reproduce** +## To Reproduce + Steps to reproduce the behavior: + 1. Go to '...' 2. Click on '....' 3. Scroll down to '....' 4. See error -**Expected behavior** +Please include code snippets or screenshots where applicable. + +## Expected Behavior + A clear and concise description of what you expected to happen. -**Screenshots** +## Screenshots + If applicable, add screenshots to help explain your problem. -**Desktop (please complete the following information):** - - OS: [e.g. iOS] - - Browser [e.g. chrome, safari] - - Version [e.g. 22] +## Environment (please complete the following information) -**Smartphone (please complete the following information):** - - Device: [e.g. iPhone6] - - OS: [e.g. iOS8.1] - - Browser [e.g. stock browser, safari] - - Version [e.g. 22] +- OS: [e.g. Ubuntu 20.04] +- Python version: [e.g. Python 3.12] +- Discord.py version: [e.g. 2.0.0] +- Tux version: [e.g. v1.0.0] -**Additional context** -Add any other context about the problem here. +## Additional Context + +Add any other context about the problem here, such as logs, configuration files, etc. From 8b7db8789989bb1cc702af39600d84c63c7fc290 Mon Sep 17 00:00:00 2001 From: kzndotsh Date: Sat, 24 Aug 2024 19:58:12 +0000 Subject: [PATCH 83/84] style(feature_request.md): improve readability and structure of feature request template feat(feature_request.md): add prefix "[FEATURE] - " to title for better issue identification feat(feature_request.md): add label "type: feature-request" for better issue categorization --- .github/ISSUE_TEMPLATE/feature_request.md | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index bbcbbe7..2884381 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -1,20 +1,24 @@ --- -name: Feature request +name: Feature Request about: Suggest an idea for this project -title: '' -labels: '' +title: "[FEATURE] - " +labels: type: feature-request assignees: '' --- -**Is your feature request related to a problem? Please describe.** +## Is your feature request related to a problem? Please describe + A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] -**Describe the solution you'd like** +## Describe the solution you'd like + A clear and concise description of what you want to happen. -**Describe alternatives you've considered** +## Describe alternatives you've considered + A clear and concise description of any alternative solutions or features you've considered. -**Additional context** +## Additional Context + Add any other context or screenshots about the feature request here. From 87b413fb187382202680c013489dc6418aaf371e Mon Sep 17 00:00:00 2001 From: kzndotsh Date: Sat, 24 Aug 2024 20:00:25 +0000 Subject: [PATCH 84/84] fix(.github/ISSUE_TEMPLATE): wrap labels in quotes for bug_report.md and feature_request.md to ensure correct label assignment --- .github/ISSUE_TEMPLATE/bug_report.md | 2 +- .github/ISSUE_TEMPLATE/feature_request.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index b01c646..60e8c36 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -2,7 +2,7 @@ name: Bug Report about: Create a report to help us improve title: "[BUG] - " -labels: type: bug +labels: "type: bug" assignees: '' --- diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 2884381..5e84edd 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -2,7 +2,7 @@ name: Feature Request about: Suggest an idea for this project title: "[FEATURE] - " -labels: type: feature-request +labels: "type: feature-request" assignees: '' ---