├── .archive
├── COG_STANDARDS.md
├── EMBED_STANDARDS.md
├── EMBED_USAGE.md
├── EVENT_STANDARDS.md
├── PROJECT_STRUCTURE.md
├── architecture.md
├── console.py
├── core.md
├── development.md
├── discordpy.md
├── emojistats.py
├── environment.md
├── error_handler.py
├── export.py
├── exports.py
├── ghost_pings.py
├── guide.py
├── guild.py
├── logging
│ ├── __init__.py
│ ├── audit.py
│ ├── commands.py
│ ├── gate.py
│ ├── member.py
│ └── message.py
├── message.py
├── mkdocstrings copy.css
├── mod.py
├── neofetch.py
├── notes.py
├── on_member_update.py
├── on_message.py
├── roles.py
├── services.md
├── snippets.py
├── starboard
│ ├── __init__.py
│ └── image_gen.py
├── test_error_handler.py
├── tests
│ ├── __init__.py
│ ├── test_EventHandler.py
│ └── test_permissions.py
├── thread.py
├── tools.py
└── voice.py
├── .cursor
└── rules
│ ├── cli_usage.mdc
│ ├── core.mdc
│ ├── database_patterns.mdc
│ ├── development_setup.mdc
│ ├── docker_environment.mdc
│ ├── extensions_system.mdc
│ └── project_structure.mdc
├── .devcontainer
└── devcontainer.json
├── .dockerignore
├── .env.example
├── .github
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ └── feature_request.md
├── PULL_REQUEST_TEMPLATE.md
├── SECURITY.md
├── SUPPORT.md
├── renovate.json
└── workflows
│ ├── codeql.yml
│ ├── docker-image.yml
│ ├── linting.yml
│ ├── pyright.yml
│ ├── remove-old-images.yml
│ └── todo.yml
├── .gitignore
├── .markdownlint.yaml
├── .mise.toml
├── .pre-commit-config.yaml
├── .python-version
├── .vscode
├── extensions.json
└── settings.json
├── DEVELOPER.md
├── Dockerfile
├── LICENSE.md
├── README.md
├── assets
├── badges
│ ├── 100k-messages.png
│ ├── 10k-messages.png
│ ├── 500h-voice.png
│ ├── 50k-messages.png
│ ├── day1-joiner.png
│ ├── former-staff.png
│ ├── helpful.png
│ ├── lucky.png
│ ├── top50-text.png
│ └── tux-dev.png
├── branding
│ ├── avatar.png
│ └── tux.gif
├── embeds
│ ├── active_case.png
│ └── inactive_case.png
├── emojis
│ ├── active_case.png
│ ├── added.png
│ ├── ban.png
│ ├── inactive_case.png
│ ├── jail.png
│ ├── kick.png
│ ├── removed.png
│ ├── snippetban.png
│ ├── snippetunban.png
│ ├── tempban.png
│ ├── timeout.png
│ ├── tux_case.png
│ ├── tux_default.png
│ ├── tux_error.png
│ ├── tux_info.png
│ ├── tux_note.png
│ ├── tux_notify.png
│ ├── tux_poll.png
│ ├── tux_prefix.png
│ ├── tux_success.png
│ ├── tux_tag.png
│ └── warn.png
└── roles
│ ├── de-wm
│ ├── Cinnamon.png
│ ├── awesome.png
│ ├── berrywm.png
│ ├── bspwm.png
│ ├── budgie.png
│ ├── dwm.png
│ ├── enlightenment.png
│ ├── exwm.png
│ ├── gnome.png
│ ├── herbsluft.png
│ ├── hyprland.png
│ ├── i3.png
│ ├── ice_wm.png
│ ├── jwm.png
│ ├── kde_plasma.png
│ ├── left_wm.png
│ ├── lx_qt.png
│ ├── mate.png
│ ├── openbox.png
│ ├── qtile.png
│ ├── river.png
│ ├── stump_wm.png
│ ├── sway_wm.png
│ ├── wayfire.png
│ ├── xfce.png
│ └── xmonad.png
│ ├── distro
│ ├── alpine.png
│ ├── anti_x.png
│ ├── antix.png
│ ├── arch.png
│ ├── arco.png
│ ├── artix.png
│ ├── asahi_linux.png
│ ├── bazzite.png
│ ├── bedrock.png
│ ├── cachy.png
│ ├── chimera.png
│ ├── debian.png
│ ├── deepin.png
│ ├── devuan.png
│ ├── endeavour.png
│ ├── exherbo.png
│ ├── fedora.png
│ ├── free_bsd.png
│ ├── garuda.png
│ ├── gentoo.png
│ ├── haiku.png
│ ├── kiss.png
│ ├── lfs.png
│ ├── mac_os.png
│ ├── manjaro.png
│ ├── mint.png
│ ├── mx.png
│ ├── net_bsd.png
│ ├── nixos.png
│ ├── nobara.png
│ ├── open_bsd.png
│ ├── opensuse.png
│ ├── plan_9.png
│ ├── popos.png
│ ├── puppy.png
│ ├── qubes.png
│ ├── redhat.png
│ ├── rocky_linux.png
│ ├── slackware.png
│ ├── solus.png
│ ├── ubuntu.png
│ ├── ubuntu_mate.png
│ ├── vanilla.png
│ ├── void.png
│ ├── windows.png
│ └── zorin.png
│ ├── donor-icons
│ ├── donor.png
│ ├── mega-donor.png
│ └── super-donor.png
│ ├── langs
│ ├── asm.png
│ ├── bash.png
│ ├── c.png
│ ├── c_sharp.png
│ ├── clojure.png
│ ├── cpp.png
│ ├── crystal.png
│ ├── dart.png
│ ├── elixr.png
│ ├── erlang.png
│ ├── gd_script.png
│ ├── go.png
│ ├── haskell.png
│ ├── html_css.png
│ ├── java.png
│ ├── js.png
│ ├── julia.png
│ ├── kotlin.png
│ ├── lisp.png
│ ├── lua.png
│ ├── nim.png
│ ├── o_caml.png
│ ├── perl.png
│ ├── php.png
│ ├── python.png
│ ├── r.png
│ ├── ruby.png
│ ├── rust.png
│ ├── sh_script.png
│ ├── swift.png
│ ├── vala.png
│ └── zig.png
│ └── text-editors
│ ├── ed.png
│ ├── emacs.png
│ ├── helix.png
│ ├── jetbrains.png
│ ├── kakoune.png
│ ├── kate.png
│ ├── micro.png
│ ├── nano.png
│ ├── neovim.png
│ └── vs_code.png
├── config
└── settings.yml.example
├── docker-compose.dev.yml
├── docker-compose.yml
├── docs
├── content
│ ├── assets
│ │ ├── images
│ │ │ └── logo.png
│ │ └── stylesheets
│ │ │ ├── extra.css
│ │ │ └── mkdocstrings.css
│ ├── dev
│ │ ├── cli
│ │ │ └── index.md
│ │ ├── contributing.md
│ │ ├── database.md
│ │ ├── database_patterns.md
│ │ ├── docker_development.md
│ │ ├── local_development.md
│ │ └── permissions.md
│ └── index.md
├── mkdocs.yml
└── overrides
│ └── python
│ └── material
│ └── function.html
├── flake.lock
├── flake.nix
├── poetry.lock
├── poetry.toml
├── prisma
└── schema
│ ├── commands
│ ├── afk.prisma
│ ├── moderation.prisma
│ ├── reminder.prisma
│ └── snippets.prisma
│ ├── guild
│ ├── config.prisma
│ ├── guild.prisma
│ ├── levels.prisma
│ └── starboard.prisma
│ └── main.prisma
├── pyproject.toml
├── shell.nix
├── tux
├── __init__.py
├── app.py
├── bot.py
├── cli
│ ├── README.md
│ ├── __init__.py
│ ├── core.py
│ ├── database.py
│ ├── dev.py
│ ├── docker.py
│ ├── docs.py
│ └── ui.py
├── cog_loader.py
├── cogs
│ ├── __init__.py
│ ├── admin
│ │ ├── __init__.py
│ │ ├── dev.py
│ │ ├── eval.py
│ │ ├── git.py
│ │ ├── mail.py
│ │ └── mock.py
│ ├── fun
│ │ ├── __init__.py
│ │ ├── fact.py
│ │ ├── imgeffect.py
│ │ ├── rand.py
│ │ └── xkcd.py
│ ├── guild
│ │ ├── __init__.py
│ │ ├── config.py
│ │ ├── rolecount.py
│ │ └── setup.py
│ ├── info
│ │ ├── __init__.py
│ │ ├── avatar.py
│ │ ├── info.py
│ │ └── membercount.py
│ ├── levels
│ │ ├── __init__.py
│ │ ├── level.py
│ │ └── levels.py
│ ├── moderation
│ │ ├── __init__.py
│ │ ├── ban.py
│ │ ├── cases.py
│ │ ├── clearafk.py
│ │ ├── jail.py
│ │ ├── kick.py
│ │ ├── pollban.py
│ │ ├── pollunban.py
│ │ ├── purge.py
│ │ ├── report.py
│ │ ├── slowmode.py
│ │ ├── snippetban.py
│ │ ├── snippetunban.py
│ │ ├── tempban.py
│ │ ├── timeout.py
│ │ ├── unban.py
│ │ ├── unjail.py
│ │ ├── untimeout.py
│ │ └── warn.py
│ ├── services
│ │ ├── __init__.py
│ │ ├── bookmarks.py
│ │ ├── gif_limiter.py
│ │ ├── influxdblogger.py
│ │ ├── levels.py
│ │ ├── starboard.py
│ │ ├── status_roles.py
│ │ ├── temp_vc.py
│ │ └── tty_roles.py
│ ├── snippets
│ │ ├── __init__.py
│ │ ├── create_snippet.py
│ │ ├── delete_snippet.py
│ │ ├── edit_snippet.py
│ │ ├── get_snippet.py
│ │ ├── get_snippet_info.py
│ │ ├── list_snippets.py
│ │ └── toggle_snippet_lock.py
│ ├── tools
│ │ ├── __init__.py
│ │ ├── tldr.py
│ │ └── wolfram.py
│ └── utility
│ │ ├── __init__.py
│ │ ├── afk.py
│ │ ├── ping.py
│ │ ├── poll.py
│ │ ├── remindme.py
│ │ ├── run.py
│ │ ├── self_timeout.py
│ │ ├── timezones.py
│ │ └── wiki.py
├── database
│ ├── __init__.py
│ ├── client.py
│ └── controllers
│ │ ├── __init__.py
│ │ ├── afk.py
│ │ ├── base.py
│ │ ├── case.py
│ │ ├── guild.py
│ │ ├── guild_config.py
│ │ ├── levels.py
│ │ ├── note.py
│ │ ├── reminder.py
│ │ ├── snippet.py
│ │ └── starboard.py
├── extensions
│ └── README.md
├── handlers
│ ├── __init__.py
│ ├── activity.py
│ ├── error.py
│ ├── event.py
│ └── sentry.py
├── help.py
├── main.py
├── ui
│ ├── __init__.py
│ ├── buttons.py
│ ├── embeds.py
│ ├── help_components.py
│ ├── modals
│ │ ├── __init__.py
│ │ └── report.py
│ └── views
│ │ ├── __init__.py
│ │ ├── config.py
│ │ ├── confirmation.py
│ │ └── tldr.py
├── utils
│ ├── __init__.py
│ ├── ascii.py
│ ├── banner.py
│ ├── checks.py
│ ├── config.py
│ ├── constants.py
│ ├── converters.py
│ ├── emoji.py
│ ├── env.py
│ ├── exceptions.py
│ ├── flags.py
│ ├── functions.py
│ ├── help_utils.py
│ ├── hot_reload.py
│ ├── logger.py
│ ├── regex.py
│ └── sentry.py
└── wrappers
│ ├── __init__.py
│ ├── github.py
│ ├── godbolt.py
│ ├── tldr.py
│ ├── wandbox.py
│ └── xkcd.py
└── typings
├── emojis
├── __init__.pyi
├── db
│ ├── __init__.pyi
│ ├── db.pyi
│ └── utils.pyi
└── emojis.pyi
└── reactionmenu
├── __init__.pyi
├── abc.pyi
├── buttons.pyi
├── core.pyi
├── decorators.pyi
├── errors.pyi
└── views_menu.pyi
/.archive/EVENT_STANDARDS.md:
--------------------------------------------------------------------------------
1 | # Event Listeners
2 |
3 | Event listeners are a crucial part of creating dynamic and responsive cogs for the Discord bot. They enable cogs to react automatically to various events within the Discord ecosystem, such as messages being sent, edited, deleted, or users joining/leaving a server.
4 |
5 | ## Implementing Event Listeners
6 |
7 | - To implement an event listener within a cog, use the `@commands.Cog.listener()` decorator above an asynchronous method.
8 | - The method name typically reflects the event it handles (e.g., `on_message_delete` for handling message deletions).
9 | - Each event listener method should accept `self` and the event-specific parameters (e.g., `message: discord.Message` for `on_message_delete`).
10 |
11 | ### Example:
12 | ```python
13 | class MyCog(commands.Cog):
14 | def __init__(self, bot: commands.Bot) -> None:
15 | self.bot = bot
16 |
17 | @commands.Cog.listener()
18 | async def on_message_delete(self, message: discord.Message) -> None:
19 | # Your code here to handle the event
20 | ```
21 |
22 | ## Best Practices
23 |
24 | - **Logging**: Use `logger` to log events handled by your listeners for easier debugging and monitoring.
25 | - **Check conditions**: Before executing your logic, check relevant conditions. For example, ignore bot messages to prevent unnecessary processing.
26 | - **Use Embeds for Rich Responses**: If your listener responds in a channel, consider using embeds to make your messages more informative and visually appealing.
27 | - **Permissions and Error Handling**: Ensure your bot has the required permissions for any actions it tries to undertake in response to the event and gracefully handle any exceptions that may occur to prevent the bot from crashing.
28 |
29 | ### Handling Permissions Example:
30 | ```python
31 | @commands.Cog.listener()
32 | async def on_member_join(self, member: discord.Member) -> None:
33 | try:
34 | # Attempt to assign a role or send a welcome message
35 | except discord.Forbidden:
36 | logger.error(f"Missing permissions to act on member join in {member.guild.name}.")
37 | ```
38 |
39 | ## Example Use Cases
40 |
41 | ### Monitoring Ghost Pings:
42 | Listen for deleted messages containing pings and notify the channel, providing visibility into ghost pinging activities.
43 |
44 | ### Welcoming Members:
45 | Automatically assign roles or send a welcome message when new members join the server, enhancing the onboarding experience.
46 |
47 | ### Custom Role Management:
48 | Dynamically manage roles based on server events, such as creating unique roles for the Nth user to join or modifying roles based on member activities.
49 |
50 | By implementing event listeners within your cogs, you contribute to creating an engaging, interactive, and well-moderated Discord community. Always consider the user experience and server performance when designing your listeners, and adhere to Discord's guidelines and rate limits.
--------------------------------------------------------------------------------
/.archive/architecture.md:
--------------------------------------------------------------------------------
1 | # Architecture Overview
2 |
3 | This document provides a high-level overview of Tux's architecture and design principles.
4 |
5 | ## Structure
6 |
7 | ```bash
8 | .
9 | ├── tux/
10 | │ ├── main.py
11 | │ ├── bot.py
12 | │ ├── cog_loader.py
13 | │ ├── help.py
14 | │ ├── cogs
15 | │ ├── database
16 | │ ├── handlers
17 | │ ├── ui
18 | │ ├── utils
19 | │ └── wrappers
20 | ```
21 |
22 | ### Key Components
23 |
24 | #### `main.py` - The main entry point for the bot
25 |
26 | #### `bot.py` - The main bot class
27 |
28 | #### `cog_loader.py` - The cog loader class
29 |
30 | #### `help.py` - The help command class
31 |
32 | #### `cogs` - The directory for all cogs
33 |
34 | #### `database` - The directory for all database controllers and client
35 |
36 | #### `handlers`
37 |
38 | The directory for various services and handlers that live "behind the scenes" of the bot.
39 |
40 | #### `ui`
41 |
42 | The directory for all UI components and views.
43 |
44 | #### `utils`
45 |
46 | The directory for all utility functions and classes.
47 |
48 | **Important utilities to note:**
49 |
50 | - `logger.py` - Our custom logger that overrides the default logger with `loguru` and `rich` for better logging.
51 | - `config.py` - Our configuration manager that handles all bot configuration and secret/environment variables.
52 | - `constants.py` - Our constants manager that handles all constant values used throughout the bot like colors, emojis, etc.
53 | - `checks.py` - Our custom permission system that provides decorators for command access checks.
54 | - `flags.py` - Our custom flag system that provides a way to handle flags for command arguments.
55 |
56 | #### `wrappers`
57 |
58 | The directory for all API wrappers and clients.
59 |
60 | - We use `httpx` for all API requests.
61 |
--------------------------------------------------------------------------------
/.archive/environment.md:
--------------------------------------------------------------------------------
1 | # Getting Started
2 |
3 | This guide will help you get started with setting up your development environment for Tux, and getting the bot running on your local machine.
4 |
5 | ## Prerequisites
6 |
7 | - Python 3.13.2+
8 | - Poetry 2.0.1+
9 |
10 | ## Installation
11 |
12 | 1. Clone the repository
13 |
14 | ```bash
15 | git clone https://github.com/allthingslinux/tux.git && cd tux
16 | ```
17 |
18 | 2. Set and activate the virtual environment
19 |
20 | ```bash
21 | poetry env use 3.13
22 | ```
23 |
24 | > Learn more about managing and activating virtual environments here:
25 |
26 | 3. Install dependencies
27 |
28 | ```bash
29 | poetry install --with dev, docs
30 | ```
31 |
32 | 4. Install the pre-commit hooks
33 |
34 | We use pre-commit to ensure code quality, secrets protection, and more.
35 |
36 | ```bash
37 | poetry run pre-commit install
38 | ```
39 |
40 | 5. Set up the environment variables
41 |
42 | ```bash
43 | cp .env.example .env
44 | ```
45 |
46 | 6. Set up the configuration settings
47 |
48 | ```bash
49 | cp config/settings.yml.example config/settings.yml
50 | ```
51 |
52 | 7. Generate the Prisma client
53 |
54 | ```bash
55 | poetry run db-generate
56 | ```
57 |
58 | 8. Push the database schema to the database
59 |
60 | ```bash
61 | poetry run db-push
62 | ```
63 |
64 | 9. Run the bot
65 |
66 | ```bash
67 | poetry run start
68 | ```
69 |
70 | 10. Sync the bot command tree
71 |
72 | Assuming your bot prefix is `$`, you can sync the command tree in Discord with the following command:
73 |
74 | ```bash
75 | $dev sync
76 | ```
77 |
--------------------------------------------------------------------------------
/.archive/ghost_pings.py:
--------------------------------------------------------------------------------
1 | import discord
2 | from discord.ext import commands
3 | from loguru import logger
4 |
5 |
6 | class GhostPings(commands.Cog):
7 | def __init__(self, bot: commands.Bot) -> None:
8 | self.bot = bot
9 |
10 | @commands.Cog.listener()
11 | async def on_message_delete(self, message: discord.Message) -> None:
12 | logger.trace(f"Message deleted: {message}")
13 |
14 | # if sender is a bot, ignore, some bots delete their own messages
15 | if message.author.bot:
16 | return
17 |
18 | # check if message has a ping (role, user, etc.)
19 | if message.mentions or message.role_mentions:
20 | if (
21 | len(message.mentions) == 1
22 | and (message.mentions[0] == message.author)
23 | or (message.mentions[0].bot)
24 | ):
25 | return
26 |
27 | # embed = discord.Embed(
28 | # title="Ghost Ping!", color=discord.Color.red(), timestamp=message.created_at
29 | # )
30 |
31 | # embed.description = f"{message.author.mention} pinged: {', '.join([mention.mention for mention in message.mentions])} {', '.join([role.mention for role in message.role_mentions])}"
32 |
33 | # await message.channel.send(embed=embed)
34 |
35 | await message.channel.send(
36 | f"{message.author.mention} ghost pinged: {', '.join([mention.mention for mention in message.mentions])} {', '.join([role.mention for role in message.role_mentions])} 👻",
37 | allowed_mentions=discord.AllowedMentions.none(),
38 | )
39 |
40 |
41 | async def setup(bot: commands.Bot) -> None:
42 | await bot.add_cog(GhostPings(bot))
43 |
--------------------------------------------------------------------------------
/.archive/guide.py:
--------------------------------------------------------------------------------
1 | import discord
2 | from discord import app_commands
3 | from discord.ext import commands
4 | from loguru import logger
5 |
6 | from tux.utils.embeds import EmbedCreator
7 |
8 | meta_fields = [
9 | "<#1172252854371749958>",
10 | "<#1172343581495795752>",
11 | "<#1172259762893754480>",
12 | "<#1193304492226129971>",
13 | ]
14 |
15 | support_fields = ["<#1172312602181902357>", "<#1172312653797007461>", "<#1172312674298761216>"]
16 |
17 | resources_fields = [
18 | "<#1221117147091304548>",
19 | "<#1221115462549504060>",
20 | "<#1174251004586381323>",
21 | "<#1174742125036961863>",
22 | "<#1220004498789896253>",
23 | ]
24 |
25 |
26 | class Guide(commands.Cog):
27 | def __init__(self, bot: commands.Bot) -> None:
28 | self.bot = bot
29 |
30 | @app_commands.command(name="guide", description="See useful channels for the server.")
31 | async def guide(self, interaction: discord.Interaction) -> None:
32 | """
33 | Send a guide to the server with useful channels and resources.
34 |
35 | Parameters
36 | ----------
37 | interaction : discord.Interaction
38 | The discord interaction object.
39 | """
40 |
41 | guild = interaction.guild
42 |
43 | if not guild:
44 | await interaction.response.send_message(
45 | "This command can only be used in a server.",
46 | ephemeral=True,
47 | )
48 | return
49 |
50 | embed = EmbedCreator.create_info_embed(
51 | title="Server Guide",
52 | description=f"Welcome to {guild.name}!",
53 | interaction=interaction,
54 | )
55 |
56 | embed.add_field(name="Meta", value="\n".join(meta_fields), inline=False)
57 | embed.add_field(name="Support", value="\n".join(support_fields), inline=False)
58 | embed.add_field(name="Resources", value="\n".join(resources_fields), inline=False)
59 |
60 | embed.set_thumbnail(url=guild.icon)
61 |
62 | await interaction.response.send_message(embed=embed)
63 |
64 | logger.info(f"{interaction.user} used the guide command in {interaction.channel}.")
65 |
66 |
67 | async def setup(bot: commands.Bot) -> None:
68 | await bot.add_cog(Guide(bot))
69 |
--------------------------------------------------------------------------------
/.archive/guild.py:
--------------------------------------------------------------------------------
1 |
2 | from discord.ext import commands
3 |
4 |
5 | class GuildEventsCog(commands.Cog, name="Guild Events Handler"):
6 | def __init__(self, bot: commands.Bot) -> None:
7 | self.bot = bot
8 |
9 | # @commands.Cog.listener()
10 | # async def on_guild_channel_create(self, channel: discord.abc.GuildChannel) -> None:
11 | # logger.trace(f"{channel} has been created in {channel.guild}.")
12 |
13 | # @commands.Cog.listener()
14 | # async def on_guild_channel_delete(self, channel: discord.abc.GuildChannel) -> None:
15 | # logger.trace(f"{channel} has been deleted in {channel.guild}.")
16 |
17 | # @commands.Cog.listener()
18 | # async def on_guild_channel_pins_update(
19 | # self,
20 | # channel: discord.abc.GuildChannel | discord.Thread,
21 | # last_pin: datetime | None,
22 | # ) -> None:
23 | # logger.trace(f"Pins in #{channel.name} have been updated. Last pin: {last_pin}")
24 |
25 | # @commands.Cog.listener()
26 | # async def on_guild_channel_update(
27 | # self, before: discord.abc.GuildChannel, after: discord.abc.GuildChannel
28 | # ) -> None:
29 | # logger.trace(f"Channel updated: {before} -> {after}")
30 |
31 | # @commands.Cog.listener()
32 | # async def on_guild_role_create(self, role: discord.Role) -> None:
33 | # logger.trace(f"Role created: {role}")
34 |
35 | # @commands.Cog.listener()
36 | # async def on_guild_role_delete(self, role: discord.Role) -> None:
37 | # logger.trace(f"Role deleted: {role}")
38 |
39 | # @commands.Cog.listener()
40 | # async def on_guild_role_update(self, before: discord.Role, after: discord.Role) -> None:
41 | # logger.trace(f"Role updated: {before} -> {after}")
42 |
43 | # @commands.Cog.listener()
44 | # async def on_guild_update(self, before: discord.Guild, after: discord.Guild) -> None:
45 | # logger.trace(f"Guild updated: {before} -> {after}")
46 |
47 |
48 | async def setup(bot: commands.Bot) -> None:
49 | await bot.add_cog(GuildEventsCog(bot))
50 |
--------------------------------------------------------------------------------
/.archive/logging/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/.archive/logging/__init__.py
--------------------------------------------------------------------------------
/.archive/logging/commands.py:
--------------------------------------------------------------------------------
1 | import discord
2 | from discord.ext import commands
3 | from loguru import logger
4 |
5 | from tux.utils.constants import Constants as CONST
6 | from tux.utils.embeds import EmbedCreator
7 |
8 |
9 | class CommandEventsCog(commands.Cog, name="Command Events Handler"):
10 | def __init__(self, bot: commands.Bot) -> None:
11 | self.bot = bot
12 | self.dev_log_channel_id: int = CONST.LOG_CHANNELS["DEV"]
13 |
14 | # @commands.Cog.listener()
15 | # async def on_app_command_completion(
16 | # self,
17 | # interaction: discord.Interaction,
18 | # command: discord.app_commands.AppCommand,
19 | # ) -> None:
20 | # logger.info(f"Command {command.name} completed by {interaction.user}")
21 |
22 | # dev_log_channel = self.bot.get_channel(self.dev_log_channel_id)
23 |
24 | # if isinstance(dev_log_channel, discord.TextChannel):
25 | # embed = EmbedCreator.create_log_embed(
26 | # title="Command Completed",
27 | # description=f"Command `{command.name}` completed by {interaction.user.mention}.",
28 | # )
29 |
30 | # await dev_log_channel.send(embed=embed)
31 |
32 | # @commands.Cog.listener()
33 | # async def on_command_completion(self, ctx: commands.Context[commands.Bot]) -> None:
34 | # if ctx.command is not None:
35 | # logger.info(f"Command {ctx.command.name} completed by {ctx.author}")
36 |
37 | # dev_log_channel = self.bot.get_channel(self.dev_log_channel_id)
38 |
39 | # if isinstance(dev_log_channel, discord.TextChannel):
40 | # embed = EmbedCreator.create_log_embed(
41 | # title="Command Completed",
42 | # description=f"Command `{ctx.command.name}` completed by {ctx.author.mention}.",
43 | # )
44 |
45 | # await dev_log_channel.send(embed=embed)
46 |
47 |
48 | async def setup(bot: commands.Bot) -> None:
49 | await bot.add_cog(CommandEventsCog(bot))
50 |
--------------------------------------------------------------------------------
/.archive/logging/member.py:
--------------------------------------------------------------------------------
1 | import discord
2 | from discord.ext import commands
3 |
4 | from tux.utils.constants import Constants as CONST
5 | from tux.utils.embeds import EmbedCreator
6 |
7 |
8 | class MemberLogging(commands.Cog):
9 | def __init__(self, bot: commands.Bot):
10 | self.bot = bot
11 | self.audit_log_channel_id: int = CONST.LOG_CHANNELS["AUDIT"]
12 |
13 | async def send_to_audit_log(self, embed: discord.Embed):
14 | channel = self.bot.get_channel(self.audit_log_channel_id)
15 | if isinstance(channel, discord.TextChannel):
16 | await channel.send(embed=embed)
17 |
18 | @commands.Cog.listener()
19 | async def on_member_update(self, before: discord.Member, after: discord.Member) -> None:
20 | """
21 | When a member is updated
22 |
23 | Parameters
24 | ----------
25 | before : discord.Member
26 | The member before the update.
27 | after : discord.Member
28 | The member after the update.
29 | """
30 |
31 | embed = EmbedCreator.create_log_embed(
32 | title="Member Updated",
33 | description=f"Member {before.mention} has been updated.",
34 | )
35 |
36 | if before.name != after.name:
37 | embed.add_field(name="Name", value=f"`{before.name}` -> `{after.name}`")
38 |
39 | if before.display_name != after.display_name:
40 | embed.add_field(
41 | name="Display Name",
42 | value=f"`{before.display_name}` -> `{after.display_name}`",
43 | )
44 |
45 | if before.global_name != after.global_name:
46 | embed.add_field(
47 | name="Global Name",
48 | value=f"`{before.global_name}` -> `{after.global_name}`",
49 | )
50 |
51 | await self.send_to_audit_log(embed)
52 |
53 |
54 | async def setup(bot: commands.Bot) -> None:
55 | await bot.add_cog(MemberLogging(bot))
56 |
--------------------------------------------------------------------------------
/.archive/logging/message.py:
--------------------------------------------------------------------------------
1 | import discord
2 | from discord.ext import commands
3 |
4 | from tux.utils.constants import Constants as CONST
5 | from tux.utils.embeds import EmbedCreator
6 |
7 |
8 | class GuildLogging(commands.Cog):
9 | def __init__(self, bot: commands.Bot):
10 | self.bot = bot
11 | self.dev_logs_channel_id: int = CONST.LOG_CHANNELS["DEV"]
12 |
13 | async def send_to_dev_log(self, embed: discord.Embed):
14 | channel = self.bot.get_channel(self.dev_logs_channel_id)
15 | if isinstance(channel, discord.TextChannel):
16 | await channel.send(embed=embed)
17 |
18 | @commands.Cog.listener()
19 | async def on_message(self, message: discord.Message) -> None:
20 | """
21 | When a message is sent in a guild
22 |
23 | Parameters
24 | ----------
25 | message : discord.Message
26 | The message that was sent.
27 | """
28 |
29 | poll_channel = self.bot.get_channel(1228717294788673656) # TODO: stop hardcoding this
30 |
31 | if message.channel == poll_channel:
32 | # delete non-poll messages in the poll channel
33 | if message.poll is None:
34 | await message.delete()
35 | embed = EmbedCreator.create_log_embed(
36 | title="Non-Poll Deleted",
37 | description=f"Message: {message.id}",
38 | )
39 | await self.send_to_dev_log(embed)
40 | return
41 |
42 | # make a thread for the poll
43 | await message.create_thread(
44 | name=f"Poll by {message.author.display_name}",
45 | reason="Poll thread",
46 | )
47 | return
48 |
49 | if not message.embeds and not message.attachments and not message.content and not message.stickers:
50 | # check if the message is not a message
51 | if message.type != discord.MessageType.default:
52 | return
53 | # delete the message and log it
54 | await message.delete()
55 | embed = EmbedCreator.create_log_embed(
56 | title="Poll Deleted",
57 | description=f"Message: {message.id}",
58 | )
59 | await self.send_to_dev_log(embed)
60 |
61 |
62 | async def setup(bot: commands.Bot) -> None:
63 | await bot.add_cog(GuildLogging(bot))
64 |
--------------------------------------------------------------------------------
/.archive/message.py:
--------------------------------------------------------------------------------
1 | from discord.ext import commands
2 |
3 |
4 | class MessageEventsCog(commands.Cog, name="Message Events Handler"):
5 | def __init__(self, bot: commands.Bot) -> None:
6 | self.bot = bot
7 |
8 | # @commands.Cog.listener()
9 | # async def on_bulk_message_delete(self, messages: list[discord.Message]) -> None:
10 | # logger.trace(f"Messages deleted: {messages}")
11 |
12 | # @commands.Cog.listener()
13 | # async def on_message_delete(self, message: discord.Message) -> None:
14 | # logger.trace(f"Message deleted: {message}")
15 |
16 | # # if sender is a bot, ignore, some bots delete their own messages
17 | # if message.author.bot:
18 | # return
19 |
20 | # @commands.Cog.listener()
21 | # async def on_message_edit(self, before: discord.Message, after: discord.Message) -> None:
22 | # logger.trace(f"Message edited: {before} -> {after}")
23 |
24 | # @commands.Cog.listener()
25 | # async def on_message(self, message: discord.Message) -> None:
26 | # logger.trace(f"Message received: {message}")
27 |
28 |
29 | async def setup(bot: commands.Bot) -> None:
30 | await bot.add_cog(MessageEventsCog(bot))
31 |
--------------------------------------------------------------------------------
/.archive/neofetch.py:
--------------------------------------------------------------------------------
1 | import time
2 |
3 | import discord
4 | import psutil # for uptime
5 | from discord import app_commands
6 | from discord.ext import commands
7 |
8 | gray = "[1;30m"
9 | red = "[1;31m"
10 | green = "[1;32m"
11 | yellow = "[1;33m"
12 | blue = "[1;34m"
13 | pink = "[1;35m"
14 | cyan = "[1;36m"
15 | white = "[1;37m"
16 | reset = "[0;0m"
17 |
18 |
19 | class Neofetch(commands.Cog):
20 | def __init__(self, bot: commands.Bot) -> None:
21 | self.bot = bot
22 |
23 | @app_commands.command(name="neofetch", description="Make a neofetch.")
24 | async def neofetch(self, interaction: discord.Interaction) -> None:
25 | if interaction.guild is None:
26 | await interaction.response.send_message(
27 | content="This command cannot be used in direct messages.", ephemeral=True,
28 | )
29 | return
30 |
31 | # base ascii art
32 | base = """⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣤⣶⣾⣿⢿⣿⣶⣦⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
33 | ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⣿⣟⣯⣷⢿⣻⣷⣻⡾⣿⣆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
34 | ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿⡿⣽⣟⣾⢿⣻⡾⣯⡿⣯⡿⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
35 | ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿⠋⠙⢽⣟⡟⠁⠀⠙⣿⢯⣿⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
36 | ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣇⢸⣧⡸⠿⣀⢼⣿⠀⢸⣿⢷⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
37 | ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿⠜⢍⢊⢂⠢⠩⡙⠤⣿⣟⡿⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
38 | ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⣧⡣⣂⢂⠢⢡⠱⡘⣑⢼⣯⡿⣧⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
39 | ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣼⣿⠈⠲⢌⣍⡢⠕⠈⠁⠈⣿⣽⡿⣦⠀⠀⠀⠀⠀⠀⠀⠀⠀
40 | ⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣼⣿⡍⠀⠀⠀⠀⠀⠀⠀⠀⠀⠹⣷⢿⣻⣷⡀⠀⠀⠀⠀⠀⠀⠀
41 | ⠀⠀⠀⠀⠀⠀⠀⠀⣠⣿⣯⡷⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢻⣟⢯⣿⣽⣆⠀⠀⠀⠀⠀⠀
42 | ⠀⠀⠀⠀⠀⠀⠀⣴⣿⣳⣯⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢻⣷⡽⣾⣟⣧⠀⠀⠀⠀⠀
43 | ⠀⠀⠀⠀⠀⠀⣼⣿⢳⣯⠃⠀⠀⠀⢀⠠⠀⠂⠄⠠⠀⠀⠀⠀⠀⢿⣽⡼⣯⡿⣧⠀⠀⠀⠀
44 | ⠀⠀⠀⠀⠀⣸⡿⡇⣿⠇⠀⠀⠀⠂⠄⠂⡈⠄⠈⠄⡈⠄⠂⠀⠀⠈⣿⡇⣿⣻⣟⡇⠀⠀⠀
45 | ⠀⠀⠀⠀⠀⣿⣟⡇⣿⠀⠀⢀⠁⡈⠄⠁⠄⠂⡁⠄⠐⢀⠡⠐⠀⠀⠙⠃⢿⣯⢿⣻⠀⠀⠀
46 | ⠀⠀⠀⠀⢸⣿⣽⣧⡘⠀⠀⡂⠄⠐⡈⠐⡈⠠⠀⢂⠁⠄⠐⡈⢀⣼⣿⣿⣷⣮⢻⡿⠀⠀⠀
47 | ⠀⠀⠀⢀⠞⢅⠢⡙⢿⣦⣀⠂⠄⢁⠐⠠⠐⢀⠁⠄⠐⡀⢁⢰⢋⠺⣟⣾⢷⠻⡛⡣⡄⠀⠀
48 | ⢀⢔⠞⠍⢌⠢⡑⠄⠍⢿⣽⣷⣄⠂⠐⡀⠡⢀⠐⢈⠠⠀⠂⡼⡂⠕⠌⠍⢅⢑⢐⢐⢹⠀⠀
49 | ⢺⠠⠡⡑⢄⠑⠌⠌⠌⢌⢳⣿⣽⡆⠁⠄⢂⠠⠈⠠⠐⢈⠀⡗⡨⠨⠨⡈⡂⡂⡂⠢⠡⣣⡀
50 | ⢸⠡⡑⢌⠢⠡⠡⠡⡑⡐⡐⡹⣌⠠⠈⠄⠂⡀⠅⠂⡁⠄⣰⠣⠨⡈⡂⡂⡂⡂⠪⡈⡂⡂⣳
51 | ⡏⢌⢂⠢⠡⠡⠡⡑⡐⡐⡐⡐⠌⣷⣤⣌⣀⣐⣀⣥⣴⣾⡟⢌⢂⢂⠢⡂⠪⡈⣂⣢⠶⠚⠁
52 | ⠙⠲⠦⠥⢥⣅⡕⡐⡐⡐⡐⠌⢌⡾⠟⠟⠛⠛⠛⠛⠽⠾⣇⢂⢂⠢⡑⢌⢂⡶⠋⠀⠀⠀⠀
53 | ⠀⠀⠀⠀⠀⠀⠈⠑⠒⠶⠬⠞⠊⠀⠀⠀⠀⠀⠀⠀⠀⠀⠘⠢⣆⣑⡬⠖⠋⠀⠀⠀⠀⠀⠀"""
54 |
55 |
56 |
57 | # get uptime in the format of days, hours, minutes
58 | uptime = time.strftime("%d days, %H hours, %M minutes", time.gmtime(time.time() - psutil.boot_time()))
59 | cpuusage = psutil.cpu_percent()
60 | memusage = psutil.virtual_memory().percent
61 |
62 | lines = f"""{yellow}tux{reset}@{yellow}{interaction.guild.name.lower().replace(" ", "")}{reset}
63 | -----------------
64 | [1;4;36;40mTux Stats{reset}
65 | {red}OS{reset}: Tux Alpha
66 | {yellow}Kernel{reset}: 6.9
67 | {green}Uptime{reset}: {uptime}
68 | {cyan}CPU{reset}: {cpuusage}%
69 | {blue}Memory{reset}: {memusage}%
70 | {pink}Ping{reset}: {round(self.bot.latency * 1000)}ms
71 | -----------------
72 | [1;4;36;40mServer Stats{reset}
73 | {red}Name{reset}: {interaction.guild.name}
74 | {yellow}Owner{reset}: {interaction.guild.owner}
75 | {green}Members{reset}: {interaction.guild.member_count}
76 | {cyan}Roles{reset}: {len(interaction.guild.roles)}
77 | {blue}Channels{reset}: {len(interaction.guild.channels)}
78 | {pink}Emojis + Stickers{reset}: {len(interaction.guild.emojis) + len(interaction.guild.stickers)}
79 | -----------------
80 | """
81 |
82 | fetch = (
83 | "\n".join([f"{base.splitlines()[i]} {lines.splitlines()[i]}" for i in range(len(lines.splitlines()))])
84 | + "\n"
85 | + "\n".join(base.splitlines()[len(lines.splitlines()) :])
86 | )
87 |
88 | await interaction.response.send_message(content=f"```ansi\n{fetch}\n```")
89 |
90 |
91 | async def setup(bot: commands.Bot) -> None:
92 | await bot.add_cog(Neofetch(bot))
93 |
--------------------------------------------------------------------------------
/.archive/on_message.py:
--------------------------------------------------------------------------------
1 | # on_message.py
2 | import discord
3 | from discord.ext import commands
4 |
5 | from tux.utils.tux_logger import TuxLogger
6 |
7 | logger = TuxLogger(__name__)
8 |
9 |
10 | class OnMessage(commands.Cog):
11 | def __init__(self, bot):
12 | self.bot = bot
13 |
14 | @commands.Cog.listener()
15 | async def on_message(self, message: discord.Message):
16 | """This event is triggered whenever a message is sent in a channel.
17 |
18 | Args:
19 | message (discord.Message): Represents a Discord message.
20 | """ # noqa E501
21 | if message.author == self.bot.user:
22 | return
23 |
24 | if message.content.startswith("!hello"):
25 | await message.channel.send("Hello!")
26 |
27 |
28 | async def setup(bot):
29 | await bot.add_cog(OnMessage(bot))
30 |
--------------------------------------------------------------------------------
/.archive/roles.py:
--------------------------------------------------------------------------------
1 | import discord
2 | from discord import app_commands
3 | from discord.ext import commands
4 | from loguru import logger
5 |
6 | from tux.bot import Tux
7 | from tux.utils import checks
8 |
9 |
10 | class Roles(commands.Cog):
11 | def __init__(self, bot: Tux) -> None:
12 | self.bot = bot
13 |
14 | roles = app_commands.Group(name="roles", description="Commands for managing roles.")
15 |
16 | @roles.command(name="toggle")
17 | @app_commands.guild_only()
18 | @checks.ac_has_pl(3)
19 | async def toggle_role(
20 | self,
21 | interaction: discord.Interaction,
22 | user: discord.Member,
23 | role: discord.Role,
24 | ) -> None:
25 | """
26 | Toggle a role for a user.
27 |
28 | Parameters
29 | ----------
30 | interaction : discord.Interaction
31 | The discord interaction object.
32 | user : discord.Member
33 | The user to toggle the role for.
34 | role : discord.Role
35 | The role to toggle.
36 | """
37 |
38 | if role in user.roles:
39 | await user.remove_roles(role)
40 |
41 | await interaction.response.send_message(
42 | f"Removed role {role.mention} from {user.mention}",
43 | allowed_mentions=discord.AllowedMentions.none(),
44 | ephemeral=True,
45 | )
46 |
47 | logger.info(f"{interaction.user} removed role {role.name} from {user}.")
48 |
49 | else:
50 | await user.add_roles(role)
51 |
52 | await interaction.response.send_message(
53 | f"Added role {role.mention} to {user.mention}",
54 | allowed_mentions=discord.AllowedMentions.none(),
55 | ephemeral=True,
56 | )
57 |
58 | logger.info(f"{interaction.user} added role {role.name} to {user}.")
59 |
60 |
61 | async def setup(bot: Tux) -> None:
62 | await bot.add_cog(Roles(bot))
63 |
--------------------------------------------------------------------------------
/.archive/services.md:
--------------------------------------------------------------------------------
1 | # Services
2 |
3 | ## Overview
4 |
5 | Services within the context of this bot are background tasks that run continuously and provide various functionalities to the server.
6 |
7 | They are typically not directly interacted with by users, but rather provide additional features to the server that are not covered by commands.
8 |
9 | ## Available Services
10 |
11 | - **TTY Roles**: Assigns roles to users based on the member count of the server to act as a vanity metric for how long a user has been a member of the server.
12 | - **Harmful Message Detection**: Detects harmful CLI commands in messages and warns users about them.
13 | - **Temp Voice Channels**: Automatically creates temporary voice channels for users to use and deletes them when they are empty.
14 |
15 | ## Planned Services
16 |
17 | - **Auto Welcome**: Sends a welcome message to new members when they join the server.
18 | - **Auto Role**: Assigns a role to new members when they join the server.
19 | - **Auto Mod**: Automatically moderates the server by enforcing rules and restrictions.
20 | - **Auto Unban**: Automatically unbans users after a specified duration.
21 |
--------------------------------------------------------------------------------
/.archive/starboard/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/.archive/starboard/__init__.py
--------------------------------------------------------------------------------
/.archive/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/.archive/tests/__init__.py
--------------------------------------------------------------------------------
/.archive/tests/test_EventHandler.py:
--------------------------------------------------------------------------------
1 | import unittest
2 | from unittest.mock import MagicMock
3 |
4 | from tux.tux_events.event_handler import EventHandler
5 |
6 |
7 | class TestEventHandler(unittest.TestCase):
8 | def setUp(self):
9 | # Create a mock bot instance
10 | self.mock_bot = MagicMock()
11 | # Create an EventHandler instance with debug mode disabled
12 | self.event_handler = EventHandler(self.mock_bot, debug=False)
13 |
14 | def test_setup_logging(self):
15 | # Ensure the logging level is correctly set based on the debug flag
16 | self.assertEqual(self.event_handler.debug, False)
17 | self.assertEqual(self.event_handler.setup_logging(), None)
18 |
19 | # Test the case when debug mode is enabled
20 | self.event_handler.debug = True
21 | self.assertEqual(self.event_handler.setup_logging(), None)
22 |
23 | def test_load_events(self):
24 | # Mock the os.listdir function to return a list of mock file names
25 | with unittest.mock.patch(
26 | "os.listdir",
27 | return_value=["on_leave.py", "on_join.py"],
28 | ):
29 | self.assertEqual(self.event_handler.load_events(), None)
30 |
31 | # Ensure that self.bot.load_extension is called for each event module
32 | self.mock_bot.load_extension.assert_called_with(
33 | "tux.tux_events.events.on_leave",
34 | )
35 | self.mock_bot.load_extension.assert_called_with("tux.tux_events.events.on_join")
36 |
37 | def test_on_ready(self):
38 | # Mock the logging.info function to track its calls
39 | with unittest.mock.patch("logging.info") as mock_info:
40 | self.assertEqual(
41 | self.event_handler.on_ready(),
42 | None,
43 | ) # It should return None
44 |
45 | # Ensure that logging.info is called with the correct message
46 | mock_info.assert_called_with(f"{self.mock_bot.user} has connected to Discord!")
47 |
48 | def test_setup(self):
49 | # Ensure that the EventHandler is added to the bot using bot.add_cog
50 | with unittest.mock.patch.object(self.mock_bot, "add_cog") as mock_add_cog:
51 | self.assertEqual(self.event_handler.setup(self.mock_bot, debug=True), None)
52 |
53 | # Ensure that bot.add_cog is called with the correct arguments
54 | mock_add_cog.assert_called_with(EventHandler(self.mock_bot, debug=True))
55 |
56 |
57 | if __name__ == "__main__":
58 | unittest.main()
59 |
--------------------------------------------------------------------------------
/.archive/tests/test_permissions.py:
--------------------------------------------------------------------------------
1 | import unittest
2 |
3 | from src.permissions import check_permission
4 |
5 |
6 | class TestPermissions(unittest.TestCase):
7 | def test_valid_permission(self):
8 | self.assertTrue(check_permission("Mod", "Kick"))
9 |
10 | def test_invalid_permission(self):
11 | self.assertFalse(check_permission("Member", "Kick"))
12 |
13 | def test_command_not_found(self):
14 | self.assertFalse(check_permission("Admin", "NonexistentCommand"))
15 |
16 | def test_valid_permission_multiple_roles(self):
17 | self.assertTrue(check_permission("Admin", "Sudo"))
18 |
19 | def test_invalid_permission_case_insensitive(self):
20 | self.assertFalse(check_permission("member", "KICK"))
21 |
22 | def test_invalid_role(self):
23 | self.assertFalse(check_permission("InvalidRole", "AnyCommand"))
24 |
25 |
26 | if __name__ == "__main__":
27 | unittest.main()
28 |
--------------------------------------------------------------------------------
/.archive/thread.py:
--------------------------------------------------------------------------------
1 | from discord.ext import commands
2 |
3 |
4 | class ThreadEventsCog(commands.Cog, name="Thread Events Handler"):
5 | def __init__(self, bot: commands.Bot) -> None:
6 | self.bot = bot
7 |
8 | # @commands.Cog.listener()
9 | # async def on_thread_create(self, thread: discord.Thread) -> None:
10 | # logger.trace(f"{thread} has been created.")
11 |
12 | # @commands.Cog.listener()
13 | # async def on_thread_delete(self, thread: discord.Thread) -> None:
14 | # logger.trace(f"{thread} has been deleted.")
15 |
16 | # @commands.Cog.listener()
17 | # async def on_thread_remove(self, thread: discord.Thread) -> None:
18 | # logger.trace(f"{thread} has been removed.")
19 |
20 | # @commands.Cog.listener()
21 | # async def on_thread_update(self, before: discord.Thread, after: discord.Thread) -> None:
22 | # logger.trace(f"Thread updated: {before} -> {after}")
23 |
24 | # @commands.Cog.listener()
25 | # async def on_thread_join(self, thread: discord.Thread) -> None:
26 | # logger.trace(f"{thread} has been joined.")
27 |
28 | # @commands.Cog.listener()
29 | # async def on_thread_member_join(self, member: discord.ThreadMember) -> None:
30 | # logger.trace(f"Member {member} joined the thread.")
31 |
32 | # @commands.Cog.listener()
33 | # async def on_thread_member_remove(self, member: discord.ThreadMember) -> None:
34 | # logger.trace(f"Member {member} left the thread.")
35 |
36 | # @commands.Cog.listener()
37 | # async def on_thread_member_update(
38 | # self, before: discord.ThreadMember, after: discord.ThreadMember
39 | # ) -> None:
40 | # logger.trace(f"Thread member updated: {before} -> {after}")
41 |
42 |
43 | async def setup(bot: commands.Bot) -> None:
44 | await bot.add_cog(ThreadEventsCog(bot))
45 |
--------------------------------------------------------------------------------
/.archive/voice.py:
--------------------------------------------------------------------------------
1 | from typing import Any
2 |
3 | import discord
4 | from discord.ext import commands
5 |
6 | from tux.utils.constants import Constants as CONST
7 | from tux.utils.embeds import EmbedCreator
8 |
9 |
10 | class VoiceLogging(commands.Cog):
11 | def __init__(self, bot: commands.Bot):
12 | self.bot = bot
13 | self.audit_log_channel_id: int = CONST.LOG_CHANNELS["AUDIT"]
14 | self.mod_log_channel_id: int = CONST.LOG_CHANNELS["MOD"]
15 |
16 | async def send_to_audit_log(self, embed: discord.Embed):
17 | channel = self.bot.get_channel(self.audit_log_channel_id)
18 | if isinstance(channel, discord.TextChannel):
19 | await channel.send(embed=embed)
20 |
21 | def get_channel_change(self, before: discord.VoiceState, after: discord.VoiceState) -> str:
22 | if before.channel != after.channel:
23 | if after.channel:
24 | return f"has joined {after.channel.name}."
25 | if before.channel:
26 | return f"has left {before.channel.name}."
27 | return ""
28 |
29 | def get_state_change(self, state_name: str, before_state: Any, after_state: Any) -> str:
30 | if before_state != after_state:
31 | action = state_name + ("d" if after_state else "ed")
32 | return f"has been {action}."
33 | return ""
34 |
35 | @commands.Cog.listener()
36 | async def on_voice_state_update(
37 | self,
38 | member: discord.Member,
39 | before: discord.VoiceState,
40 | after: discord.VoiceState,
41 | ) -> None:
42 | embed = EmbedCreator.create_log_embed(
43 | title="Voice State Update",
44 | description=f"User: {member.name}",
45 | )
46 |
47 | changes: list[str] = []
48 |
49 | if channel_change := self.get_channel_change(before, after):
50 | changes.append(channel_change)
51 |
52 | if mute_change := self.get_state_change("mute", before.self_mute, after.self_mute):
53 | changes.append(mute_change)
54 |
55 | if deaf_change := self.get_state_change("deaf", before.self_deaf, after.self_deaf):
56 | changes.append(deaf_change)
57 |
58 | if stream_change := self.get_state_change(
59 | "start streaming", before.self_stream, after.self_stream
60 | ):
61 | changes.append(stream_change)
62 |
63 | if video_change := self.get_state_change(
64 | "start video", before.self_video, after.self_video
65 | ):
66 | changes.append(video_change)
67 |
68 | if changes:
69 | embed.add_field(name="Changes", value=" ".join(changes), inline=False)
70 |
71 | await self.send_to_audit_log(embed)
72 |
73 |
74 | async def setup(bot: commands.Bot) -> None:
75 | await bot.add_cog(VoiceLogging(bot))
76 |
--------------------------------------------------------------------------------
/.cursor/rules/cli_usage.mdc:
--------------------------------------------------------------------------------
1 | ---
2 | description:
3 | globs: tux/cli/**,README.md,DEVELOPER.md,pyproject.toml,docs/**
4 | alwaysApply: false
5 | ---
6 | # Tux CLI Usage
7 |
8 | This rule describes the custom `tux` CLI tool used for managing the application.
9 |
10 | ## Invocation
11 |
12 | Commands are run via Poetry: `poetry run tux `
13 |
14 | ## Environment Flags
15 |
16 | - **Development (Default):** Commands run in development mode by default (using `.env` variables like `DEV_BOT_TOKEN`, `DEV_DATABASE_URL`). No flag or the `--dev` flag can be used.
17 | - **Production:** To target production resources, **must** use the global `--prod` flag immediately after `tux`: `poetry run tux --prod ...`
18 |
19 | See [tux/utils/env.py](mdc:tux/utils/env.py) for environment logic.
20 |
21 | ## Command Groups
22 |
23 | - **`bot`**: Commands related to running the bot.
24 | - `start`: Starts the bot (uses hot-reloading in dev mode).
25 | - **`db`**: Commands for database management (interacts with Prisma).
26 | - `push`: Pushes schema changes directly (dev only).
27 | - `generate`: Generates the Prisma client.
28 | - `migrate`: Creates and applies database migrations.
29 | - `pull`: Updates `schema.prisma` from the database.
30 | - `reset`: Drops and recreates the database (destructive).
31 | - **`dev`**: Commands for development quality checks.
32 | - `lint`: Runs Ruff linter.
33 | - `lint-fix`: Runs Ruff linter and applies fixes.
34 | - `format`: Runs Ruff formatter.
35 | - `type-check`: Runs Pyright type checker.
36 | - `pre-commit`: Runs all configured pre-commit hooks.
37 | - **`docker`**: Commands for managing the Docker environment.
38 | - `build`: Builds Docker images.
39 | - `up`: Starts Docker services (uses `docker-compose.dev.yml` overrides in dev mode).
40 | - `down`: Stops Docker services.
41 | - `logs`: Shows container logs.
42 | - `exec`: Executes a command inside a running container.
43 |
44 | Refer to [DEVELOPER.md](mdc:DEVELOPER.md) for detailed examples and explanations.
45 |
--------------------------------------------------------------------------------
/.cursor/rules/core.mdc:
--------------------------------------------------------------------------------
1 | ---
2 | description:
3 | globs:
4 | alwaysApply: false
5 | ---
6 | # Core Functionality
7 |
8 | This rule describes the core components and processes of the Tux bot.
9 |
10 | ## Key Components:
11 |
12 | - **Main Entrypoint (`tux/main.py`)**: Orchestrates the bot's startup sequence. It initializes logging, Sentry, signal handlers, loads configuration, creates the bot instance, and starts the bot's event loop. [tux/main.py](mdc:tux/main.py)
13 | - **Bot Core (`tux/bot.py`)**: Defines the main `Tux` bot class, inheriting from `discord.ext.commands.Bot`. It handles the core Discord connection, event processing, and command dispatching logic. It initializes database connections and other core services. [tux/bot.py](mdc:tux/bot.py)
14 | - **Cog Loader (`tux/cog_loader.py`)**: Responsible for dynamically loading, unloading, and reloading Discord bot extensions (cogs) found primarily within the `tux/cogs/` directory. This allows for modular command and feature management. [tux/cog_loader.py](mdc:tux/cog_loader.py)
15 | - **Configuration (`tux/utils/config.py` & `tux/utils/env.py`)**: Configuration is managed through environment variables (loaded via `tux/utils/env.py`, likely using `.env` files) and a primary settings file (`config/settings.yml`) loaded and accessed via `tux/utils/config.py`. [tux/utils/config.py](mdc:tux/utils/config.py), [tux/utils/env.py](mdc:tux/utils/env.py), [config/settings.yml](mdc:config/settings.yml)
16 | - **Error Handling (`tux/handlers/error.py`)**: Contains centralized logic for handling errors that occur during command execution or other bot operations. It remaps the tree for app command errors, defines `on_command_error` listeners and formats error messages for users and logging. [tux/handlers/error.py](mdc:tux/handlers/error.py)
17 | - **Custom Help Command (`tux/help.py`)**: Implements a custom help command, overriding the default `discord.py` help behavior to provide a tailored user experience for discovering commands and features. [tux/help.py](mdc:tux/help.py)
18 | - **Utilities (`tux/utils/`)**: A collection of helper modules providing various utility functions used across the codebase (e.g., logging setup, embed creation, time formatting, constants). [tux/utils/](mdc:tux/utils)
--------------------------------------------------------------------------------
/.cursor/rules/database_patterns.mdc:
--------------------------------------------------------------------------------
1 | ---
2 | description:
3 | globs: tux/database/**,prisma/**,tux/cli/database.py
4 | alwaysApply: false
5 | ---
6 | # Database Interaction Patterns
7 |
8 | This rule summarizes the conventions and patterns for interacting with the database using controllers in `tux/database/controllers/`.
9 |
10 | ## Core Concepts
11 |
12 | - **BaseController:** All controllers inherit from `BaseController`, providing common CRUD, error handling, and transaction support. [tux/database/controllers/base.py](mdc:tux/database/controllers/base.py)
13 | - **Type Safety:** Controllers use generics and type hints for safety.
14 | - **Standardization:** Controllers provide a consistent interface for model interactions.
15 |
16 | ## Key Patterns & Best Practices
17 |
18 | 1. **Handling Relations:** Use `self.connect_or_create_relation(field_name, value)` when creating or updating entities with relations to avoid race conditions and ensure consistency.
19 | ```python
20 | # Example from tux/database/controllers/README.md
21 | await self.create(
22 | data={
23 | "user_id": user_id,
24 | "guild": self.connect_or_create_relation("guild_id", guild_id),
25 | }
26 | )
27 | ```
28 | 2. **Transactions:** For atomic operations (e.g., read-then-update), wrap the logic in an async function and pass it to `self.execute_transaction()`.
29 | ```python
30 | # Example from tux/database/controllers/README.md
31 | async def update_tx():
32 | entity = await self.find_unique(where=...)
33 | if entity is None: return None
34 | return await self.update(where=..., data=...)
35 |
36 | return await self.execute_transaction(update_tx)
37 | ```
38 | 3. **Safe Attribute Access:** Use `self.safe_get_attr(model_instance, attribute_name, default_value)` to access model attributes safely, providing a default if the attribute is missing or None.
39 | ```python
40 | # Example from tux/database/controllers/README.md
41 | count = self.safe_get_attr(entity, "count", 0) + 1
42 | ```
43 | 4. **Unique Lookups:** Use `find_unique` for lookups based on primary keys or unique fields.
44 | 5. **Batch Operations:** Utilize `update_many` and `delete_many` where appropriate for performance.
45 |
46 | Refer to the controllers README for more details: [tux/database/controllers/README.md](mdc:tux/database/controllers/README.md)
47 |
--------------------------------------------------------------------------------
/.cursor/rules/development_setup.mdc:
--------------------------------------------------------------------------------
1 | ---
2 | description:
3 | globs: tux/cli/**,README.md,DEVELOPER.md,docs/**,pyproject.toml,.env
4 | alwaysApply: false
5 | ---
6 | # Development Environment Setup
7 |
8 | This rule outlines the basic steps to set up a local development environment for Tux.
9 |
10 | ## Prerequisites
11 |
12 | - Git
13 | - Python 3.13+ (Managed via `mise`, `pyenv`, `asdf`, or system install)
14 | - Poetry (1.2+ recommended)
15 | - A PostgreSQL database
16 |
17 | ## Setup Steps
18 |
19 | 1. **Clone:** `git clone https://github.com/allthingslinux/tux && cd tux`
20 | 2. **Select Python Version:** Ensure Poetry uses the correct Python version (e.g., `poetry env use 3.13.2`).
21 | 3. **Install Dependencies:** `poetry install` installs project and dev dependencies.
22 | 4. **Install Pre-commit Hooks:** `poetry run pre-commit install` sets up Git hooks for quality checks.
23 | 5. **Configure Environment Variables:** Copy `.env.example` to `.env` (`cp .env.example .env`) and fill in required values like `DEV_BOT_TOKEN` and `DEV_DATABASE_URL`. See [.env.example](mdc:.env.example).
24 | 6. **Configure Bot Settings:** Copy `config/settings.yml.example` to `config/settings.yml` (`cp config/settings.yml.example config/settings.yml`) and customize settings, ensuring your Discord User ID is added to the owner list. See [config/settings.yml.example](mdc:config/settings.yml.example).
25 | 7. **Initialize Database:** Run `poetry run tux --dev db push` to apply the schema to your development database and generate the Prisma client.
26 |
27 | Refer to [DEVELOPER.md](mdc:DEVELOPER.md) for more comprehensive details.
28 |
--------------------------------------------------------------------------------
/.cursor/rules/docker_environment.mdc:
--------------------------------------------------------------------------------
1 | ---
2 | description:
3 | globs: docker-compose.yml,docker-compose.dev.yml,Dockerfile,README.md,.github/workflows/docker-image.yml,tux/cli/docker.py,.dockerignore
4 | alwaysApply: false
5 | ---
6 | # Docker Development Environment
7 |
8 | This rule describes the setup and usage of the optional Docker-based development environment.
9 |
10 | ## Overview
11 |
12 | Docker provides a containerized environment for consistency. It uses Docker Compose with overrides for development.
13 |
14 | - **Base Configuration:** [docker-compose.yml](mdc:docker-compose.yml)
15 | - **Development Overrides:** [docker-compose.dev.yml](mdc:docker-compose.dev.yml)
16 | - **Dockerfile:** [Dockerfile](mdc:Dockerfile) (multi-stage)
17 | - **Github Action:** [docker-image.yml](mdc:.github/workflows/docker-image.yml)
18 |
19 | ## Workflow
20 |
21 | Commands are run using the `tux` CLI's `docker` group (ensure you are in development mode - default or `--dev`).
22 |
23 | 1. **Build Images (Initial/Dockerfile Changes):**
24 | ```bash
25 | poetry run tux --dev docker build
26 | ```
27 | 2. **Start Services:**
28 | ```bash
29 | # Starts containers using dev overrides
30 | poetry run tux --dev docker up
31 |
32 | # Rebuild images before starting
33 | poetry run tux --dev docker up --build
34 | ```
35 | - Uses `docker-compose.dev.yml`.
36 | - Mounts the codebase using `develop: watch:` for live code syncing (replaces Python hot-reloading).
37 | - Runs `python -m tux --dev bot start` inside the `app` container.
38 |
39 | 3. **Stop Services:**
40 | ```bash
41 | poetry run tux --dev docker down
42 | ```
43 |
44 | ## Interacting with Containers
45 |
46 | Use `poetry run tux --dev docker exec app ` to run commands inside the `app` container.
47 |
48 | - **Logs:** `poetry run tux --dev docker logs -f`
49 | - **Shell:** `poetry run tux --dev docker exec app bash`
50 | - **Database Commands:** Must be run *inside* the container.
51 | ```bash
52 | # Example: Push schema
53 | poetry run tux --dev docker exec app poetry run tux --dev db push
54 | # Example: Create migration
55 | poetry run tux --dev docker exec app poetry run tux --dev db migrate --name
56 | ```
57 | - **Linting/Formatting/Type Checking:** Must be run *inside* the container.
58 | ```bash
59 | poetry run tux --dev docker exec app poetry run tux dev lint
60 | poetry run tux --dev docker exec app poetry run tux dev format
61 | # etc.
62 | ```
63 |
64 | Refer to the Docker section in [DEVELOPER.md](mdc:DEVELOPER.md) for more context.
65 |
--------------------------------------------------------------------------------
/.cursor/rules/extensions_system.mdc:
--------------------------------------------------------------------------------
1 | ---
2 | description:
3 | globs:
4 | alwaysApply: false
5 | ---
6 | # Extensions System
7 |
8 | This rule describes how to add custom functionality to Tux using the extensions system.
9 |
10 | ## Overview
11 |
12 | The extensions system allows adding custom commands and features without modifying the core bot code. This is achieved by placing standard `discord.py` cog files within the `tux/extensions/` directory.
13 |
14 | - **Location:** Place your custom cog Python files inside [tux/extensions/](mdc:tux/extensions).
15 | - **Discovery:** The bot automatically scans this directory (including subdirectories) and loads any valid cogs found.
16 | - **Submodules:** Subdirectories can be used, enabling the use of Git submodules to manage extensions.
17 |
18 | ## How to Add an Extension
19 |
20 | 1. Create a standard Python file containing a `discord.py` Cog class.
21 | 2. Ensure the cog has a `setup(bot)` function at the module level.
22 | 3. Place the file inside the `tux/extensions/` directory or a subdirectory within it.
23 | 4. The bot's [Cog Loader](mdc:tux/cog_loader.py) will automatically load it on startup or reload.
24 |
25 | ## Limitations
26 |
27 | As noted in the [Extensions README](mdc:tux/extensions/README.md):
28 |
29 | - **Category:** All commands from extensions currently appear under a single "Extensions" category in the help command.
30 | - **Database Schema:** Extensions cannot currently define or modify the database schema managed by Prisma. Database interactions are limited to the existing schema and controllers.
31 | - **Dependencies:** Extensions cannot add new Python package dependencies; they must use packages already listed in [pyproject.toml](mdc:pyproject.toml).
32 |
--------------------------------------------------------------------------------
/.cursor/rules/project_structure.mdc:
--------------------------------------------------------------------------------
1 | ---
2 | description:
3 | globs:
4 | alwaysApply: false
5 | ---
6 | # Tux Project Structure
7 |
8 | This project is a Python application managed with Poetry.
9 |
10 | ## Key Directories:
11 |
12 | - **`tux/`**: Contains the main Python source code for the application (e.g., `main.py`, `bot.py`, `cogs/`). [tux/](mdc:tux)
13 | - **`config/`**: Holds configuration files (e.g., `settings.yml`). [config/](mdc:config)
14 | - **`prisma/`**: Contains database schema definitions, primarily `prisma/schema/main.prisma`. [prisma/](mdc:prisma)
15 | - **`docs/`**: Contains project documentation managed with MkDocs (`mkdocs.yml`). [docs/](mdc:docs)
16 |
17 | ## Key Files:
18 |
19 | - **`pyproject.toml`**: Defines project metadata, dependencies (Poetry), and tool configurations (Ruff, Pyright, etc.). [pyproject.toml](mdc:pyproject.toml)
20 | - **`poetry.lock`**: Locks dependency versions. [poetry.lock](mdc:poetry.lock)
21 | - **`Dockerfile`**: Defines the Docker image build process. [Dockerfile](mdc:Dockerfile)
22 | - **`docker-compose.yml`**: Defines services for Docker Compose. [docker-compose.yml](mdc:docker-compose.yml)
23 | - **`README.md`**: Main project README file. [README.md](mdc:README.md)
24 | - **`DEVELOPER.md`**: Guide for developers contributing to the project. [DEVELOPER.md](mdc:DEVELOPER.md)
25 |
26 | This rule provides a high-level overview. You can add more details or specific file references as needed.
27 |
--------------------------------------------------------------------------------
/.devcontainer/devcontainer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Tux Development Container",
3 | "dockerFile": "../Dockerfile",
4 | "context": "..",
5 | "runArgs": [
6 | "--init",
7 | "--env-file",
8 | ".env"
9 | ],
10 | "forwardPorts": [
11 | 3000
12 | ],
13 | "build": {
14 | "target": "dev",
15 | "args": {
16 | "DEVCONTAINER": "1"
17 | }
18 | },
19 | "features": {
20 | "ghcr.io/devcontainers/features/github-cli:1": {}
21 | },
22 | "customizations": {
23 | "vscode": {
24 | "extensions": [
25 | "ms-python.python",
26 | "ms-python.vscode-pylance",
27 | "charliermarsh.ruff",
28 | "prisma.prisma",
29 | "kevinrose.vsc-python-indent",
30 | "mikestead.dotenv",
31 | "njpwerner.autodocstring",
32 | "usernamehw.errorlens",
33 | "redhat.vscode-yaml"
34 | ]
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------
1 | .env
2 | .venv/
3 | .cache/
4 | __pycache__/
5 | *.pyc
6 | assets/
7 | docs-build/
8 | site/
9 | .cursorrules
10 | .editorconfig
11 |
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
1 | # Tux Environment Configuration (.env.example)
2 | # -------------------------------------------
3 | # Copy this file to .env and fill in the values.
4 | # Do NOT commit your actual .env file to version control.
5 |
6 | # Core Requirements
7 | # -----------------
8 | # These variables are fundamental and required depending on the mode.
9 |
10 | # Database URLs (Required: one depending on mode)
11 | # The application uses DEV_DATABASE_URL when run with '--dev' flag,
12 | # and PROD_DATABASE_URL otherwise (production mode).
13 | DEV_DATABASE_URL=""
14 | PROD_DATABASE_URL=""
15 |
16 | # Bot Tokens (Required: one depending on mode)
17 | # The application uses DEV_BOT_TOKEN when run with '--dev' flag,
18 | # and PROD_BOT_TOKEN otherwise (production mode).
19 | DEV_BOT_TOKEN=""
20 | PROD_BOT_TOKEN=""
21 |
22 | # Development Specific Settings
23 | # ---------------------------
24 | # These settings primarily affect development mode ('--dev').
25 |
26 | # Cogs to ignore during development (Optional, comma-separated)
27 | # Example: DEV_COG_IGNORE_LIST="somecog,anothercog"
28 | DEV_COG_IGNORE_LIST="rolecount,mail,git" # Default ignores ATL-specific cogs
29 |
30 | # Production Specific Settings
31 | # --------------------------
32 | # These settings primarily affect production mode (no '--dev' flag).
33 |
34 | # Cogs to ignore in production (Optional, comma-separated)
35 | # Example: PROD_COG_IGNORE_LIST="debugcog"
36 | PROD_COG_IGNORE_LIST="rolecount,mail,git" # Default ignores ATL-specific cogs
37 |
38 | # Optional Feature Configuration
39 | # ----------------------------
40 | # Fill these variables to enable optional integrations.
41 |
42 | # Sentry (Error Tracking)
43 | # SENTRY_DSN=""
44 |
45 | # Wolfram Alpha (Math/Science Queries)
46 | # MAKE SURE THIS IS FOR THE SIMPLE API OR IT WILL NOT WORK
47 | # WOLFRAM_APP_ID=""
48 |
49 | # InfluxDB (Metrics/Logging)
50 | # ------------------
51 |
52 | # INFLUXDB_TOKEN=""
53 | # INFLUXDB_URL=""
54 | # INFLUXDB_ORG=""
55 |
56 | # GitHub Integration
57 | # ------------------
58 | # These variables are used for the ATL GitHub integration that is is used for creating issues quickly.
59 | # You can safely ignore these until we have a proper way to guide using them multi guild/self hosted wise.
60 |
61 | # GITHUB_APP_ID=
62 | # GITHUB_CLIENT_ID=""
63 | # GITHUB_CLIENT_SECRET=""
64 | # GITHUB_PUBLIC_KEY=""
65 | # GITHUB_INSTALLATION_ID=
66 | # GITHUB_PRIVATE_KEY_BASE64="" # Base64 encoded private key
67 | # GITHUB_REPO_URL=
68 | # GITHUB_REPO_OWNER=
69 | # GITHUB_REPO=
70 |
71 | # Mailcow Integration (Email related features)
72 | # MAILCOW_API_KEY=
73 | # MAILCOW_API_URL=
74 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug Report
3 | about: Create a report to help us improve
4 | title: "[BUG] - "
5 | labels: "type: bug"
6 | assignees: ''
7 |
8 | ---
9 |
10 | ## Describe the Bug
11 |
12 | A clear and concise description of what the bug is.
13 |
14 | ## To Reproduce
15 |
16 | Steps to reproduce the behavior:
17 |
18 | 1. Go to '...'
19 | 2. Click on '....'
20 | 3. Scroll down to '....'
21 | 4. See error
22 |
23 | Please include code snippets or screenshots where applicable.
24 |
25 | ## Expected Behavior
26 |
27 | A clear and concise description of what you expected to happen.
28 |
29 | ## Screenshots
30 |
31 | If applicable, add screenshots to help explain your problem.
32 |
33 | ## Environment (please complete the following information)
34 |
35 | - OS: [e.g. Ubuntu 20.04]
36 | - Python version: [e.g. Python 3.12]
37 | - Discord.py version: [e.g. 2.0.0]
38 | - Tux version: [e.g. v1.0.0]
39 |
40 | ## Additional Context
41 |
42 | Add any other context about the problem here, such as logs, configuration files, etc.
43 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature Request
3 | about: Suggest an idea for this project
4 | title: "[FEATURE] - "
5 | labels: "type: feature-request"
6 | assignees: ''
7 |
8 | ---
9 |
10 | ## Is your feature request related to a problem? Please describe
11 |
12 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
13 |
14 | ## Describe the solution you'd like
15 |
16 | A clear and concise description of what you want to happen.
17 |
18 | ## Describe alternatives you've considered
19 |
20 | A clear and concise description of any alternative solutions or features you've considered.
21 |
22 | ## Additional Context
23 |
24 | Add any other context or screenshots about the feature request here.
25 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | ## Description
2 |
3 | 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. If this change fixes any issues please put "Fixes #XX" in the description. Please also ensure to add the appropriate labels to the PR.
4 |
5 | ## Guidelines
6 |
7 | - My code follows the style guidelines of this project (formatted with Ruff)
8 | - I have performed a self-review of my own code
9 | - I have commented my code, particularly in hard-to-understand areas
10 | - I have made corresponding changes to the documentation if needed
11 | - My changes generate no new warnings
12 | - I have tested this change
13 | - Any dependent changes have been merged and published in downstream modules
14 | - I have added all appropriate labels to this PR
15 |
16 | - [ ] I have followed all of these guidelines.
17 |
18 | ## How Has This Been Tested? (if applicable)
19 |
20 | Please describe how you tested your code. e.g describe what commands you ran, what arguments, and any config stuff (if applicable)
21 |
22 | ## Screenshots (if applicable)
23 |
24 | Please add screenshots to help explain your changes.
25 |
26 | ## Additional Information
27 |
28 | Please add any other information that is important to this PR.
29 |
--------------------------------------------------------------------------------
/.github/SECURITY.md:
--------------------------------------------------------------------------------
1 | # Security Policy
2 |
3 | ## Supported Versions
4 |
5 | Currently only the latest stable release will be supported with security updates. It is your responsibility to keep your Tux instance up to date.
6 |
7 | ## Reporting a Vulnerability
8 |
9 | If you find a Vulnerability please report it to `tux@allthingslinux.com` and someone will get back ASAP.
10 |
11 | **Please note that Outlook does not like our email server so emails will not work with it.**
12 |
--------------------------------------------------------------------------------
/.github/SUPPORT.md:
--------------------------------------------------------------------------------
1 | # Support for Tux
2 |
3 | Thank you for your interest in Tux, the all-in-one Discord bot for the All Things Linux Discord server. This guide will help you find the support you need to utilize Tux effectively.
4 |
5 | ## Table of Contents
6 |
7 | - [Support for Tux](#support-for-tux)
8 | - [Table of Contents](#table-of-contents)
9 | - [Getting Started](#getting-started)
10 | - [Frequently Asked Questions (FAQs)](#frequently-asked-questions-faqs)
11 | - [1. How do I install and set up Tux?](#1-how-do-i-install-and-set-up-tux)
12 | - [2. Where can I find the development guide?](#2-where-can-i-find-the-development-guide)
13 | - [Bug Reports and Feature Requests](#bug-reports-and-feature-requests)
14 | - [Contributing](#contributing)
15 | - [Contact Information](#contact-information)
16 |
17 | ## Getting Started
18 |
19 | If you're just getting started with Tux, please refer to the [README.md](README.md) file in the repository for installation instructions, prerequisites, and a high-level overview of the project.
20 |
21 | ## Frequently Asked Questions (FAQs)
22 |
23 | Here are some commonly asked questions about Tux:
24 |
25 | ### 1. How do I install and set up Tux?
26 |
27 | Refer to the [README](README.md) section "Installation" for detailed instructions on how to install and set up the bot.
28 |
29 | ### 2. Where can I find the development guide?
30 |
31 | You can find the development guide in the `docs` directory at [docs/development.md](docs/development.md).
32 |
33 | ## Bug Reports and Feature Requests
34 |
35 | If you encounter any bugs or have feature requests, please follow these steps:
36 |
37 | 1. **Search Existing Issues**: Before creating a new issue, please search the existing [issues](https://github.com/allthingslinux/tux/issues) to check if your concern has already been addressed.
38 |
39 | 2. **Create a New Issue**: If you can't find a relevant issue, you can create a new one. Ensure you provide detailed info, including steps to reproduce the issue, expected behavior & any relevant logs or screenshots.
40 |
41 | ## Contributing
42 |
43 | We appreciate and welcome contributions to Tux! If you're interested in contributing, please check out our [Contributing Guide](CONTRIBUTING.md) which provides guidelines on how to get started.
44 |
45 | ## Contact Information
46 |
47 | For additional help or questions that you can't find answers to, feel free to reach out to us through:
48 |
49 | - **Discord Support Server**: [atl.dev](https://discord.gg/gpmSjcjQxg)
50 | - **GitHub Issues**: [Tux Issues](https://github.com/allthingslinux/tux/issues)
51 |
52 | We aim to respond to all queries as quickly as possible.
53 |
54 | Thank you for using Tux and being a part of our community!
55 |
--------------------------------------------------------------------------------
/.github/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json",
3 | "timezone": "America/New_York",
4 | "schedule": ["* 0 * * 0"],
5 | "extends": [
6 | "config:recommended"
7 | ]
8 | }
9 |
--------------------------------------------------------------------------------
/.github/workflows/docker-image.yml:
--------------------------------------------------------------------------------
1 | name: "GHCR - Build and Push Docker Image"
2 |
3 | on:
4 | push:
5 | branches: ["main"]
6 | tags: ["*"]
7 | pull_request:
8 | workflow_dispatch:
9 |
10 | jobs:
11 | docker:
12 | runs-on: ubuntu-latest
13 | permissions:
14 | contents: read
15 | packages: write
16 |
17 | steps:
18 | - name: Checkout
19 | uses: actions/checkout@v4
20 | with:
21 | ref: ${{ github.event_name == 'pull_request' && github.head_ref || github.ref_name }}
22 | fetch-depth: 0
23 |
24 | - name: Docker meta
25 | id: meta
26 | uses: docker/metadata-action@v5
27 | with:
28 | images: |
29 | ghcr.io/allthingslinux/tux
30 | flavor: |
31 | latest=${{ github.ref_type == 'tag' }}
32 | tags: |
33 | type=sha,enable={{is_default_branch}},event=push
34 | type=pep440,pattern={{version}},event=tag
35 | type=ref,event=pr
36 |
37 | - name: Set up Docker Buildx
38 | uses: docker/setup-buildx-action@v3
39 |
40 | - name: Login to GHCR
41 | if: github.ref_type == 'tag'
42 | uses: docker/login-action@v3
43 | with:
44 | registry: ghcr.io
45 | username: ${{ github.actor }}
46 | password: ${{ secrets.GITHUB_TOKEN }}
47 |
48 | - name: Build and push
49 | uses: docker/build-push-action@v6
50 | with:
51 | push: ${{ github.ref_type == 'tag' }}
52 | tags: ${{ steps.meta.outputs.tags }}
53 | labels: ${{ steps.meta.outputs.labels }}
54 | context: .
55 | provenance: false
56 | build-args: |
57 | BUILDKIT_CONTEXT_KEEP_GIT_DIR=1
58 |
59 | - name: Remove old images
60 | uses: actions/delete-package-versions@v5
61 | with:
62 | package-name: 'tux'
63 | package-type: 'container'
64 | min-versions-to-keep: 10
65 |
66 |
67 |
--------------------------------------------------------------------------------
/.github/workflows/linting.yml:
--------------------------------------------------------------------------------
1 | name: "Ruff - Linting and Formatting"
2 |
3 | on: [push, pull_request]
4 |
5 | permissions:
6 | contents: write
7 | issues: write
8 | pull-requests: write
9 |
10 | jobs:
11 | Ruff:
12 | runs-on: ubuntu-24.04
13 | steps:
14 | - name: "Checkout Repository"
15 | uses: actions/checkout@v4
16 | with:
17 | token: ${{ github.token }}
18 |
19 | # Install Python
20 | - name: Setup Python
21 | uses: actions/setup-python@v5
22 | with:
23 | python-version: 3.13
24 |
25 | # Install Ruff
26 | - name: Install Ruff
27 | run: sudo snap install ruff
28 |
29 | # Run Ruff linter
30 | - name: Run Ruff format
31 | run: ruff format && ruff check . --fix
32 | - uses: stefanzweifel/git-auto-commit-action@v5
33 | with:
34 | commit_message: "chore: Linting and formatting via Ruff"
35 |
--------------------------------------------------------------------------------
/.github/workflows/pyright.yml:
--------------------------------------------------------------------------------
1 | name: "Pyright - Static Type Checking"
2 |
3 | on: [push, pull_request]
4 |
5 | jobs:
6 | pyright:
7 | runs-on: ubuntu-latest
8 |
9 | steps:
10 | - name: Check out repository
11 | uses: actions/checkout@v4
12 |
13 | - name: Install Poetry
14 | run: pipx install poetry
15 |
16 | - name: Set up Python
17 | uses: actions/setup-python@v5
18 | with:
19 | python-version: "3.13"
20 | cache: "poetry"
21 |
22 | - name: Install project
23 | run: poetry install --no-interaction
24 |
25 | - name: Activate virtual environment
26 | run: echo "${{ github.workspace }}/.venv/bin" >> $GITHUB_PATH
27 |
28 | - name: Add Poetry binary to PATH
29 | run: echo "${HOME}/.local/bin" >> $GITHUB_PATH
30 |
31 | - name: Print environment for debug
32 | run: |
33 | echo "Python location: $(which python)"
34 | echo "Poetry location: $(which poetry)"
35 | poetry --version
36 | which pyright
37 |
38 | - name: Generate Prisma Client
39 | run: poetry run prisma generate
40 |
41 | - name: Run Pyright
42 | uses: jakebailey/pyright-action@v2
43 | with:
44 | version: "PATH"
45 | annotate: "all"
46 |
--------------------------------------------------------------------------------
/.github/workflows/remove-old-images.yml:
--------------------------------------------------------------------------------
1 | name: Remove old images
2 |
3 | on:
4 | workflow_dispatch:
5 | inputs:
6 | KEEP_AMOUNT:
7 | description: "Number of images to keep"
8 | required: true
9 | default: "10"
10 | REMOVE_UNTAGGED:
11 | description: "Remove untagged images"
12 | required: true
13 | default: "false"
14 |
15 | jobs:
16 | remove-old-images:
17 | runs-on: ubuntu-latest
18 |
19 | steps:
20 | - name: Remove old images
21 | uses: actions/delete-package-versions@v5
22 | with:
23 | package-name: 'tux'
24 | package-type: 'container'
25 | min-versions-to-keep: ${{ github.event.inputs.KEEP_AMOUNT }}
26 | delete-only-untagged-versions: ${{ github.event.inputs.REMOVE_UNTAGGED }}
27 |
--------------------------------------------------------------------------------
/.github/workflows/todo.yml:
--------------------------------------------------------------------------------
1 | name: "Actions - TODO to Issue"
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | workflow_dispatch:
8 | inputs:
9 | MANUAL_COMMIT_REF:
10 | description: "SHA to compare"
11 | required: true
12 | MANUAL_BASE_REF:
13 | description: "Optional earlier SHA"
14 | required: false
15 |
16 | jobs:
17 | build:
18 | runs-on: ubuntu-latest
19 | steps:
20 | - uses: "actions/checkout@v4"
21 | with:
22 | fetch-depth: 0
23 |
24 | - name: "TODO to Issue"
25 | uses: "alstr/todo-to-issue-action@v5.1.12"
26 | with:
27 | CLOSE_ISSUES: true
28 | INSERT_ISSUE_URLS: true
29 | AUTO_ASSIGN: true
30 | IDENTIFIERS: '[{"name": "TODO", "labels": ["enhancement"]}, {"name": "FIXME", "labels": ["bug"]}]'
31 | ESCAPE: true
32 | IGNORE: ".github/,node_modules/,dist/,build/,vendor/poetry.lock"
33 | PROJECTS_SECRET: ${{ secrets.ADMIN_PAT }}
34 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # Replit
7 | *.replit
8 |
9 | # C extensions
10 | *.so
11 |
12 | # Distribution / packaging
13 | .Python
14 | build/
15 | develop-eggs/
16 | dist/
17 | downloads/
18 | eggs/
19 | .eggs/
20 | lib/
21 | lib64/
22 | parts/
23 | sdist/
24 | var/
25 | wheels/
26 | share/python-wheels/
27 | *.egg-info/
28 | .installed.cfg
29 | *.egg
30 | MANIFEST
31 |
32 | # PyInstaller
33 | *.manifest
34 | *.spec
35 |
36 | # Installer logs
37 | pip-log.txt
38 | pip-delete-this-directory.txt
39 |
40 | # Unit test / coverage reports
41 | htmlcov/
42 | .tox/
43 | .nox/
44 | .coverage
45 | .coverage.*
46 | .cache
47 | nosetests.xml
48 | coverage.xml
49 | *.cover
50 | *.py,cover
51 | .hypothesis/
52 | .pytest_cache/
53 | cover/
54 |
55 | # Translations
56 | *.mo
57 | *.pot
58 |
59 | # Django
60 | *.log
61 | local_settings.py
62 | db.sqlite3
63 | db.sqlite3-journal
64 |
65 | # Flask
66 | instance/
67 | .webassets-cache
68 |
69 | # Scrapy
70 | .scrapy
71 |
72 | # Sphinx documentation
73 | docs/_build/
74 |
75 | # PyBuilder
76 | .pybuilder/
77 | target/
78 |
79 | # Jupyter Notebook
80 | .ipynb_checkpoints
81 |
82 | # IPython
83 | profile_default/
84 | ipython_config.py
85 |
86 | # Pipenv
87 | Pipfile.lock
88 |
89 | # Poetry
90 | poetry.lock
91 |
92 | # Pdm
93 | .pdm.toml
94 |
95 | # PEP 582
96 | __pypackages__/
97 |
98 | # Celery
99 | celerybeat-schedule
100 | celerybeat.pid
101 |
102 | # SageMath parsed files
103 | *.sage.py
104 |
105 | # Spyder
106 | .spyderproject
107 | .spyproject
108 |
109 | # Rope
110 | .ropeproject
111 |
112 | # mkdocs
113 | /site
114 |
115 | # Mypy
116 | .mypy_cache/
117 | .dmypy.json
118 | dmypy.json
119 |
120 | # Pyre
121 | .pyre/
122 |
123 | # Pytype
124 | .pytype/
125 |
126 | # Cython
127 | cython_debug/
128 |
129 | # Ruff
130 | .ruff_cache/
131 |
132 | # Logs
133 | logs/
134 | tux/logs/
135 |
136 | # IDEs
137 | # PyCharm
138 | .idea/
139 |
140 | # Prisma
141 | .binaries/
142 | prisma/database.db
143 |
144 | # Environments
145 | .env
146 | .venv
147 | env/
148 | venv/
149 | env.bak
150 | .env.*
151 |
152 | # Sensitive files
153 | github-private-key.pem
154 |
155 | # Miscellaneous
156 | /debug.csv
157 | config/settings*
158 | !config/settings.yml.example
159 |
160 | # MacOS
161 | .DS_Store
162 |
163 | build/
164 | docs-build/
165 | docs/site/
166 | site/
167 |
168 | # extensions
169 | tux/extensions/*
170 | !tux/extensions/README.md
171 |
172 | # misc
173 | slim.report.json
174 | prisma_binaries/
175 |
176 | # direnv
177 | .direnv/
178 |
--------------------------------------------------------------------------------
/.mise.toml:
--------------------------------------------------------------------------------
1 | [tools]
2 | python = "3.13.2"
--------------------------------------------------------------------------------
/.pre-commit-config.yaml:
--------------------------------------------------------------------------------
1 | default_language_version:
2 | python: python3.13
3 |
4 | repos:
5 | # 1. Fast File Checks
6 | - repo: https://github.com/pre-commit/pre-commit-hooks
7 | rev: v5.0.0
8 | hooks:
9 | - id: check-yaml
10 | - id: check-json
11 | - id: check-toml
12 |
13 | - repo: https://github.com/abravalheri/validate-pyproject
14 | rev: v0.24.1
15 | hooks:
16 | - id: validate-pyproject
17 | additional_dependencies: ["validate-pyproject-schema-store[all]"]
18 |
19 | # 2. Code Upgraders/Modifiers
20 | - repo: https://github.com/asottile/pyupgrade
21 | rev: v3.20.0
22 | hooks:
23 | - id: pyupgrade
24 | args: ["--py313-plus"]
25 |
26 | - repo: https://github.com/asottile/add-trailing-comma
27 | rev: v3.2.0
28 | hooks:
29 | - id: add-trailing-comma
30 |
31 | # 3. Main Linter (with auto-fix)
32 | - repo: https://github.com/astral-sh/ruff-pre-commit
33 | # Ruff version should match the one in pyproject.toml
34 | rev: v0.11.12 # Use the same Ruff version tag as formatter
35 | hooks:
36 | - id: ruff
37 | args: [--fix]
38 |
39 | # 4. Main Formatter (after linting/fixing)
40 | - repo: https://github.com/astral-sh/ruff-pre-commit
41 | # Ruff version should match the one in pyproject.toml
42 | rev: v0.11.12
43 | hooks:
44 | - id: ruff-format
45 |
46 | # 5. Project Config / Dependency Checks
47 | # TODO: Disabled due to a issue with "No module named 'jinja2'"
48 | # relevant: https://github.com/mtkennerly/poetry-dynamic-versioning/issues/13
49 | #- repo: https://github.com/python-poetry/poetry
50 | # rev: 2.1.3 # Use the latest tag from the repo
51 | # hooks:
52 | # - id: poetry-check
53 |
54 | # 6. Security Check
55 | - repo: https://github.com/gitleaks/gitleaks
56 | rev: v8.27.0 # Use the latest tag from the repo
57 | hooks:
58 | - id: gitleaks
59 |
60 | exclude: '(^\.archive|/typings)/'
61 |
--------------------------------------------------------------------------------
/.python-version:
--------------------------------------------------------------------------------
1 | 3.13.2
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": [
3 | "ms-vscode-remote.remote-containers",
4 | "ms-python.python",
5 | "ms-python.vscode-pylance",
6 | "ms-azuretools.vscode-docker",
7 | "charliermarsh.ruff",
8 | "prisma.prisma",
9 | "kevinrose.vsc-python-indent",
10 | "mikestead.dotenv",
11 | "njpwerner.autodocstring",
12 | "usernamehw.errorlens",
13 | "sourcery.sourcery",
14 | "redhat.vscode-yaml"
15 | ]
16 | }
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "ruff.lint.enable": true,
3 | "ruff.codeAction.fixViolation": {
4 | "enable": true
5 | },
6 | "ruff.fixAll": true,
7 | "editor.formatOnSave": true,
8 | "editor.defaultFormatter": "charliermarsh.ruff",
9 | "editor.codeActionsOnSave": {
10 | "source.organizeImports.ruff": "always",
11 | "source.fixAll": "always"
12 | },
13 | "python.languageServer": "Pylance",
14 | "python.analysis.typeCheckingMode": "strict",
15 | "python.analysis.autoFormatStrings": true,
16 | "python.analysis.completeFunctionParens": true,
17 | "python.analysis.autoImportCompletions": true,
18 | "python.analysis.inlayHints.functionReturnTypes": true,
19 | "python.analysis.inlayHints.variableTypes": true,
20 | "python.analysis.inlayHints.callArgumentNames": "all",
21 | "python.terminal.activateEnvInCurrentTerminal": true,
22 | "files.exclude": {
23 | "**/__pycache__": true,
24 | "**/*.pyc": true,
25 | "**/pycache": true
26 | },
27 | "search.exclude": {
28 | ".archive/**": true,
29 | "build/**": true
30 | },
31 | "python.analysis.exclude": [
32 | ".archive/**",
33 | "build/**"
34 | ],
35 | "python.analysis.diagnosticSeverityOverrides": {
36 | "reportIncompatibleMethodOverride": "none",
37 | "reportGeneralTypeIssues": "information"
38 | },
39 | "git.autofetch": true,
40 | "editor.inlayHints.enabled": "offUnlessPressed",
41 | "[yaml]": {
42 | "editor.defaultFormatter": "redhat.vscode-yaml"
43 | },
44 | "yaml.schemas": {
45 | "https://squidfunk.github.io/mkdocs-material/schema.json": "mkdocs.yml"
46 | },
47 | "[markdown]": {
48 | "editor.defaultFormatter": "DavidAnson.vscode-markdownlint"
49 | },
50 | "[dockerfile]": {
51 | "editor.defaultFormatter": "ms-azuretools.vscode-docker"
52 | },
53 | "[json]": {
54 | "editor.defaultFormatter": "vscode.json-language-features"
55 | }
56 | }
--------------------------------------------------------------------------------
/DEVELOPER.md:
--------------------------------------------------------------------------------
1 | # Developer Guide: Tux
2 |
3 | Welcome to the Tux developer documentation!
4 |
5 | This area provides in-depth information for developers working on Tux, beyond the initial setup and contribution workflow.
6 |
7 | ## Getting Started & Contributing
8 |
9 | For information on setting up your environment, the development workflow (branching, PRs), and basic quality checks, please refer to the main contribution guide:
10 |
11 | * [**Contributing Guide**](./.github/CONTRIBUTING.md)
12 |
13 | ## Developer Topics
14 |
15 | Explore the following pages for more detailed information on specific development aspects:
16 |
17 | * **[Local Development](./docs/content/dev/local_development.md)**
18 | * Running the bot locally.
19 | * Understanding the hot reloading mechanism.
20 | * **[Tux CLI Usage](./docs/content/dev/cli/index.md)**
21 | * Understanding development vs. production modes (`--dev`, `--prod`).
22 | * Overview of command groups (`bot`, `db`, `dev`, `docker`).
23 | * **[Database Management](./docs/content/dev/database.md)**
24 | * Detailed usage of `tux db` commands (push, migrate, generate, pull, reset).
25 | * Working with Prisma migrations.
26 | * **[Database Controller Patterns](./docs/content/dev/database_patterns.md)**
27 | * Using controllers for CRUD, transactions, relations.
28 | * Best practices for database interactions in code.
29 | * **[Docker Environment](./docs/content/dev/docker_development.md)** (Optional)
30 | * Setting up and using the Docker-based development environment.
31 | * Running commands within Docker containers.
32 |
--------------------------------------------------------------------------------
/assets/badges/100k-messages.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/badges/100k-messages.png
--------------------------------------------------------------------------------
/assets/badges/10k-messages.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/badges/10k-messages.png
--------------------------------------------------------------------------------
/assets/badges/500h-voice.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/badges/500h-voice.png
--------------------------------------------------------------------------------
/assets/badges/50k-messages.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/badges/50k-messages.png
--------------------------------------------------------------------------------
/assets/badges/day1-joiner.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/badges/day1-joiner.png
--------------------------------------------------------------------------------
/assets/badges/former-staff.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/badges/former-staff.png
--------------------------------------------------------------------------------
/assets/badges/helpful.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/badges/helpful.png
--------------------------------------------------------------------------------
/assets/badges/lucky.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/badges/lucky.png
--------------------------------------------------------------------------------
/assets/badges/top50-text.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/badges/top50-text.png
--------------------------------------------------------------------------------
/assets/badges/tux-dev.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/badges/tux-dev.png
--------------------------------------------------------------------------------
/assets/branding/avatar.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/branding/avatar.png
--------------------------------------------------------------------------------
/assets/branding/tux.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/branding/tux.gif
--------------------------------------------------------------------------------
/assets/embeds/active_case.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/embeds/active_case.png
--------------------------------------------------------------------------------
/assets/embeds/inactive_case.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/embeds/inactive_case.png
--------------------------------------------------------------------------------
/assets/emojis/active_case.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/emojis/active_case.png
--------------------------------------------------------------------------------
/assets/emojis/added.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/emojis/added.png
--------------------------------------------------------------------------------
/assets/emojis/ban.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/emojis/ban.png
--------------------------------------------------------------------------------
/assets/emojis/inactive_case.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/emojis/inactive_case.png
--------------------------------------------------------------------------------
/assets/emojis/jail.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/emojis/jail.png
--------------------------------------------------------------------------------
/assets/emojis/kick.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/emojis/kick.png
--------------------------------------------------------------------------------
/assets/emojis/removed.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/emojis/removed.png
--------------------------------------------------------------------------------
/assets/emojis/snippetban.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/emojis/snippetban.png
--------------------------------------------------------------------------------
/assets/emojis/snippetunban.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/emojis/snippetunban.png
--------------------------------------------------------------------------------
/assets/emojis/tempban.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/emojis/tempban.png
--------------------------------------------------------------------------------
/assets/emojis/timeout.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/emojis/timeout.png
--------------------------------------------------------------------------------
/assets/emojis/tux_case.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/emojis/tux_case.png
--------------------------------------------------------------------------------
/assets/emojis/tux_default.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/emojis/tux_default.png
--------------------------------------------------------------------------------
/assets/emojis/tux_error.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/emojis/tux_error.png
--------------------------------------------------------------------------------
/assets/emojis/tux_info.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/emojis/tux_info.png
--------------------------------------------------------------------------------
/assets/emojis/tux_note.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/emojis/tux_note.png
--------------------------------------------------------------------------------
/assets/emojis/tux_notify.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/emojis/tux_notify.png
--------------------------------------------------------------------------------
/assets/emojis/tux_poll.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/emojis/tux_poll.png
--------------------------------------------------------------------------------
/assets/emojis/tux_prefix.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/emojis/tux_prefix.png
--------------------------------------------------------------------------------
/assets/emojis/tux_success.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/emojis/tux_success.png
--------------------------------------------------------------------------------
/assets/emojis/tux_tag.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/emojis/tux_tag.png
--------------------------------------------------------------------------------
/assets/emojis/warn.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/emojis/warn.png
--------------------------------------------------------------------------------
/assets/roles/de-wm/Cinnamon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/roles/de-wm/Cinnamon.png
--------------------------------------------------------------------------------
/assets/roles/de-wm/awesome.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/roles/de-wm/awesome.png
--------------------------------------------------------------------------------
/assets/roles/de-wm/berrywm.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/roles/de-wm/berrywm.png
--------------------------------------------------------------------------------
/assets/roles/de-wm/bspwm.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/roles/de-wm/bspwm.png
--------------------------------------------------------------------------------
/assets/roles/de-wm/budgie.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/roles/de-wm/budgie.png
--------------------------------------------------------------------------------
/assets/roles/de-wm/dwm.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/roles/de-wm/dwm.png
--------------------------------------------------------------------------------
/assets/roles/de-wm/enlightenment.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/roles/de-wm/enlightenment.png
--------------------------------------------------------------------------------
/assets/roles/de-wm/exwm.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/roles/de-wm/exwm.png
--------------------------------------------------------------------------------
/assets/roles/de-wm/gnome.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/roles/de-wm/gnome.png
--------------------------------------------------------------------------------
/assets/roles/de-wm/herbsluft.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/roles/de-wm/herbsluft.png
--------------------------------------------------------------------------------
/assets/roles/de-wm/hyprland.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/roles/de-wm/hyprland.png
--------------------------------------------------------------------------------
/assets/roles/de-wm/i3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/roles/de-wm/i3.png
--------------------------------------------------------------------------------
/assets/roles/de-wm/ice_wm.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/roles/de-wm/ice_wm.png
--------------------------------------------------------------------------------
/assets/roles/de-wm/jwm.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/roles/de-wm/jwm.png
--------------------------------------------------------------------------------
/assets/roles/de-wm/kde_plasma.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/roles/de-wm/kde_plasma.png
--------------------------------------------------------------------------------
/assets/roles/de-wm/left_wm.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/roles/de-wm/left_wm.png
--------------------------------------------------------------------------------
/assets/roles/de-wm/lx_qt.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/roles/de-wm/lx_qt.png
--------------------------------------------------------------------------------
/assets/roles/de-wm/mate.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/roles/de-wm/mate.png
--------------------------------------------------------------------------------
/assets/roles/de-wm/openbox.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/roles/de-wm/openbox.png
--------------------------------------------------------------------------------
/assets/roles/de-wm/qtile.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/roles/de-wm/qtile.png
--------------------------------------------------------------------------------
/assets/roles/de-wm/river.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/roles/de-wm/river.png
--------------------------------------------------------------------------------
/assets/roles/de-wm/stump_wm.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/roles/de-wm/stump_wm.png
--------------------------------------------------------------------------------
/assets/roles/de-wm/sway_wm.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/roles/de-wm/sway_wm.png
--------------------------------------------------------------------------------
/assets/roles/de-wm/wayfire.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/roles/de-wm/wayfire.png
--------------------------------------------------------------------------------
/assets/roles/de-wm/xfce.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/roles/de-wm/xfce.png
--------------------------------------------------------------------------------
/assets/roles/de-wm/xmonad.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/roles/de-wm/xmonad.png
--------------------------------------------------------------------------------
/assets/roles/distro/alpine.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/roles/distro/alpine.png
--------------------------------------------------------------------------------
/assets/roles/distro/anti_x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/roles/distro/anti_x.png
--------------------------------------------------------------------------------
/assets/roles/distro/antix.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/roles/distro/antix.png
--------------------------------------------------------------------------------
/assets/roles/distro/arch.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/roles/distro/arch.png
--------------------------------------------------------------------------------
/assets/roles/distro/arco.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/roles/distro/arco.png
--------------------------------------------------------------------------------
/assets/roles/distro/artix.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/roles/distro/artix.png
--------------------------------------------------------------------------------
/assets/roles/distro/asahi_linux.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/roles/distro/asahi_linux.png
--------------------------------------------------------------------------------
/assets/roles/distro/bazzite.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/roles/distro/bazzite.png
--------------------------------------------------------------------------------
/assets/roles/distro/bedrock.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/roles/distro/bedrock.png
--------------------------------------------------------------------------------
/assets/roles/distro/cachy.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/roles/distro/cachy.png
--------------------------------------------------------------------------------
/assets/roles/distro/chimera.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/roles/distro/chimera.png
--------------------------------------------------------------------------------
/assets/roles/distro/debian.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/roles/distro/debian.png
--------------------------------------------------------------------------------
/assets/roles/distro/deepin.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/roles/distro/deepin.png
--------------------------------------------------------------------------------
/assets/roles/distro/devuan.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/roles/distro/devuan.png
--------------------------------------------------------------------------------
/assets/roles/distro/endeavour.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/roles/distro/endeavour.png
--------------------------------------------------------------------------------
/assets/roles/distro/exherbo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/roles/distro/exherbo.png
--------------------------------------------------------------------------------
/assets/roles/distro/fedora.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/roles/distro/fedora.png
--------------------------------------------------------------------------------
/assets/roles/distro/free_bsd.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/roles/distro/free_bsd.png
--------------------------------------------------------------------------------
/assets/roles/distro/garuda.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/roles/distro/garuda.png
--------------------------------------------------------------------------------
/assets/roles/distro/gentoo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/roles/distro/gentoo.png
--------------------------------------------------------------------------------
/assets/roles/distro/haiku.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/roles/distro/haiku.png
--------------------------------------------------------------------------------
/assets/roles/distro/kiss.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/roles/distro/kiss.png
--------------------------------------------------------------------------------
/assets/roles/distro/lfs.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/roles/distro/lfs.png
--------------------------------------------------------------------------------
/assets/roles/distro/mac_os.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/roles/distro/mac_os.png
--------------------------------------------------------------------------------
/assets/roles/distro/manjaro.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/roles/distro/manjaro.png
--------------------------------------------------------------------------------
/assets/roles/distro/mint.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/roles/distro/mint.png
--------------------------------------------------------------------------------
/assets/roles/distro/mx.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/roles/distro/mx.png
--------------------------------------------------------------------------------
/assets/roles/distro/net_bsd.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/roles/distro/net_bsd.png
--------------------------------------------------------------------------------
/assets/roles/distro/nixos.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/roles/distro/nixos.png
--------------------------------------------------------------------------------
/assets/roles/distro/nobara.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/roles/distro/nobara.png
--------------------------------------------------------------------------------
/assets/roles/distro/open_bsd.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/roles/distro/open_bsd.png
--------------------------------------------------------------------------------
/assets/roles/distro/opensuse.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/roles/distro/opensuse.png
--------------------------------------------------------------------------------
/assets/roles/distro/plan_9.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/roles/distro/plan_9.png
--------------------------------------------------------------------------------
/assets/roles/distro/popos.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/roles/distro/popos.png
--------------------------------------------------------------------------------
/assets/roles/distro/puppy.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/roles/distro/puppy.png
--------------------------------------------------------------------------------
/assets/roles/distro/qubes.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/roles/distro/qubes.png
--------------------------------------------------------------------------------
/assets/roles/distro/redhat.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/roles/distro/redhat.png
--------------------------------------------------------------------------------
/assets/roles/distro/rocky_linux.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/roles/distro/rocky_linux.png
--------------------------------------------------------------------------------
/assets/roles/distro/slackware.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/roles/distro/slackware.png
--------------------------------------------------------------------------------
/assets/roles/distro/solus.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/roles/distro/solus.png
--------------------------------------------------------------------------------
/assets/roles/distro/ubuntu.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/roles/distro/ubuntu.png
--------------------------------------------------------------------------------
/assets/roles/distro/ubuntu_mate.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/roles/distro/ubuntu_mate.png
--------------------------------------------------------------------------------
/assets/roles/distro/vanilla.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/roles/distro/vanilla.png
--------------------------------------------------------------------------------
/assets/roles/distro/void.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/roles/distro/void.png
--------------------------------------------------------------------------------
/assets/roles/distro/windows.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/roles/distro/windows.png
--------------------------------------------------------------------------------
/assets/roles/distro/zorin.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/roles/distro/zorin.png
--------------------------------------------------------------------------------
/assets/roles/donor-icons/donor.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/roles/donor-icons/donor.png
--------------------------------------------------------------------------------
/assets/roles/donor-icons/mega-donor.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/roles/donor-icons/mega-donor.png
--------------------------------------------------------------------------------
/assets/roles/donor-icons/super-donor.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/roles/donor-icons/super-donor.png
--------------------------------------------------------------------------------
/assets/roles/langs/asm.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/roles/langs/asm.png
--------------------------------------------------------------------------------
/assets/roles/langs/bash.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/roles/langs/bash.png
--------------------------------------------------------------------------------
/assets/roles/langs/c.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/roles/langs/c.png
--------------------------------------------------------------------------------
/assets/roles/langs/c_sharp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/roles/langs/c_sharp.png
--------------------------------------------------------------------------------
/assets/roles/langs/clojure.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/roles/langs/clojure.png
--------------------------------------------------------------------------------
/assets/roles/langs/cpp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/roles/langs/cpp.png
--------------------------------------------------------------------------------
/assets/roles/langs/crystal.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/roles/langs/crystal.png
--------------------------------------------------------------------------------
/assets/roles/langs/dart.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/roles/langs/dart.png
--------------------------------------------------------------------------------
/assets/roles/langs/elixr.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/roles/langs/elixr.png
--------------------------------------------------------------------------------
/assets/roles/langs/erlang.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/roles/langs/erlang.png
--------------------------------------------------------------------------------
/assets/roles/langs/gd_script.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/roles/langs/gd_script.png
--------------------------------------------------------------------------------
/assets/roles/langs/go.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/roles/langs/go.png
--------------------------------------------------------------------------------
/assets/roles/langs/haskell.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/roles/langs/haskell.png
--------------------------------------------------------------------------------
/assets/roles/langs/html_css.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/roles/langs/html_css.png
--------------------------------------------------------------------------------
/assets/roles/langs/java.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/roles/langs/java.png
--------------------------------------------------------------------------------
/assets/roles/langs/js.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/roles/langs/js.png
--------------------------------------------------------------------------------
/assets/roles/langs/julia.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/roles/langs/julia.png
--------------------------------------------------------------------------------
/assets/roles/langs/kotlin.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/roles/langs/kotlin.png
--------------------------------------------------------------------------------
/assets/roles/langs/lisp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/roles/langs/lisp.png
--------------------------------------------------------------------------------
/assets/roles/langs/lua.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/roles/langs/lua.png
--------------------------------------------------------------------------------
/assets/roles/langs/nim.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/roles/langs/nim.png
--------------------------------------------------------------------------------
/assets/roles/langs/o_caml.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/roles/langs/o_caml.png
--------------------------------------------------------------------------------
/assets/roles/langs/perl.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/roles/langs/perl.png
--------------------------------------------------------------------------------
/assets/roles/langs/php.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/roles/langs/php.png
--------------------------------------------------------------------------------
/assets/roles/langs/python.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/roles/langs/python.png
--------------------------------------------------------------------------------
/assets/roles/langs/r.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/roles/langs/r.png
--------------------------------------------------------------------------------
/assets/roles/langs/ruby.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/roles/langs/ruby.png
--------------------------------------------------------------------------------
/assets/roles/langs/rust.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/roles/langs/rust.png
--------------------------------------------------------------------------------
/assets/roles/langs/sh_script.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/roles/langs/sh_script.png
--------------------------------------------------------------------------------
/assets/roles/langs/swift.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/roles/langs/swift.png
--------------------------------------------------------------------------------
/assets/roles/langs/vala.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/roles/langs/vala.png
--------------------------------------------------------------------------------
/assets/roles/langs/zig.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/roles/langs/zig.png
--------------------------------------------------------------------------------
/assets/roles/text-editors/ed.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/roles/text-editors/ed.png
--------------------------------------------------------------------------------
/assets/roles/text-editors/emacs.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/roles/text-editors/emacs.png
--------------------------------------------------------------------------------
/assets/roles/text-editors/helix.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/roles/text-editors/helix.png
--------------------------------------------------------------------------------
/assets/roles/text-editors/jetbrains.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/roles/text-editors/jetbrains.png
--------------------------------------------------------------------------------
/assets/roles/text-editors/kakoune.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/roles/text-editors/kakoune.png
--------------------------------------------------------------------------------
/assets/roles/text-editors/kate.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/roles/text-editors/kate.png
--------------------------------------------------------------------------------
/assets/roles/text-editors/micro.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/roles/text-editors/micro.png
--------------------------------------------------------------------------------
/assets/roles/text-editors/nano.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/roles/text-editors/nano.png
--------------------------------------------------------------------------------
/assets/roles/text-editors/neovim.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/roles/text-editors/neovim.png
--------------------------------------------------------------------------------
/assets/roles/text-editors/vs_code.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/assets/roles/text-editors/vs_code.png
--------------------------------------------------------------------------------
/docker-compose.dev.yml:
--------------------------------------------------------------------------------
1 | ---
2 |
3 | # NOTE: This file is used for local development purposes only.
4 |
5 | services:
6 | tux:
7 | container_name: tux
8 | image: tux:dev
9 | user: root
10 | build:
11 | context: .
12 | dockerfile: Dockerfile
13 | target: dev
14 | develop:
15 | watch:
16 | - action: sync
17 | path: .
18 | target: /app/
19 | ignore:
20 | - .venv/
21 | - .git/
22 | - .cache/
23 | - .vscode/
24 | - .idea/
25 | - "**/__pycache__/"
26 | - "**/*.pyc"
27 | - "*.log"
28 | - ".*.swp"
29 | - "*.swp"
30 | - "*~"
31 | env_file:
32 | - .env
33 | restart: unless-stopped
34 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | ---
2 |
3 | # NOTE: This file is used for production deployment.
4 |
5 | services:
6 | tux:
7 | container_name: tux
8 | image: ghcr.io/allthingslinux/tux:latest
9 | build:
10 | context: .
11 | dockerfile: Dockerfile
12 | target: production
13 | volumes:
14 | - ./config:/app/config:ro
15 | - ./tux/extensions:/app/tux/extensions
16 | - ./assets:/app/assets
17 | env_file:
18 | - .env
19 | restart: unless-stopped
20 |
--------------------------------------------------------------------------------
/docs/content/assets/images/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/docs/content/assets/images/logo.png
--------------------------------------------------------------------------------
/docs/content/dev/cli/index.md:
--------------------------------------------------------------------------------
1 | # CLI Reference
2 |
3 | This section provides details on using the custom `tux` command-line interface, built with Click.
4 |
5 | ## Environment Selection
6 |
7 | The `tux` CLI defaults to **development mode** for all command groups (`bot`, `db`, `dev`, `docker`). This ensures that operations like database migrations or starting the bot target your development resources unless explicitly specified otherwise.
8 |
9 | * **Production Mode:**
10 | To run a command targeting production resources (e.g., production database, production bot token), you **must** use the global `--prod` flag immediately after `tux`:
11 |
12 | ```bash
13 | # Example: Apply migrations to production database
14 | poetry run tux --prod db migrate
15 |
16 | # Example: Start the bot using production token/DB
17 | poetry run tux --prod bot start
18 | ```
19 |
20 | * **Development Mode (Default / Explicit):**
21 | Running any command without `--prod` automatically uses development mode. You can also explicitly use the `--dev` flag, although it is redundant.
22 |
23 | ```bash
24 | # These are equivalent and run in development mode:
25 | poetry run tux db push
26 | poetry run tux --dev db push
27 |
28 | poetry run tux bot start
29 | poetry run tux --dev bot start
30 | ```
31 |
32 | This default-to-development approach prioritizes safety by preventing accidental operations on production environments. The environment determination logic can be found in `tux/utils/env.py`.
33 |
34 | ::: mkdocs-click
35 | :module: tux.cli
36 | :command: cli
37 | :prog_name: tux
38 | :depth: 0
39 | :style: table
40 | :list_subcommands: True
41 |
--------------------------------------------------------------------------------
/docs/content/dev/contributing.md:
--------------------------------------------------------------------------------
1 | ../../../.github/CONTRIBUTING.md
--------------------------------------------------------------------------------
/docs/content/dev/local_development.md:
--------------------------------------------------------------------------------
1 | # Local Development
2 |
3 | This section covers running and developing Tux directly on your local machine, which is the recommended approach.
4 |
5 | **Running the Bot:**
6 |
7 | 1. **Push Database Schema:**
8 | If this is your first time setting up or if you've made changes to `schema.prisma`, push the schema to your development database. This command also generates the Prisma client.
9 |
10 | ```bash
11 | # Ensure you use --dev or rely on the default development mode
12 | poetry run tux --dev db push
13 | ```
14 |
15 | *You can explicitly regenerate the Prisma client anytime with `poetry run tux --dev db generate`.*
16 |
17 | 2. **Start the Bot:**
18 |
19 | Start the bot in development mode:
20 |
21 | ```bash
22 | poetry run tux --dev start
23 | ```
24 |
25 | This command will:
26 | * Read `DEV_DATABASE_URL` and `DEV_BOT_TOKEN` from your `.env` file.
27 | * Connect to the development database.
28 | * Authenticate with Discord using the development token.
29 | * Load all cogs.
30 | * Start the Discord bot.
31 | * Enable the built-in **Hot Reloading** system.
32 |
33 | **Hot Reloading:**
34 |
35 | The project includes a hot-reloading utility (`tux/utils/hot_reload.py`).
36 |
37 | When the bot is running locally via `poetry run tux --dev start`, this utility watches for changes in the `tux/cogs/` directory. It attempts to automatically reload modified cogs or cogs affected by changes in watched utility files without requiring a full bot restart.
38 |
39 | This significantly speeds up development for cog-related changes. Note that changes outside the watched directories (e.g., core bot logic, dependencies) may still require a manual restart (`Ctrl+C` and run the start command again).
40 |
--------------------------------------------------------------------------------
/docs/content/dev/permissions.md:
--------------------------------------------------------------------------------
1 | # Permissions Management
2 |
3 | Tux employs a level-based permissions system to control command execution.
4 |
5 | Each command is associated with a specific permission level, ensuring that only users with the necessary clearance can execute it.
6 |
7 | ## Initial Setup
8 |
9 | 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.
10 |
11 | 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.
12 |
13 | ## Advantages
14 |
15 | The level-based system allows Tux to manage command execution efficiently across different servers.
16 |
17 | It offers a more flexible solution than just relying on Discord's built-in permissions, avoiding the need to hardcode permissions into the bot.
18 |
19 | This flexibility makes it easier to modify permissions without changing the bot’s underlying code, accommodating servers with custom role names seamlessly.
20 |
21 | ## Available Permission Levels
22 |
23 | Below is the hierarchy of permission levels available in Tux:
24 |
25 | - **0: Member**
26 | - **1: Support**
27 | - **2: Junior Moderator**
28 | - **3: Moderator**
29 | - **4: Senior Moderator**
30 | - **5: Administrator**
31 | - **6: Head Administrator**
32 | - **7: Server Owner** (Not the actual discord assigned server owner)
33 | - **8: Sys Admin** (User ID list in `config/settings.yml`)
34 | - **9: Bot Owner** (User ID in `config/settings.yml`)
35 |
36 | 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.
37 |
--------------------------------------------------------------------------------
/docs/content/index.md:
--------------------------------------------------------------------------------
1 | # Welcome to the Tux Documentation
2 |
3 | Tux is an open-source Discord bot developed for the All Things Linux community. This documentation serves as a comprehensive resource for:
4 |
5 | - **Developers**: Architecture guides, API references, and contribution workflows
6 | - **Server Administrators**: Setup instructions, configuration options, and self-hosting guides
7 | - **Users**: Command references, feature explanations, and usage examples
8 |
9 | Whether you're looking to contribute to the codebase, deploy your own instance, or simply learn how to use Tux's features, you'll find everything you need in these docs.
10 |
11 | Find the source code on GitHub: [allthingslinux/tux](https://github.com/allthingslinux/tux)
12 |
13 | ## Contributing
14 |
15 | Interested in contributing? Please read our contribution guidelines. (Link to `CONTRIBUTING.md` or relevant page needed)
16 |
17 | ---
18 |
19 | *These docs are built using [MkDocs](https://www.mkdocs.org/).*
20 |
--------------------------------------------------------------------------------
/flake.lock:
--------------------------------------------------------------------------------
1 | {
2 | "nodes": {
3 | "flake-parts": {
4 | "inputs": {
5 | "nixpkgs-lib": "nixpkgs-lib"
6 | },
7 | "locked": {
8 | "lastModified": 1743550720,
9 | "narHash": "sha256-hIshGgKZCgWh6AYJpJmRgFdR3WUbkY04o82X05xqQiY=",
10 | "owner": "hercules-ci",
11 | "repo": "flake-parts",
12 | "rev": "c621e8422220273271f52058f618c94e405bb0f5",
13 | "type": "github"
14 | },
15 | "original": {
16 | "owner": "hercules-ci",
17 | "ref": "main",
18 | "repo": "flake-parts",
19 | "type": "github"
20 | }
21 | },
22 | "nixpkgs": {
23 | "locked": {
24 | "lastModified": 1743315132,
25 | "narHash": "sha256-6hl6L/tRnwubHcA4pfUUtk542wn2Om+D4UnDhlDW9BE=",
26 | "owner": "nixos",
27 | "repo": "nixpkgs",
28 | "rev": "52faf482a3889b7619003c0daec593a1912fddc1",
29 | "type": "github"
30 | },
31 | "original": {
32 | "owner": "NixOS",
33 | "ref": "nixos-unstable",
34 | "repo": "nixpkgs",
35 | "type": "github"
36 | }
37 | },
38 | "nixpkgs-lib": {
39 | "locked": {
40 | "lastModified": 1743296961,
41 | "narHash": "sha256-b1EdN3cULCqtorQ4QeWgLMrd5ZGOjLSLemfa00heasc=",
42 | "owner": "nix-community",
43 | "repo": "nixpkgs.lib",
44 | "rev": "e4822aea2a6d1cdd36653c134cacfd64c97ff4fa",
45 | "type": "github"
46 | },
47 | "original": {
48 | "owner": "nix-community",
49 | "repo": "nixpkgs.lib",
50 | "type": "github"
51 | }
52 | },
53 | "root": {
54 | "inputs": {
55 | "flake-parts": "flake-parts",
56 | "nixpkgs": "nixpkgs"
57 | }
58 | }
59 | },
60 | "root": "root",
61 | "version": 7
62 | }
63 |
--------------------------------------------------------------------------------
/flake.nix:
--------------------------------------------------------------------------------
1 | {
2 | description = "All Thing's Linux discord bot - Tux";
3 |
4 | inputs = {
5 | nixpkgs = {
6 | type = "github";
7 | owner = "NixOS";
8 | repo = "nixpkgs";
9 | ref = "nixos-unstable";
10 | };
11 |
12 | flake-parts = {
13 | type = "github";
14 | owner = "hercules-ci";
15 | repo = "flake-parts";
16 | ref = "main";
17 | };
18 | };
19 |
20 | outputs = inputs@{
21 | self,
22 | nixpkgs,
23 | flake-parts,
24 | ...
25 | }:
26 | flake-parts.lib.mkFlake { inherit inputs; } {
27 | systems = [
28 | "x86_64-linux"
29 | "x86_64-darwin"
30 | "aarch64-linux"
31 | "aarch64-darwin"
32 | ];
33 |
34 | perSystem = { pkgs, self', system, ... }: {
35 | devShells = {
36 | default = self'.devShells.tux;
37 | tux = pkgs.callPackage ./shell.nix { inherit pkgs self; };
38 | };
39 |
40 | apps.envrc = {
41 | type = "app";
42 | program = self'.packages.envrc;
43 | };
44 |
45 | # Creates .envrc if does not exist
46 | packages.envrc = pkgs.writeShellScriptBin "envrc" ''
47 | echo
48 |
49 | if [ ! -e ".envrc" ]; then
50 | echo "Creating .envrc"
51 | printf "use flake .\n\n\n" | cat - .env.example > .envrc
52 | echo
53 |
54 | echo "The directory is now set up for direnv usage."
55 | echo
56 | else
57 | echo "Please delete .envrc if you wish to recreate it."
58 | echo
59 | fi
60 | '';
61 | };
62 | };
63 | }
64 |
--------------------------------------------------------------------------------
/poetry.toml:
--------------------------------------------------------------------------------
1 | [virtualenvs]
2 | in-project = true
3 |
--------------------------------------------------------------------------------
/prisma/schema/commands/afk.prisma:
--------------------------------------------------------------------------------
1 | model AFKModel {
2 | member_id BigInt @id
3 | nickname String
4 | reason String
5 | since DateTime @default(now())
6 | until DateTime?
7 | guild_id BigInt
8 | enforced Boolean @default(false)
9 | perm_afk Boolean @default(false)
10 | guild Guild @relation(fields: [guild_id], references: [guild_id])
11 |
12 | @@unique([member_id, guild_id])
13 | @@index([member_id])
14 | }
--------------------------------------------------------------------------------
/prisma/schema/commands/moderation.prisma:
--------------------------------------------------------------------------------
1 | model Note {
2 | note_id BigInt @id @default(autoincrement())
3 | note_content String
4 | note_created_at DateTime @default(now())
5 | note_moderator_id BigInt
6 | note_user_id BigInt
7 | note_number BigInt?
8 | guild_id BigInt
9 | guild Guild @relation(fields: [guild_id], references: [guild_id])
10 |
11 | @@unique([note_number, guild_id])
12 | @@index([note_number, guild_id])
13 | }
14 |
15 | model Case {
16 | case_id BigInt @id @default(autoincrement())
17 | case_status Boolean? @default(true)
18 | case_type CaseType
19 | case_reason String
20 | case_moderator_id BigInt
21 | case_user_id BigInt
22 | case_user_roles BigInt[] @default([])
23 | case_number BigInt?
24 | case_created_at DateTime? @default(now())
25 | case_expires_at DateTime?
26 | case_tempban_expired Boolean? @default(false)
27 | guild_id BigInt
28 | guild Guild @relation(fields: [guild_id], references: [guild_id])
29 |
30 | @@unique([case_number, guild_id])
31 | @@index([case_number, guild_id])
32 |
33 | @@index([guild_id, case_user_id])
34 |
35 | @@index([guild_id, case_moderator_id])
36 |
37 | @@index([guild_id, case_type])
38 |
39 | @@index([case_type, case_expires_at, case_tempban_expired])
40 |
41 | @@index([case_created_at(sort: Desc)])
42 | }
43 |
44 | enum CaseType {
45 | BAN
46 | UNBAN
47 | HACKBAN
48 | TEMPBAN
49 | KICK
50 | SNIPPETBAN
51 | TIMEOUT
52 | UNTIMEOUT
53 | WARN
54 | JAIL
55 | UNJAIL
56 | SNIPPETUNBAN
57 | UNTEMPBAN
58 | POLLBAN
59 | POLLUNBAN
60 | }
--------------------------------------------------------------------------------
/prisma/schema/commands/reminder.prisma:
--------------------------------------------------------------------------------
1 | model Reminder {
2 | reminder_id BigInt @id @default(autoincrement())
3 | reminder_content String
4 | reminder_created_at DateTime @default(now())
5 | reminder_expires_at DateTime
6 | reminder_channel_id BigInt
7 | reminder_user_id BigInt
8 | reminder_sent Boolean @default(false)
9 | guild_id BigInt
10 | guild Guild @relation(fields: [guild_id], references: [guild_id])
11 |
12 | @@unique([reminder_id, guild_id])
13 | @@index([reminder_id, guild_id])
14 | }
--------------------------------------------------------------------------------
/prisma/schema/commands/snippets.prisma:
--------------------------------------------------------------------------------
1 | model Snippet {
2 | snippet_id BigInt @id @default(autoincrement())
3 | snippet_name String
4 | snippet_content String? // optional cause of snippet aliases
5 | snippet_user_id BigInt
6 | snippet_created_at DateTime @default(now())
7 | guild_id BigInt
8 | uses BigInt @default(0)
9 | locked Boolean @default(false)
10 | alias String? // name of another snippet
11 | guild Guild @relation(fields: [guild_id], references: [guild_id])
12 |
13 | @@unique([snippet_name, guild_id])
14 | @@index([snippet_name, guild_id])
15 | }
16 |
--------------------------------------------------------------------------------
/prisma/schema/guild/config.prisma:
--------------------------------------------------------------------------------
1 | model GuildConfig {
2 | prefix String?
3 | mod_log_id BigInt?
4 | audit_log_id BigInt?
5 | join_log_id BigInt?
6 | private_log_id BigInt?
7 | report_log_id BigInt?
8 | dev_log_id BigInt?
9 | jail_channel_id BigInt?
10 | general_channel_id BigInt?
11 | starboard_channel_id BigInt?
12 | perm_level_0_role_id BigInt?
13 | perm_level_1_role_id BigInt?
14 | perm_level_2_role_id BigInt?
15 | perm_level_3_role_id BigInt?
16 | perm_level_4_role_id BigInt?
17 | perm_level_5_role_id BigInt?
18 | perm_level_6_role_id BigInt?
19 | perm_level_7_role_id BigInt?
20 | base_staff_role_id BigInt?
21 | base_member_role_id BigInt?
22 | jail_role_id BigInt?
23 | quarantine_role_id BigInt?
24 | guild_id BigInt @id @unique
25 | guild Guild @relation(fields: [guild_id], references: [guild_id])
26 |
27 | @@index([guild_id])
28 | }
--------------------------------------------------------------------------------
/prisma/schema/guild/guild.prisma:
--------------------------------------------------------------------------------
1 | model Guild {
2 | guild_id BigInt @id
3 | guild_joined_at DateTime? @default(now())
4 | cases Case[]
5 | snippets Snippet[]
6 | notes Note[]
7 | reminders Reminder[]
8 | guild_config GuildConfig[]
9 | AFK AFKModel[]
10 | Starboard Starboard?
11 | StarboardMessage StarboardMessage[]
12 | case_count BigInt @default(0)
13 | levels Levels[]
14 |
15 | @@index([guild_id])
16 | }
--------------------------------------------------------------------------------
/prisma/schema/guild/levels.prisma:
--------------------------------------------------------------------------------
1 | model Levels {
2 | member_id BigInt
3 | xp Float @default(0)
4 | level BigInt @default(0)
5 | blacklisted Boolean @default(false)
6 | last_message DateTime @default(now())
7 | guild_id BigInt
8 | guild Guild @relation(fields: [guild_id], references: [guild_id])
9 |
10 | @@id([member_id, guild_id])
11 | @@unique([member_id, guild_id])
12 | @@index([member_id])
13 | }
--------------------------------------------------------------------------------
/prisma/schema/guild/starboard.prisma:
--------------------------------------------------------------------------------
1 | model Starboard {
2 | guild_id BigInt @id @unique
3 | starboard_channel_id BigInt
4 | starboard_emoji String
5 | starboard_threshold Int
6 | Guild Guild @relation(fields: [guild_id], references: [guild_id])
7 |
8 | @@index([guild_id])
9 | }
10 |
11 | model StarboardMessage {
12 | message_id BigInt @id
13 | message_content String
14 | message_created_at DateTime @default(now())
15 | message_expires_at DateTime
16 | message_channel_id BigInt
17 | message_user_id BigInt
18 | message_guild_id BigInt
19 | star_count Int @default(0)
20 | starboard_message_id BigInt
21 | Guild Guild @relation(fields: [message_guild_id], references: [guild_id])
22 |
23 | @@unique([message_id, message_guild_id])
24 | @@index([message_id, message_guild_id])
25 | }
--------------------------------------------------------------------------------
/prisma/schema/main.prisma:
--------------------------------------------------------------------------------
1 | generator client {
2 | provider = "prisma-client-py"
3 | recursive_type_depth = "-1"
4 | interface = "asyncio"
5 | previewFeatures = ["prismaSchemaFolder"]
6 | }
7 |
8 | datasource db {
9 | provider = "postgresql"
10 | url = env("DATABASE_URL")
11 | directUrl = env("DATABASE_URL")
12 | }
13 |
--------------------------------------------------------------------------------
/shell.nix:
--------------------------------------------------------------------------------
1 | { pkgs ? import {}, self ? null }:
2 |
3 | pkgs.mkShell {
4 | buildInputs = if self == null then [] else [
5 | self.packages.${pkgs.system}.envrc
6 | ];
7 |
8 | packages = with pkgs; [
9 | python313
10 | poetry
11 | git
12 | jq
13 | ];
14 |
15 | shellHook = ''
16 | # See perSystem.packages.envrc
17 | if command -v envrc >/dev/null 2>&1; then
18 | envrc
19 | fi
20 |
21 | # Enters the user's preferred shell using a more robust method
22 | $(getent passwd $(id -un) | cut -d: -f7 | tr -d '\n')
23 |
24 | # Exits after child shell exits
25 | exit
26 | '';
27 | }
28 |
--------------------------------------------------------------------------------
/tux/__init__.py:
--------------------------------------------------------------------------------
1 | from importlib import metadata
2 |
3 | # Dynamically get the version from pyproject.toml
4 | __version__: str = metadata.version("tux")
5 |
--------------------------------------------------------------------------------
/tux/cli/__init__.py:
--------------------------------------------------------------------------------
1 | """Command-line interface for Tux development tools.
2 |
3 | This module provides a modern command-line interface using Click.
4 | """
5 |
6 | # Import cli and main directly from core
7 | from tux.cli.core import cli, main
8 |
9 | __all__ = ["cli", "main"]
10 |
--------------------------------------------------------------------------------
/tux/cli/database.py:
--------------------------------------------------------------------------------
1 | """Database commands for the Tux CLI."""
2 |
3 | import os
4 | from collections.abc import Callable
5 | from typing import TypeVar
6 |
7 | from loguru import logger
8 |
9 | from tux.cli.core import command_registration_decorator, create_group, run_command
10 | from tux.utils.env import get_database_url
11 |
12 | # Type for command functions
13 | T = TypeVar("T")
14 | CommandFunction = Callable[[], int]
15 |
16 |
17 | # Helper function moved from impl/database.py
18 | def _run_prisma_command(args: list[str], env: dict[str, str]) -> int:
19 | """
20 | Run a Prisma command directly.
21 |
22 | When using 'poetry run tux', the prisma binary is already
23 | properly configured, so we can run it directly.
24 | """
25 |
26 | logger.info(f"Using database URL: {env['DATABASE_URL']}")
27 |
28 | # Set the environment variables for the process
29 | env_vars = os.environ | env
30 |
31 | # Use prisma directly - it's already available through Poetry
32 | try:
33 | logger.info(f"Running: prisma {' '.join(args)}")
34 | return run_command(["prisma", *args], env=env_vars)
35 |
36 | except Exception as e:
37 | logger.error(f"Error running prisma command: {e}")
38 | return 1
39 |
40 |
41 | # Create the database command group
42 | db_group = create_group("db", "Database management commands")
43 |
44 |
45 | @command_registration_decorator(db_group, name="generate")
46 | def generate() -> int:
47 | """Generate Prisma client."""
48 |
49 | env = {"DATABASE_URL": get_database_url()}
50 | return _run_prisma_command(["generate"], env=env)
51 |
52 |
53 | @command_registration_decorator(db_group, name="push")
54 | def push() -> int:
55 | """Push schema changes to database."""
56 |
57 | env = {"DATABASE_URL": get_database_url()}
58 | return _run_prisma_command(["db", "push"], env=env)
59 |
60 |
61 | @command_registration_decorator(db_group, name="pull")
62 | def pull() -> int:
63 | """Pull schema from database."""
64 |
65 | env = {"DATABASE_URL": get_database_url()}
66 | return _run_prisma_command(["db", "pull"], env=env)
67 |
68 |
69 | @command_registration_decorator(db_group, name="migrate")
70 | def migrate() -> int:
71 | """Run database migrations."""
72 |
73 | env = {"DATABASE_URL": get_database_url()}
74 | return _run_prisma_command(["migrate", "dev"], env=env)
75 |
76 |
77 | @command_registration_decorator(db_group, name="reset")
78 | def reset() -> int:
79 | """Reset database."""
80 |
81 | env = {"DATABASE_URL": get_database_url()}
82 | return _run_prisma_command(["migrate", "reset"], env=env)
83 |
--------------------------------------------------------------------------------
/tux/cli/dev.py:
--------------------------------------------------------------------------------
1 | """Development tools and utilities for Tux."""
2 |
3 | from tux.cli.core import (
4 | command_registration_decorator,
5 | create_group,
6 | run_command,
7 | )
8 |
9 | # Create the dev command group
10 | dev_group = create_group("dev", "Development tools")
11 |
12 |
13 | @command_registration_decorator(dev_group, name="lint")
14 | def lint() -> int:
15 | """Run linting with Ruff."""
16 | return run_command(["ruff", "check", "."])
17 |
18 |
19 | @command_registration_decorator(dev_group, name="lint-fix")
20 | def lint_fix() -> int:
21 | """Run linting with Ruff and apply fixes."""
22 | return run_command(["ruff", "check", "--fix", "."])
23 |
24 |
25 | @command_registration_decorator(dev_group, name="format")
26 | def format_code() -> int:
27 | """Format code with Ruff."""
28 | return run_command(["ruff", "format", "."])
29 |
30 |
31 | @command_registration_decorator(dev_group, name="type-check")
32 | def type_check() -> int:
33 | """Check types with Pyright."""
34 | return run_command(["pyright"])
35 |
36 |
37 | @command_registration_decorator(dev_group, name="pre-commit")
38 | def check() -> int:
39 | """Run pre-commit checks."""
40 | return run_command(["pre-commit", "run", "--all-files"])
41 |
--------------------------------------------------------------------------------
/tux/cli/docker.py:
--------------------------------------------------------------------------------
1 | """Docker commands for the Tux CLI."""
2 |
3 | import click
4 | from loguru import logger
5 |
6 | from tux.cli.core import (
7 | command_registration_decorator,
8 | create_group,
9 | run_command,
10 | )
11 | from tux.utils.env import is_dev_mode
12 |
13 |
14 | # Helper function moved from impl/docker.py
15 | def _get_compose_base_cmd() -> list[str]:
16 | """Get the base docker compose command with appropriate -f flags."""
17 | base = ["docker", "compose", "-f", "docker-compose.yml"]
18 | if is_dev_mode():
19 | base.extend(["-f", "docker-compose.dev.yml"])
20 | return base
21 |
22 |
23 | # Create the docker command group
24 | docker_group = create_group("docker", "Docker management commands")
25 |
26 |
27 | @command_registration_decorator(docker_group, name="build")
28 | def build() -> int:
29 | """Build Docker images.
30 |
31 | Runs `docker compose build`.
32 | """
33 | cmd = [*_get_compose_base_cmd(), "build"]
34 | return run_command(cmd)
35 |
36 |
37 | @command_registration_decorator(docker_group, name="up")
38 | @click.option("-d", "--detach", is_flag=True, help="Run containers in the background.")
39 | @click.option("--build", is_flag=True, help="Build images before starting containers.")
40 | def up(detach: bool, build: bool) -> int:
41 | """Start Docker services.
42 |
43 | Runs `docker compose up`.
44 | Can optionally build images first with --build.
45 | """
46 | cmd = [*_get_compose_base_cmd(), "up"]
47 | if build:
48 | cmd.append("--build")
49 | if detach:
50 | cmd.append("-d")
51 | return run_command(cmd)
52 |
53 |
54 | @command_registration_decorator(docker_group, name="down")
55 | def down() -> int:
56 | """Stop Docker services.
57 |
58 | Runs `docker compose down`.
59 | """
60 | cmd = [*_get_compose_base_cmd(), "down"]
61 | return run_command(cmd)
62 |
63 |
64 | @command_registration_decorator(docker_group, name="logs")
65 | @click.option("-f", "--follow", is_flag=True, help="Follow log output.")
66 | @click.argument("service", default="tux", required=False)
67 | def logs(follow: bool, service: str) -> int:
68 | """Show logs for a Docker service.
69 |
70 | Runs `docker compose logs [service]`.
71 | """
72 | cmd = [*_get_compose_base_cmd(), "logs"]
73 | if follow:
74 | cmd.append("-f")
75 | cmd.append(service)
76 | return run_command(cmd)
77 |
78 |
79 | @command_registration_decorator(docker_group, name="ps")
80 | def ps() -> int:
81 | """List running Docker containers.
82 |
83 | Runs `docker compose ps`.
84 | """
85 | cmd = [*_get_compose_base_cmd(), "ps"]
86 | return run_command(cmd)
87 |
88 |
89 | @command_registration_decorator(docker_group, name="exec")
90 | @click.argument("service", default="tux", required=False)
91 | @click.argument("command", nargs=-1, required=True)
92 | def exec_cmd(service: str, command: tuple[str, ...]) -> int:
93 | """Execute a command inside a running service container.
94 |
95 | Runs `docker compose exec [service] [command]`.
96 | """
97 | if not command:
98 | logger.error("Error: No command provided to execute.")
99 | return 1
100 |
101 | cmd = [*_get_compose_base_cmd(), "exec", service, *command]
102 | return run_command(cmd)
103 |
--------------------------------------------------------------------------------
/tux/cli/docs.py:
--------------------------------------------------------------------------------
1 | """Documentation commands for the Tux CLI."""
2 |
3 | import pathlib
4 |
5 | from loguru import logger
6 |
7 | from tux.cli.core import (
8 | command_registration_decorator,
9 | create_group,
10 | run_command,
11 | )
12 |
13 | # Create the docs command group
14 | docs_group = create_group("docs", "Documentation related commands")
15 |
16 |
17 | def find_mkdocs_config() -> str:
18 | """Find the mkdocs.yml configuration file.
19 |
20 | Returns
21 | -------
22 | str
23 | Path to the mkdocs.yml file
24 | """
25 |
26 | current_dir = pathlib.Path.cwd()
27 |
28 | # Check if we're in the docs directory
29 | if (current_dir / "mkdocs.yml").exists():
30 | return "mkdocs.yml"
31 |
32 | # Check if we're in the root repo with docs subdirectory
33 | if (current_dir / "docs" / "mkdocs.yml").exists():
34 | return "docs/mkdocs.yml"
35 | logger.error("Can't find mkdocs.yml file. Please run from the project root or docs directory.")
36 |
37 | return ""
38 |
39 |
40 | @command_registration_decorator(docs_group, name="serve")
41 | def docs_serve() -> int:
42 | """Serve documentation locally."""
43 | if mkdocs_path := find_mkdocs_config():
44 | return run_command(["mkdocs", "serve", "--dirty", "-f", mkdocs_path])
45 | return 1
46 |
47 |
48 | @command_registration_decorator(docs_group, name="build")
49 | def docs_build() -> int:
50 | """Build documentation site."""
51 | if mkdocs_path := find_mkdocs_config():
52 | return run_command(["mkdocs", "build", "-f", mkdocs_path])
53 | return 1
54 |
--------------------------------------------------------------------------------
/tux/cli/ui.py:
--------------------------------------------------------------------------------
1 | """Terminal UI utilities for the CLI.
2 |
3 | This module provides rich formatting for terminal output.
4 | """
5 |
6 | from rich.console import Console
7 | from rich.table import Table
8 | from rich.text import Text
9 |
10 | # Create a shared console instance
11 | console = Console()
12 |
13 | # Styles for different types of messages
14 | SUCCESS_STYLE = "bold green"
15 | ERROR_STYLE = "bold red"
16 | WARNING_STYLE = "bold yellow"
17 | INFO_STYLE = "bold blue"
18 |
19 |
20 | def success(message: str) -> None:
21 | console.print(f"[{SUCCESS_STYLE}]✓[/] {message}")
22 |
23 |
24 | def error(message: str) -> None:
25 | console.print(f"[{ERROR_STYLE}]✗[/] {message}")
26 |
27 |
28 | def warning(message: str) -> None:
29 | console.print(f"[{WARNING_STYLE}]![/] {message}")
30 |
31 |
32 | def info(message: str) -> None:
33 | console.print(f"[{INFO_STYLE}]i[/] {message}")
34 |
35 |
36 | def command_header(group_name: str, command_name: str) -> None:
37 | """Print a header for a command."""
38 | text = Text()
39 |
40 | text.append("Running ", style="dim")
41 | text.append(f"{group_name}", style=INFO_STYLE)
42 | text.append(":")
43 | text.append(f"{command_name}", style=SUCCESS_STYLE)
44 |
45 | console.print(text)
46 |
47 |
48 | def command_result(is_success: bool, message: str = "") -> None:
49 | """Print the result of a command."""
50 |
51 | if is_success:
52 | if message:
53 | success(message)
54 |
55 | else:
56 | success("Command completed successfully")
57 |
58 | elif message:
59 | error(message)
60 |
61 | else:
62 | error("Command failed")
63 |
64 |
65 | def create_table(title: str, columns: list[str]) -> Table:
66 | """Create a rich table with the given title and columns."""
67 |
68 | table = Table(title=title)
69 |
70 | for column in columns:
71 | table.add_column(column)
72 |
73 | return table
74 |
--------------------------------------------------------------------------------
/tux/cogs/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/tux/cogs/__init__.py
--------------------------------------------------------------------------------
/tux/cogs/admin/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/tux/cogs/admin/__init__.py
--------------------------------------------------------------------------------
/tux/cogs/fun/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/tux/cogs/fun/__init__.py
--------------------------------------------------------------------------------
/tux/cogs/guild/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/tux/cogs/guild/__init__.py
--------------------------------------------------------------------------------
/tux/cogs/info/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/tux/cogs/info/__init__.py
--------------------------------------------------------------------------------
/tux/cogs/info/membercount.py:
--------------------------------------------------------------------------------
1 | import discord
2 | from discord import app_commands
3 | from discord.ext import commands
4 |
5 | from tux.bot import Tux
6 | from tux.ui.embeds import EmbedCreator
7 |
8 |
9 | class MemberCount(commands.Cog):
10 | def __init__(self, bot: Tux) -> None:
11 | self.bot = bot
12 |
13 | @app_commands.command(name="membercount", description="Shows server member count")
14 | async def membercount(self, interaction: discord.Interaction) -> None:
15 | """
16 | Show the member count for the server.
17 |
18 | Parameters
19 | ----------
20 | interaction : discord.Interaction
21 | The discord interaction object.
22 | """
23 |
24 | assert interaction.guild
25 |
26 | # Get the member count for the server (total members)
27 | members = interaction.guild.member_count
28 | # Get the number of humans in the server (subtract bots from total members)
29 | humans = sum(not member.bot for member in interaction.guild.members)
30 | # Get the number of bots in the server (subtract humans from total members)
31 | bots = sum(member.bot for member in interaction.guild.members if member.bot)
32 | # Get the number of staff members in the server
33 | staff_role = discord.utils.get(interaction.guild.roles, name="%wheel")
34 | staff = len(staff_role.members) if staff_role else 0
35 |
36 | embed = EmbedCreator.create_embed(
37 | bot=self.bot,
38 | embed_type=EmbedCreator.INFO,
39 | user_name=interaction.user.name,
40 | user_display_avatar=interaction.user.display_avatar.url,
41 | title="Member Count",
42 | description="Here is the member count for the server.",
43 | )
44 |
45 | embed.add_field(name="Members", value=str(members), inline=False)
46 | embed.add_field(name="Humans", value=str(humans), inline=True)
47 | embed.add_field(name="Bots", value=str(bots), inline=True)
48 | if staff > 0:
49 | embed.add_field(name="Staff", value=str(staff), inline=True)
50 |
51 | await interaction.response.send_message(embed=embed)
52 |
53 |
54 | async def setup(bot: Tux) -> None:
55 | await bot.add_cog(MemberCount(bot))
56 |
--------------------------------------------------------------------------------
/tux/cogs/levels/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/tux/cogs/levels/__init__.py
--------------------------------------------------------------------------------
/tux/cogs/levels/level.py:
--------------------------------------------------------------------------------
1 | import discord
2 | from discord.ext import commands
3 |
4 | from tux.bot import Tux
5 | from tux.cogs.services.levels import LevelsService
6 | from tux.database.controllers import DatabaseController
7 | from tux.ui.embeds import EmbedCreator, EmbedType
8 | from tux.utils.config import CONFIG
9 | from tux.utils.functions import generate_usage
10 |
11 |
12 | class Level(commands.Cog):
13 | def __init__(self, bot: Tux) -> None:
14 | self.bot = bot
15 | self.levels_service = LevelsService(bot)
16 | self.db = DatabaseController()
17 | self.level.usage = generate_usage(self.level)
18 |
19 | @commands.guild_only()
20 | @commands.hybrid_command(
21 | name="level",
22 | aliases=["lvl", "rank", "xp"],
23 | )
24 | async def level(self, ctx: commands.Context[Tux], member: discord.User | discord.Member | None = None) -> None:
25 | """
26 | Fetches the XP and level for a member (or the person who runs the command if no member is provided).
27 |
28 | Parameters
29 | ----------
30 | ctx : commands.Context[Tux]
31 | The context object for the command.
32 |
33 | member : discord.User
34 | The member to fetch XP and level for.
35 | """
36 |
37 | if ctx.guild is None:
38 | await ctx.send("This command can only be executed within a guild.")
39 | return
40 |
41 | if member is None:
42 | member = ctx.author
43 |
44 | xp: float = await self.db.levels.get_xp(member.id, ctx.guild.id)
45 | level: int = await self.db.levels.get_level(member.id, ctx.guild.id)
46 |
47 | if self.levels_service.enable_xp_cap and level >= self.levels_service.max_level:
48 | max_xp: float = self.levels_service.calculate_xp_for_level(self.levels_service.max_level)
49 | level_display: int = self.levels_service.max_level
50 | xp_display: str = f"{round(max_xp)} (limit reached)"
51 | else:
52 | level_display: int = level
53 | xp_display: str = f"{round(xp)}"
54 |
55 | if CONFIG.SHOW_XP_PROGRESS:
56 | xp_progress: int
57 | xp_required: int
58 | xp_progress, xp_required = self.levels_service.get_level_progress(xp, level)
59 | progress_bar: str = self.levels_service.generate_progress_bar(xp_progress, xp_required)
60 |
61 | embed: discord.Embed = EmbedCreator.create_embed(
62 | embed_type=EmbedType.DEFAULT,
63 | title=f"Level {level_display}",
64 | description=f"Progress to Next Level:\n{progress_bar}",
65 | custom_color=discord.Color.blurple(),
66 | custom_author_text=f"{member.name}",
67 | custom_author_icon_url=member.display_avatar.url,
68 | custom_footer_text=f"Total XP: {xp_display}",
69 | )
70 | else:
71 | embed: discord.Embed = EmbedCreator.create_embed(
72 | embed_type=EmbedType.DEFAULT,
73 | description=f"**Level {level_display}** - `XP: {xp_display}`",
74 | custom_color=discord.Color.blurple(),
75 | custom_author_text=f"{member.name}",
76 | custom_author_icon_url=member.display_avatar.url,
77 | )
78 |
79 | await ctx.send(embed=embed)
80 |
81 |
82 | async def setup(bot: Tux) -> None:
83 | await bot.add_cog(Level(bot))
84 |
--------------------------------------------------------------------------------
/tux/cogs/moderation/ban.py:
--------------------------------------------------------------------------------
1 | import discord
2 | from discord.ext import commands
3 |
4 | from prisma.enums import CaseType
5 | from tux.bot import Tux
6 | from tux.utils import checks
7 | from tux.utils.flags import BanFlags
8 | from tux.utils.functions import generate_usage
9 |
10 | from . import ModerationCogBase
11 |
12 |
13 | class Ban(ModerationCogBase):
14 | def __init__(self, bot: Tux) -> None:
15 | super().__init__(bot)
16 | self.ban.usage = generate_usage(self.ban, BanFlags)
17 |
18 | @commands.hybrid_command(name="ban", aliases=["b"])
19 | @commands.guild_only()
20 | @checks.has_pl(3)
21 | async def ban(
22 | self,
23 | ctx: commands.Context[Tux],
24 | member: discord.Member,
25 | *,
26 | flags: BanFlags,
27 | ) -> None:
28 | """
29 | Ban a member from the server.
30 |
31 | Parameters
32 | ----------
33 | ctx : commands.Context[Tux]
34 | The context in which the command is being invoked.
35 | member : discord.Member
36 | The member to ban.
37 | flags : BanFlags
38 | The flags for the command. (reason: str, purge: int (< 7), silent: bool)
39 |
40 | Raises
41 | ------
42 | discord.Forbidden
43 | If the bot is unable to ban the user.
44 | discord.HTTPException
45 | If an error occurs while banning the user.
46 | """
47 |
48 | assert ctx.guild
49 |
50 | # Check if moderator has permission to ban the member
51 | if not await self.check_conditions(ctx, member, ctx.author, "ban"):
52 | return
53 |
54 | # Execute ban with case creation and DM
55 | await self.execute_mod_action(
56 | ctx=ctx,
57 | case_type=CaseType.BAN,
58 | user=member,
59 | reason=flags.reason,
60 | silent=flags.silent,
61 | dm_action="banned",
62 | actions=[
63 | (ctx.guild.ban(member, reason=flags.reason, delete_message_seconds=flags.purge * 86400), type(None)),
64 | ],
65 | )
66 |
67 |
68 | async def setup(bot: Tux) -> None:
69 | await bot.add_cog(Ban(bot))
70 |
--------------------------------------------------------------------------------
/tux/cogs/moderation/clearafk.py:
--------------------------------------------------------------------------------
1 | import contextlib
2 |
3 | import discord
4 | from discord.ext import commands
5 |
6 | from tux.bot import Tux
7 | from tux.database.controllers import AfkController
8 | from tux.utils import checks
9 |
10 |
11 | class ClearAFK(commands.Cog):
12 | def __init__(self, bot: Tux) -> None:
13 | self.bot = bot
14 | self.db = AfkController()
15 | self.clear_afk.usage = "clearafk "
16 |
17 | @commands.hybrid_command(
18 | name="clearafk",
19 | aliases=["cafk", "removeafk"],
20 | description="Clear a member's AFK status and reset their nickname.",
21 | )
22 | @commands.guild_only()
23 | @checks.has_pl(2) # Ensure the user has the required permission level
24 | async def clear_afk(
25 | self,
26 | ctx: commands.Context[Tux],
27 | member: discord.Member,
28 | ) -> discord.Message:
29 | """
30 | Clear a member's AFK status and reset their nickname.
31 |
32 | Parameters
33 | ----------
34 | ctx : commands.Context[Tux]
35 | The context in which the command is being invoked.
36 | member : discord.Member
37 | The member whose AFK status is to be cleared.
38 | """
39 |
40 | assert ctx.guild
41 |
42 | if not await self.db.is_afk(member.id, guild_id=ctx.guild.id):
43 | return await ctx.send(f"{member.mention} is not currently AFK.", ephemeral=True)
44 |
45 | # Fetch the AFK entry to retrieve the original nickname
46 | entry = await self.db.get_afk_member(member.id, guild_id=ctx.guild.id)
47 |
48 | await self.db.remove_afk(member.id)
49 |
50 | if entry:
51 | if entry.nickname:
52 | with contextlib.suppress(discord.Forbidden):
53 | await member.edit(nick=entry.nickname) # Reset nickname to original
54 | if entry.enforced: # untimeout the user if the afk status is a self-timeout
55 | await member.timeout(None, reason="removing self-timeout")
56 |
57 | return await ctx.send(f"AFK status for {member.mention} has been cleared.", ephemeral=True)
58 |
59 |
60 | async def setup(bot: Tux) -> None:
61 | await bot.add_cog(ClearAFK(bot))
62 |
--------------------------------------------------------------------------------
/tux/cogs/moderation/kick.py:
--------------------------------------------------------------------------------
1 | import discord
2 | from discord.ext import commands
3 |
4 | from prisma.enums import CaseType
5 | from tux.bot import Tux
6 | from tux.utils import checks
7 | from tux.utils.flags import KickFlags
8 | from tux.utils.functions import generate_usage
9 |
10 | from . import ModerationCogBase
11 |
12 |
13 | class Kick(ModerationCogBase):
14 | def __init__(self, bot: Tux) -> None:
15 | super().__init__(bot)
16 | self.kick.usage = generate_usage(self.kick, KickFlags)
17 |
18 | @commands.hybrid_command(
19 | name="kick",
20 | aliases=["k"],
21 | )
22 | @commands.guild_only()
23 | @checks.has_pl(2)
24 | async def kick(
25 | self,
26 | ctx: commands.Context[Tux],
27 | member: discord.Member,
28 | *,
29 | flags: KickFlags,
30 | ) -> None:
31 | """
32 | Kick a member from the server.
33 |
34 | Parameters
35 | ----------
36 | ctx : commands.Context[Tux]
37 | The context in which the command is being invoked.
38 | member : discord.Member
39 | The member to kick.
40 | flags : KickFlags
41 | The flags for the command. (reason: str, silent: bool)
42 |
43 | Raises
44 | ------
45 | discord.Forbidden
46 | If the bot is unable to kick the user.
47 | discord.HTTPException
48 | If an error occurs while kicking the user.
49 | """
50 | assert ctx.guild
51 |
52 | # Check if moderator has permission to kick the member
53 | if not await self.check_conditions(ctx, member, ctx.author, "kick"):
54 | return
55 |
56 | # Execute kick with case creation and DM
57 | await self.execute_mod_action(
58 | ctx=ctx,
59 | case_type=CaseType.KICK,
60 | user=member,
61 | reason=flags.reason,
62 | silent=flags.silent,
63 | dm_action="kicked",
64 | actions=[(ctx.guild.kick(member, reason=flags.reason), type(None))],
65 | )
66 |
67 |
68 | async def setup(bot: Tux) -> None:
69 | await bot.add_cog(Kick(bot))
70 |
--------------------------------------------------------------------------------
/tux/cogs/moderation/pollban.py:
--------------------------------------------------------------------------------
1 | import discord
2 | from discord.ext import commands
3 |
4 | from prisma.enums import CaseType
5 | from tux.bot import Tux
6 | from tux.utils import checks
7 | from tux.utils.flags import PollBanFlags
8 | from tux.utils.functions import generate_usage
9 |
10 | from . import ModerationCogBase
11 |
12 |
13 | class PollBan(ModerationCogBase):
14 | def __init__(self, bot: Tux) -> None:
15 | super().__init__(bot)
16 | self.poll_ban.usage = generate_usage(self.poll_ban, PollBanFlags)
17 |
18 | @commands.hybrid_command(
19 | name="pollban",
20 | aliases=["pb"],
21 | )
22 | @commands.guild_only()
23 | @checks.has_pl(3)
24 | async def poll_ban(
25 | self,
26 | ctx: commands.Context[Tux],
27 | member: discord.Member,
28 | *,
29 | flags: PollBanFlags,
30 | ) -> None:
31 | """
32 | Ban a user from creating polls.
33 |
34 | Parameters
35 | ----------
36 | ctx : commands.Context[Tux]
37 | The context object.
38 | member : discord.Member
39 | The member to poll ban.
40 | flags : PollBanFlags
41 | The flags for the command. (reason: str, silent: bool)
42 | """
43 | assert ctx.guild
44 |
45 | # Check if user is already poll banned
46 | if await self.is_pollbanned(ctx.guild.id, member.id):
47 | await ctx.send("User is already poll banned.", ephemeral=True)
48 | return
49 |
50 | # Check if moderator has permission to poll ban the member
51 | if not await self.check_conditions(ctx, member, ctx.author, "poll ban"):
52 | return
53 |
54 | # Execute poll ban with case creation and DM
55 | await self.execute_mod_action(
56 | ctx=ctx,
57 | case_type=CaseType.POLLBAN,
58 | user=member,
59 | reason=flags.reason,
60 | silent=flags.silent,
61 | dm_action="poll banned",
62 | # Use dummy coroutine for actions that don't need Discord API calls
63 | actions=[(self._dummy_action(), type(None))],
64 | )
65 |
66 |
67 | async def setup(bot: Tux) -> None:
68 | await bot.add_cog(PollBan(bot))
69 |
--------------------------------------------------------------------------------
/tux/cogs/moderation/pollunban.py:
--------------------------------------------------------------------------------
1 | import discord
2 | from discord.ext import commands
3 |
4 | from prisma.enums import CaseType
5 | from tux.bot import Tux
6 | from tux.utils import checks
7 | from tux.utils.flags import PollUnbanFlags
8 | from tux.utils.functions import generate_usage
9 |
10 | from . import ModerationCogBase
11 |
12 |
13 | class PollUnban(ModerationCogBase):
14 | def __init__(self, bot: Tux) -> None:
15 | super().__init__(bot)
16 | self.poll_unban.usage = generate_usage(self.poll_unban, PollUnbanFlags)
17 |
18 | @commands.hybrid_command(
19 | name="pollunban",
20 | aliases=["pub"],
21 | )
22 | @commands.guild_only()
23 | @checks.has_pl(3)
24 | async def poll_unban(
25 | self,
26 | ctx: commands.Context[Tux],
27 | member: discord.Member,
28 | *,
29 | flags: PollUnbanFlags,
30 | ) -> None:
31 | """
32 | Remove a poll ban from a member.
33 |
34 | Parameters
35 | ----------
36 | ctx : commands.Context[Tux]
37 | The context object.
38 | member : discord.Member
39 | The member to remove poll ban from.
40 | flags : PollUnbanFlags
41 | The flags for the command. (reason: str, silent: bool)
42 | """
43 | assert ctx.guild
44 |
45 | # Check if user is poll banned
46 | if not await self.is_pollbanned(ctx.guild.id, member.id):
47 | await ctx.send("User is not poll banned.", ephemeral=True)
48 | return
49 |
50 | # Check if moderator has permission to poll unban the member
51 | if not await self.check_conditions(ctx, member, ctx.author, "poll unban"):
52 | return
53 |
54 | # Execute poll unban with case creation and DM
55 | await self.execute_mod_action(
56 | ctx=ctx,
57 | case_type=CaseType.POLLUNBAN,
58 | user=member,
59 | reason=flags.reason,
60 | silent=flags.silent,
61 | dm_action="poll unbanned",
62 | # Use dummy coroutine for actions that don't need Discord API calls
63 | actions=[(self._dummy_action(), type(None))],
64 | )
65 |
66 |
67 | async def setup(bot: Tux) -> None:
68 | await bot.add_cog(PollUnban(bot))
69 |
--------------------------------------------------------------------------------
/tux/cogs/moderation/report.py:
--------------------------------------------------------------------------------
1 | import discord
2 | from discord import app_commands
3 | from discord.ext import commands
4 |
5 | from tux.bot import Tux
6 | from tux.ui.modals.report import ReportModal
7 |
8 |
9 | class Report(commands.Cog):
10 | def __init__(self, bot: Tux) -> None:
11 | self.bot = bot
12 |
13 | @app_commands.command(name="report")
14 | @app_commands.guild_only()
15 | async def report(self, interaction: discord.Interaction) -> None:
16 | """
17 | Report a user or issue anonymously
18 |
19 | Parameters
20 | ----------
21 | interaction : discord.Interaction
22 | The interaction that triggered the command.
23 | """
24 |
25 | modal = ReportModal(bot=self.bot)
26 |
27 | await interaction.response.send_modal(modal)
28 |
29 |
30 | async def setup(bot: Tux) -> None:
31 | await bot.add_cog(Report(bot))
32 |
--------------------------------------------------------------------------------
/tux/cogs/moderation/snippetban.py:
--------------------------------------------------------------------------------
1 | import discord
2 | from discord.ext import commands
3 |
4 | from prisma.enums import CaseType
5 | from tux.bot import Tux
6 | from tux.utils import checks
7 | from tux.utils.flags import SnippetBanFlags
8 | from tux.utils.functions import generate_usage
9 |
10 | from . import ModerationCogBase
11 |
12 |
13 | class SnippetBan(ModerationCogBase):
14 | def __init__(self, bot: Tux) -> None:
15 | super().__init__(bot)
16 | self.snippet_ban.usage = generate_usage(self.snippet_ban, SnippetBanFlags)
17 |
18 | @commands.hybrid_command(
19 | name="snippetban",
20 | aliases=["sb"],
21 | )
22 | @commands.guild_only()
23 | @checks.has_pl(3)
24 | async def snippet_ban(
25 | self,
26 | ctx: commands.Context[Tux],
27 | member: discord.Member,
28 | *,
29 | flags: SnippetBanFlags,
30 | ) -> None:
31 | """
32 | Ban a member from creating snippets.
33 |
34 | Parameters
35 | ----------
36 | ctx : commands.Context[Tux]
37 | The context object.
38 | member : discord.Member
39 | The member to snippet ban.
40 | flags : SnippetBanFlags
41 | The flags for the command. (reason: str, silent: bool)
42 | """
43 | assert ctx.guild
44 |
45 | # Check if user is already snippet banned
46 | if await self.is_snippetbanned(ctx.guild.id, member.id):
47 | await ctx.send("User is already snippet banned.", ephemeral=True)
48 | return
49 |
50 | # Check if moderator has permission to snippet ban the member
51 | if not await self.check_conditions(ctx, member, ctx.author, "snippet ban"):
52 | return
53 |
54 | # Execute snippet ban with case creation and DM
55 | await self.execute_mod_action(
56 | ctx=ctx,
57 | case_type=CaseType.SNIPPETBAN,
58 | user=member,
59 | reason=flags.reason,
60 | silent=flags.silent,
61 | dm_action="snippet banned",
62 | # Use dummy coroutine for actions that don't need Discord API calls
63 | actions=[(self._dummy_action(), type(None))],
64 | )
65 |
66 |
67 | async def setup(bot: Tux) -> None:
68 | await bot.add_cog(SnippetBan(bot))
69 |
--------------------------------------------------------------------------------
/tux/cogs/moderation/snippetunban.py:
--------------------------------------------------------------------------------
1 | import discord
2 | from discord.ext import commands
3 |
4 | from prisma.enums import CaseType
5 | from tux.bot import Tux
6 | from tux.utils import checks
7 | from tux.utils.flags import SnippetUnbanFlags
8 | from tux.utils.functions import generate_usage
9 |
10 | from . import ModerationCogBase
11 |
12 |
13 | class SnippetUnban(ModerationCogBase):
14 | def __init__(self, bot: Tux) -> None:
15 | super().__init__(bot)
16 | self.snippet_unban.usage = generate_usage(self.snippet_unban, SnippetUnbanFlags)
17 |
18 | @commands.hybrid_command(
19 | name="snippetunban",
20 | aliases=["sub"],
21 | )
22 | @commands.guild_only()
23 | @checks.has_pl(3)
24 | async def snippet_unban(
25 | self,
26 | ctx: commands.Context[Tux],
27 | member: discord.Member,
28 | *,
29 | flags: SnippetUnbanFlags,
30 | ) -> None:
31 | """
32 | Remove a snippet ban from a member.
33 |
34 | Parameters
35 | ----------
36 | ctx : commands.Context[Tux]
37 | The context object.
38 | member : discord.Member
39 | The member to remove snippet ban from.
40 | flags : SnippetUnbanFlags
41 | The flags for the command. (reason: str, silent: bool)
42 | """
43 | assert ctx.guild
44 |
45 | # Check if user is snippet banned
46 | if not await self.is_snippetbanned(ctx.guild.id, member.id):
47 | await ctx.send("User is not snippet banned.", ephemeral=True)
48 | return
49 |
50 | # Check if moderator has permission to snippet unban the member
51 | if not await self.check_conditions(ctx, member, ctx.author, "snippet unban"):
52 | return
53 |
54 | # Execute snippet unban with case creation and DM
55 | await self.execute_mod_action(
56 | ctx=ctx,
57 | case_type=CaseType.SNIPPETUNBAN,
58 | user=member,
59 | reason=flags.reason,
60 | silent=flags.silent,
61 | dm_action="snippet unbanned",
62 | # Use dummy coroutine for actions that don't need Discord API calls
63 | actions=[(self._dummy_action(), type(None))],
64 | )
65 |
66 |
67 | async def setup(bot: Tux) -> None:
68 | await bot.add_cog(SnippetUnban(bot))
69 |
--------------------------------------------------------------------------------
/tux/cogs/moderation/timeout.py:
--------------------------------------------------------------------------------
1 | import datetime
2 |
3 | import discord
4 | from discord.ext import commands
5 |
6 | from prisma.enums import CaseType
7 | from tux.bot import Tux
8 | from tux.utils import checks
9 | from tux.utils.flags import TimeoutFlags
10 | from tux.utils.functions import generate_usage, parse_time_string
11 |
12 | from . import ModerationCogBase
13 |
14 |
15 | class Timeout(ModerationCogBase):
16 | def __init__(self, bot: Tux) -> None:
17 | super().__init__(bot)
18 | self.timeout.usage = generate_usage(self.timeout, TimeoutFlags)
19 |
20 | @commands.hybrid_command(
21 | name="timeout",
22 | aliases=["t", "to", "mute", "m"],
23 | )
24 | @commands.guild_only()
25 | @checks.has_pl(2)
26 | async def timeout(
27 | self,
28 | ctx: commands.Context[Tux],
29 | member: discord.Member,
30 | *,
31 | flags: TimeoutFlags,
32 | ) -> None:
33 | """
34 | Timeout a member from the server.
35 |
36 | Parameters
37 | ----------
38 | ctx : commands.Context[Tux]
39 | The context in which the command is being invoked.
40 | member : discord.Member
41 | The member to timeout.
42 | flags : TimeoutFlags
43 | The flags for the command (duration: str, silent: bool).
44 |
45 | Raises
46 | ------
47 | discord.DiscordException
48 | If an error occurs while timing out the user.
49 | """
50 | assert ctx.guild
51 |
52 | # Check if member is already timed out
53 | if member.is_timed_out():
54 | await ctx.send(f"{member} is already timed out.", ephemeral=True)
55 | return
56 |
57 | # Check if moderator has permission to timeout the member
58 | if not await self.check_conditions(ctx, member, ctx.author, "timeout"):
59 | return
60 |
61 | # Parse and validate duration
62 | try:
63 | duration = parse_time_string(flags.duration)
64 |
65 | # Discord maximum timeout duration is 28 days
66 | max_duration = datetime.timedelta(days=28)
67 | if duration > max_duration:
68 | await ctx.send(
69 | "Timeout duration exceeds Discord's maximum of 28 days. Setting timeout to maximum allowed (28 days).",
70 | ephemeral=True,
71 | )
72 | duration = max_duration
73 | # Update the display duration for consistency
74 | flags.duration = "28d"
75 | except ValueError as e:
76 | await ctx.send(f"Invalid duration format: {e}", ephemeral=True)
77 | return
78 |
79 | # Execute timeout with case creation and DM
80 | await self.execute_mod_action(
81 | ctx=ctx,
82 | case_type=CaseType.TIMEOUT,
83 | user=member,
84 | reason=flags.reason,
85 | silent=flags.silent,
86 | dm_action=f"timed out for {flags.duration}",
87 | actions=[(member.timeout(duration, reason=flags.reason), type(None))],
88 | duration=flags.duration,
89 | )
90 |
91 |
92 | async def setup(bot: Tux) -> None:
93 | await bot.add_cog(Timeout(bot))
94 |
--------------------------------------------------------------------------------
/tux/cogs/moderation/untimeout.py:
--------------------------------------------------------------------------------
1 | import discord
2 | from discord.ext import commands
3 |
4 | from prisma.enums import CaseType
5 | from tux.bot import Tux
6 | from tux.utils import checks
7 | from tux.utils.flags import UntimeoutFlags
8 | from tux.utils.functions import generate_usage
9 |
10 | from . import ModerationCogBase
11 |
12 |
13 | class Untimeout(ModerationCogBase):
14 | def __init__(self, bot: Tux) -> None:
15 | super().__init__(bot)
16 | self.untimeout.usage = generate_usage(self.untimeout, UntimeoutFlags)
17 |
18 | @commands.hybrid_command(
19 | name="untimeout",
20 | aliases=["ut", "uto", "unmute"],
21 | )
22 | @commands.guild_only()
23 | @checks.has_pl(2)
24 | async def untimeout(
25 | self,
26 | ctx: commands.Context[Tux],
27 | member: discord.Member,
28 | *,
29 | flags: UntimeoutFlags,
30 | ) -> None:
31 | """
32 | Remove timeout from a member.
33 |
34 | Parameters
35 | ----------
36 | ctx : commands.Context[Tux]
37 | The context in which the command is being invoked.
38 | member : discord.Member
39 | The member to remove timeout from.
40 | flags : UntimeoutFlags
41 | The flags for the command. (reason: str, silent: bool)
42 |
43 | Raises
44 | ------
45 | discord.DiscordException
46 | If an error occurs while removing the timeout.
47 | """
48 | assert ctx.guild
49 |
50 | # Check if member is timed out
51 | if not member.is_timed_out():
52 | await ctx.send(f"{member} is not timed out.", ephemeral=True)
53 | return
54 |
55 | # Check if moderator has permission to untimeout the member
56 | if not await self.check_conditions(ctx, member, ctx.author, "untimeout"):
57 | return
58 |
59 | # Execute untimeout with case creation and DM
60 | await self.execute_mod_action(
61 | ctx=ctx,
62 | case_type=CaseType.UNTIMEOUT,
63 | user=member,
64 | reason=flags.reason,
65 | silent=flags.silent,
66 | dm_action="removed from timeout",
67 | actions=[(member.timeout(None, reason=flags.reason), type(None))],
68 | )
69 |
70 |
71 | async def setup(bot: Tux) -> None:
72 | await bot.add_cog(Untimeout(bot))
73 |
--------------------------------------------------------------------------------
/tux/cogs/moderation/warn.py:
--------------------------------------------------------------------------------
1 | import discord
2 | from discord.ext import commands
3 |
4 | from prisma.enums import CaseType
5 | from tux.bot import Tux
6 | from tux.utils import checks
7 | from tux.utils.flags import WarnFlags
8 | from tux.utils.functions import generate_usage
9 |
10 | from . import ModerationCogBase
11 |
12 |
13 | class Warn(ModerationCogBase):
14 | def __init__(self, bot: Tux) -> None:
15 | super().__init__(bot)
16 | self.warn.usage = generate_usage(self.warn, WarnFlags)
17 |
18 | @commands.hybrid_command(
19 | name="warn",
20 | aliases=["w"],
21 | )
22 | @commands.guild_only()
23 | @checks.has_pl(2)
24 | async def warn(
25 | self,
26 | ctx: commands.Context[Tux],
27 | member: discord.Member,
28 | *,
29 | flags: WarnFlags,
30 | ) -> None:
31 | """
32 | Warn a member from the server.
33 |
34 | Parameters
35 | ----------
36 | ctx : commands.Context[Tux]
37 | The context in which the command is being invoked.
38 | member : discord.Member
39 | The member to warn.
40 | flags : WarnFlags
41 | The flags for the command. (reason: str, silent: bool)
42 | """
43 | assert ctx.guild
44 |
45 | # Check if moderator has permission to warn the member
46 | if not await self.check_conditions(ctx, member, ctx.author, "warn"):
47 | return
48 |
49 | # Execute warn with case creation and DM
50 | await self.execute_mod_action(
51 | ctx=ctx,
52 | case_type=CaseType.WARN,
53 | user=member,
54 | reason=flags.reason,
55 | silent=flags.silent,
56 | dm_action="warned",
57 | # Use dummy coroutine for actions that don't need Discord API calls
58 | actions=[(self._dummy_action(), type(None))],
59 | )
60 |
61 |
62 | async def setup(bot: Tux) -> None:
63 | await bot.add_cog(Warn(bot))
64 |
--------------------------------------------------------------------------------
/tux/cogs/services/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/tux/cogs/services/__init__.py
--------------------------------------------------------------------------------
/tux/cogs/snippets/delete_snippet.py:
--------------------------------------------------------------------------------
1 | from discord.ext import commands
2 | from loguru import logger
3 |
4 | from tux.bot import Tux
5 | from tux.utils.constants import CONST
6 | from tux.utils.functions import generate_usage
7 |
8 | from . import SnippetsBaseCog
9 |
10 |
11 | class DeleteSnippet(SnippetsBaseCog):
12 | def __init__(self, bot: Tux) -> None:
13 | super().__init__(bot)
14 | self.delete_snippet.usage = generate_usage(self.delete_snippet)
15 |
16 | @commands.command(
17 | name="deletesnippet",
18 | aliases=["ds"],
19 | )
20 | @commands.guild_only()
21 | async def delete_snippet(self, ctx: commands.Context[Tux], name: str) -> None:
22 | """Delete a snippet by name.
23 |
24 | Checks for ownership and lock status before deleting.
25 |
26 | Parameters
27 | ----------
28 | ctx : commands.Context[Tux]
29 | The context of the command.
30 | name : str
31 | The name of the snippet to delete.
32 | """
33 | assert ctx.guild
34 |
35 | # Fetch the snippet, send error if not found
36 | snippet = await self._get_snippet_or_error(ctx, name)
37 | if not snippet:
38 | return
39 |
40 | # Check permissions (role, ban, lock, ownership)
41 | can_delete, reason = await self.snippet_check(
42 | ctx,
43 | snippet_locked=snippet.locked,
44 | snippet_user_id=snippet.snippet_user_id,
45 | )
46 |
47 | if not can_delete:
48 | await self.send_snippet_error(ctx, description=reason)
49 | return
50 |
51 | # Delete the snippet
52 | await self.db.snippet.delete_snippet_by_id(snippet.snippet_id)
53 |
54 | await ctx.send("Snippet deleted.", delete_after=CONST.DEFAULT_DELETE_AFTER, ephemeral=True)
55 |
56 | logger.info(f"{ctx.author} deleted snippet '{name}'. Override: {reason}")
57 |
58 |
59 | async def setup(bot: Tux) -> None:
60 | """Load the DeleteSnippet cog."""
61 | await bot.add_cog(DeleteSnippet(bot))
62 |
--------------------------------------------------------------------------------
/tux/cogs/snippets/edit_snippet.py:
--------------------------------------------------------------------------------
1 | from discord.ext import commands
2 | from loguru import logger
3 |
4 | from tux.bot import Tux
5 | from tux.utils.constants import CONST
6 | from tux.utils.functions import generate_usage
7 |
8 | from . import SnippetsBaseCog
9 |
10 |
11 | class EditSnippet(SnippetsBaseCog):
12 | def __init__(self, bot: Tux) -> None:
13 | super().__init__(bot)
14 | self.edit_snippet.usage = generate_usage(self.edit_snippet)
15 |
16 | @commands.command(
17 | name="editsnippet",
18 | aliases=["es"],
19 | )
20 | @commands.guild_only()
21 | async def edit_snippet(self, ctx: commands.Context[Tux], name: str, *, content: str) -> None:
22 | """Edit an existing snippet.
23 |
24 | Checks for ownership and lock status before editing.
25 |
26 | Parameters
27 | ----------
28 | ctx : commands.Context[Tux]
29 | The context of the command.
30 | name : str
31 | The name of the snippet to edit.
32 | content : str
33 | The new content for the snippet.
34 | """
35 | assert ctx.guild
36 |
37 | # Fetch the snippet, send error if not found
38 | snippet = await self._get_snippet_or_error(ctx, name)
39 |
40 | if not snippet:
41 | return
42 |
43 | # Check permissions (role, ban, lock, ownership)
44 | can_edit, reason = await self.snippet_check(
45 | ctx,
46 | snippet_locked=snippet.locked,
47 | snippet_user_id=snippet.snippet_user_id,
48 | )
49 |
50 | if not can_edit:
51 | await self.send_snippet_error(ctx, description=reason)
52 | return
53 |
54 | # Update the snippet content
55 | await self.db.snippet.update_snippet_by_id(
56 | snippet_id=snippet.snippet_id,
57 | snippet_content=content,
58 | )
59 |
60 | await ctx.send("Snippet edited.", delete_after=CONST.DEFAULT_DELETE_AFTER, ephemeral=True)
61 |
62 | logger.info(f"{ctx.author} edited snippet '{name}'. Override: {reason}")
63 |
64 |
65 | async def setup(bot: Tux) -> None:
66 | """Load the EditSnippet cog."""
67 | await bot.add_cog(EditSnippet(bot))
68 |
--------------------------------------------------------------------------------
/tux/cogs/snippets/get_snippet_info.py:
--------------------------------------------------------------------------------
1 | from datetime import UTC, datetime
2 |
3 | import discord
4 | from discord.ext import commands
5 |
6 | from tux.bot import Tux
7 | from tux.ui.embeds import EmbedCreator
8 | from tux.utils.functions import generate_usage, truncate
9 |
10 | from . import SnippetsBaseCog
11 |
12 |
13 | class SnippetInfo(SnippetsBaseCog):
14 | def __init__(self, bot: Tux) -> None:
15 | super().__init__(bot)
16 | self.snippet_info.usage = generate_usage(self.snippet_info)
17 |
18 | @commands.command(
19 | name="snippetinfo",
20 | aliases=["si"],
21 | )
22 | @commands.guild_only()
23 | async def snippet_info(self, ctx: commands.Context[Tux], name: str) -> None:
24 | """Display detailed information about a snippet.
25 |
26 | Shows the author, creation date, content/alias target, uses, and lock status.
27 |
28 | Parameters
29 | ----------
30 | ctx : commands.Context[Tux]
31 | The context of the command.
32 | name : str
33 | The name of the snippet to get information about.
34 | """
35 | assert ctx.guild
36 |
37 | # Fetch the snippet, send error if not found
38 | snippet = await self._get_snippet_or_error(ctx, name)
39 | if not snippet:
40 | return
41 |
42 | # Attempt to resolve author, default to showing ID if not found
43 | author = self.bot.get_user(snippet.snippet_user_id)
44 | author_display = author.mention if author else f"<@!{snippet.snippet_user_id}> (Not found)"
45 |
46 | # Attempt to get aliases if any
47 | aliases = [alias.snippet_name for alias in (await self.db.snippet.get_all_aliases(name, ctx.guild.id))]
48 |
49 | # Determine content field details
50 | content_field_name = "Alias Target" if snippet.alias else "Content Preview"
51 | content_field_value = f"{snippet.alias or snippet.snippet_content}"
52 |
53 | # Create and populate the info embed
54 | embed: discord.Embed = EmbedCreator.create_embed(
55 | bot=self.bot,
56 | embed_type=EmbedCreator.DEFAULT,
57 | user_name=ctx.author.name,
58 | user_display_avatar=ctx.author.display_avatar.url,
59 | title="Snippet Information",
60 | message_timestamp=snippet.snippet_created_at or datetime.fromtimestamp(0, UTC),
61 | )
62 |
63 | embed.add_field(name="Name", value=snippet.snippet_name, inline=True)
64 | embed.add_field(name="Aliases", value=", ".join(f"`{alias}`" for alias in aliases), inline=True)
65 | embed.add_field(name="Author", value=author_display, inline=True)
66 | embed.add_field(name="Uses", value=str(snippet.uses), inline=True) # Ensure string
67 | embed.add_field(name="Locked", value="Yes" if snippet.locked else "No", inline=True)
68 |
69 | embed.add_field(
70 | name=content_field_name,
71 | value=f"> -# {truncate(text=content_field_value, length=256)}",
72 | inline=False,
73 | )
74 |
75 | await ctx.send(embed=embed)
76 |
77 |
78 | async def setup(bot: Tux) -> None:
79 | """Load the SnippetInfo cog."""
80 | await bot.add_cog(SnippetInfo(bot))
81 |
--------------------------------------------------------------------------------
/tux/cogs/tools/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/tux/cogs/tools/__init__.py
--------------------------------------------------------------------------------
/tux/cogs/utility/__init__.py:
--------------------------------------------------------------------------------
1 | import contextlib
2 | from datetime import datetime
3 | from types import NoneType
4 |
5 | import discord
6 |
7 | from tux.database.controllers import DatabaseController
8 | from tux.utils.constants import CONST
9 |
10 | __all__ = ("add_afk", "del_afk")
11 |
12 |
13 | def _generate_afk_nickname(display_name: str) -> str:
14 | """Generates the AFK nickname, handling truncation if necessary."""
15 | prefix_len = len(CONST.AFK_PREFIX)
16 |
17 | if len(display_name) >= CONST.NICKNAME_MAX_LENGTH - prefix_len:
18 | suffix_len = len(CONST.AFK_TRUNCATION_SUFFIX)
19 | available_space = CONST.NICKNAME_MAX_LENGTH - prefix_len - suffix_len
20 | truncated_name = f"{display_name[:available_space]}{CONST.AFK_TRUNCATION_SUFFIX}"
21 |
22 | return f"{CONST.AFK_PREFIX}{truncated_name}"
23 |
24 | return f"{CONST.AFK_PREFIX}{display_name}"
25 |
26 |
27 | async def add_afk(
28 | db: DatabaseController,
29 | reason: str,
30 | target: discord.Member,
31 | guild_id: int,
32 | is_perm: bool,
33 | until: datetime | NoneType | None = None,
34 | enforced: bool = False,
35 | ) -> None:
36 | """Sets a member as AFK, updates their nickname, and saves to the database."""
37 | new_name = _generate_afk_nickname(target.display_name)
38 |
39 | await db.afk.set_afk(target.id, target.display_name, reason, guild_id, is_perm, until, enforced)
40 |
41 | # Suppress Forbidden errors if the bot doesn't have permission to change the nickname
42 | with contextlib.suppress(discord.Forbidden):
43 | await target.edit(nick=new_name)
44 |
45 |
46 | async def del_afk(db: DatabaseController, target: discord.Member, nickname: str) -> None:
47 | """Removes a member's AFK status, restores their nickname, and updates the database."""
48 | await db.afk.remove_afk(target.id)
49 |
50 | # Suppress Forbidden errors if the bot doesn't have permission to change the nickname
51 | with contextlib.suppress(discord.Forbidden):
52 | # Only attempt to restore nickname if it was actually changed by add_afk
53 | # Prevents resetting a manually changed nickname if del_afk is called unexpectedly
54 | if target.display_name.startswith(CONST.AFK_PREFIX):
55 | await target.edit(nick=nickname)
56 |
--------------------------------------------------------------------------------
/tux/cogs/utility/ping.py:
--------------------------------------------------------------------------------
1 | import psutil
2 | from discord.ext import commands
3 |
4 | from tux.bot import Tux
5 | from tux.ui.embeds import EmbedCreator
6 | from tux.utils.functions import generate_usage
7 |
8 |
9 | class Ping(commands.Cog):
10 | def __init__(self, bot: Tux) -> None:
11 | self.bot = bot
12 | self.ping.usage = generate_usage(self.ping)
13 |
14 | @commands.hybrid_command(
15 | name="ping",
16 | )
17 | async def ping(self, ctx: commands.Context[Tux]) -> None:
18 | """
19 | Check the bot's latency and other stats.
20 |
21 | Parameters
22 | ----------
23 | ctx : commands.Context[Tux]
24 | The discord context object.
25 | """
26 |
27 | # Get the latency of the bot in milliseconds
28 | discord_ping = round(self.bot.latency * 1000)
29 |
30 | # Get the CPU usage and RAM usage of the bot
31 | cpu_usage = psutil.Process().cpu_percent()
32 | # Get the amount of RAM used by the bot
33 | ram_amount_in_bytes = psutil.Process().memory_info().rss
34 | ram_amount_in_mb = ram_amount_in_bytes / (1024 * 1024)
35 |
36 | # Format the RAM usage to be in GB or MB, rounded to nearest integer
37 | if ram_amount_in_mb >= 1024:
38 | ram_amount_formatted = f"{round(ram_amount_in_mb / 1024)}GB"
39 | else:
40 | ram_amount_formatted = f"{round(ram_amount_in_mb)}MB"
41 |
42 | embed = EmbedCreator.create_embed(
43 | embed_type=EmbedCreator.INFO,
44 | bot=self.bot,
45 | user_name=ctx.author.name,
46 | user_display_avatar=ctx.author.display_avatar.url,
47 | title="Pong!",
48 | description="Here are some stats about the bot.",
49 | )
50 |
51 | embed.add_field(name="API Latency", value=f"{discord_ping}ms", inline=True)
52 | embed.add_field(name="CPU Usage", value=f"{cpu_usage}%", inline=True)
53 | embed.add_field(name="RAM Usage", value=f"{ram_amount_formatted}", inline=True)
54 |
55 | await ctx.send(embed=embed)
56 |
57 |
58 | async def setup(bot: Tux) -> None:
59 | await bot.add_cog(Ping(bot))
60 |
--------------------------------------------------------------------------------
/tux/database/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/tux/database/__init__.py
--------------------------------------------------------------------------------
/tux/database/controllers/guild.py:
--------------------------------------------------------------------------------
1 | from typing import Any
2 |
3 | from prisma.models import Guild
4 | from tux.database.controllers.base import BaseController
5 |
6 |
7 | class GuildController(BaseController[Guild]):
8 | """Controller for managing guild records.
9 |
10 | This controller provides methods for managing guild records in the database.
11 | It inherits common CRUD operations from BaseController.
12 | """
13 |
14 | def __init__(self):
15 | """Initialize the GuildController with the guild table."""
16 | super().__init__("guild")
17 | # Type hint for better IDE support
18 | self.table: Any = self.table
19 |
20 | async def get_guild_by_id(self, guild_id: int) -> Guild | None:
21 | """Get a guild by its ID.
22 |
23 | Parameters
24 | ----------
25 | guild_id : int
26 | The ID of the guild to get
27 |
28 | Returns
29 | -------
30 | Guild | None
31 | The guild if found, None otherwise
32 | """
33 | return await self.find_one(where={"guild_id": guild_id})
34 |
35 | async def get_or_create_guild(self, guild_id: int) -> Guild:
36 | """Get an existing guild or create it if it doesn't exist.
37 |
38 | Parameters
39 | ----------
40 | guild_id : int
41 | The ID of the guild to get or create
42 |
43 | Returns
44 | -------
45 | Guild
46 | The existing or newly created guild
47 | """
48 | return await self.table.upsert(
49 | where={"guild_id": guild_id},
50 | data={
51 | "create": {"guild_id": guild_id},
52 | "update": {},
53 | },
54 | )
55 |
56 | async def insert_guild_by_id(self, guild_id: int) -> Guild:
57 | """Insert a new guild.
58 |
59 | Parameters
60 | ----------
61 | guild_id : int
62 | The ID of the guild to insert
63 |
64 | Returns
65 | -------
66 | Guild
67 | The created guild
68 | """
69 | return await self.create(data={"guild_id": guild_id})
70 |
71 | async def delete_guild_by_id(self, guild_id: int) -> None:
72 | """Delete a guild by its ID.
73 |
74 | Parameters
75 | ----------
76 | guild_id : int
77 | The ID of the guild to delete
78 | """
79 | await self.delete(where={"guild_id": guild_id})
80 |
81 | async def get_all_guilds(self) -> list[Guild]:
82 | """Get all guilds.
83 |
84 | Returns
85 | -------
86 | list[Guild]
87 | List of all guilds
88 | """
89 | return await self.find_many(where={})
90 |
--------------------------------------------------------------------------------
/tux/extensions/README.md:
--------------------------------------------------------------------------------
1 | # Extensions
2 |
3 | This is one of the more new/basic features of Tux, however it is a very powerful one. This will let you add custom commands to Tux without having to modify the code. This is done by creating a new file in the `tux/extensions` folder. The file is just a regular Discord.py cog.
4 |
5 | At the end of the day it is about the same as just adding a cog to the bot manually, you can also do this if you so wish (the src/ folder is docker mounted so modifications will be reflected in the container as well).
6 |
7 | > [!TIP]
8 | > We scan subdirectories so you can use git submodules to add extensions!
9 |
10 | ## Limitations
11 |
12 | Unfortunately using extensions does come with some limitations:
13 |
14 | - Everything is in the same category (Extensions)
15 | - You cannot add your own data to the database schema (unless you want to modify the code), a solution might be added in the future.
16 | - You cannot add extra packages (unless you modify the code), a solution might be added in the future.
17 |
--------------------------------------------------------------------------------
/tux/handlers/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/tux/handlers/__init__.py
--------------------------------------------------------------------------------
/tux/main.py:
--------------------------------------------------------------------------------
1 | """Entrypoint for the Tux Discord bot application."""
2 |
3 | from tux.app import TuxApp
4 |
5 |
6 | def run() -> None:
7 | """Instantiate and run the Tux application."""
8 | app = TuxApp()
9 | app.run()
10 |
11 |
12 | if __name__ == "__main__":
13 | run()
14 |
--------------------------------------------------------------------------------
/tux/ui/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/tux/ui/__init__.py
--------------------------------------------------------------------------------
/tux/ui/buttons.py:
--------------------------------------------------------------------------------
1 | import discord
2 |
3 |
4 | class XkcdButtons(discord.ui.View):
5 | def __init__(self, explain_url: str, webpage_url: str) -> None:
6 | super().__init__()
7 | self.add_item(
8 | discord.ui.Button(style=discord.ButtonStyle.link, label="Explainxkcd", url=explain_url),
9 | )
10 | self.add_item(
11 | discord.ui.Button(style=discord.ButtonStyle.link, label="Webpage", url=webpage_url),
12 | )
13 |
14 |
15 | class GithubButton(discord.ui.View):
16 | def __init__(self, url: str) -> None:
17 | super().__init__()
18 | self.add_item(
19 | discord.ui.Button(style=discord.ButtonStyle.link, label="View on Github", url=url),
20 | )
21 |
--------------------------------------------------------------------------------
/tux/ui/modals/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/tux/ui/modals/__init__.py
--------------------------------------------------------------------------------
/tux/ui/views/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/tux/ui/views/__init__.py
--------------------------------------------------------------------------------
/tux/ui/views/confirmation.py:
--------------------------------------------------------------------------------
1 | import discord
2 |
3 | # Confirmation dialog view:
4 | # This view is to be used for a confirmation dialog.
5 | # ideally it should be sent as a DM to ensure the user requesting it is the only one able to interact.
6 | # The base class implements the buttons themselves,
7 | # and the subclasses, which are intended to be imported and used in cogs,
8 | # change the style and labels depending on severity of the action being confirmed.
9 |
10 |
11 | class BaseConfirmationView(discord.ui.View):
12 | confirm_label: str
13 | confirm_style: discord.ButtonStyle
14 |
15 | def __init__(self, user: int) -> None:
16 | super().__init__()
17 | self.value: bool | None = None
18 | self.user = user
19 |
20 | @discord.ui.button(label="PLACEHOLDER", style=discord.ButtonStyle.secondary, custom_id="confirm")
21 | async def confirm(self, interaction: discord.Interaction, button: discord.ui.Button[discord.ui.View]) -> None:
22 | if interaction.user.id is not self.user:
23 | await interaction.response.send_message("This interaction is locked to the command author.", ephemeral=True)
24 | return
25 | await interaction.response.send_message("Confirming", ephemeral=True)
26 | self.value = True
27 | self.stop()
28 |
29 | @discord.ui.button(label="Cancel", style=discord.ButtonStyle.grey)
30 | async def cancel(self, interaction: discord.Interaction, button: discord.ui.Button[discord.ui.View]) -> None:
31 | if interaction.user.id is not self.user:
32 | await interaction.response.send_message("This interaction is locked to the command author.", ephemeral=True)
33 | return
34 | await interaction.response.send_message("Cancelling", ephemeral=True)
35 | self.value = False
36 | self.stop()
37 |
38 | def update_button_styles(self) -> None:
39 | for item in self.children:
40 | if isinstance(item, discord.ui.Button) and item.custom_id == "confirm":
41 | item.label = self.confirm_label
42 | item.style = self.confirm_style
43 |
44 |
45 | class ConfirmationDanger(BaseConfirmationView):
46 | def __init__(self, user: int) -> None:
47 | super().__init__(user)
48 | self.confirm_label = "I understand and wish to proceed anyway"
49 | self.confirm_style = discord.ButtonStyle.danger
50 | self.update_button_styles()
51 |
52 |
53 | class ConfirmationNormal(BaseConfirmationView):
54 | def __init__(self, user: int) -> None:
55 | super().__init__(user)
56 | self.confirm_label = "Confirm"
57 | self.confirm_style = discord.ButtonStyle.green
58 | self.update_button_styles()
59 |
--------------------------------------------------------------------------------
/tux/ui/views/tldr.py:
--------------------------------------------------------------------------------
1 | """
2 | TLDR Paginator View.
3 |
4 | A Discord UI view for paginating through long TLDR command documentation pages.
5 | """
6 |
7 | import discord
8 | from discord.ui import Button, View
9 |
10 | from tux.bot import Tux
11 | from tux.ui.embeds import EmbedCreator
12 |
13 |
14 | class TldrPaginatorView(View):
15 | """Paginator view for navigating through long TLDR pages."""
16 |
17 | def __init__(self, pages: list[str], title: str, user: discord.abc.User, bot: Tux):
18 | super().__init__(timeout=120)
19 | self.pages = pages
20 | self.page = 0
21 | self.title = title
22 | self.user = user
23 | self.bot = bot
24 | self.message: discord.Message | None = None
25 | self.add_item(Button[View](label="Previous", style=discord.ButtonStyle.secondary, custom_id="prev"))
26 | self.add_item(Button[View](label="Next", style=discord.ButtonStyle.secondary, custom_id="next"))
27 |
28 | async def interaction_check(self, interaction: discord.Interaction) -> bool:
29 | return interaction.user.id == self.user.id
30 |
31 | async def on_timeout(self) -> None:
32 | if self.message:
33 | await self.message.edit(view=None)
34 |
35 | @discord.ui.button(label="Previous", style=discord.ButtonStyle.secondary, custom_id="prev")
36 | async def prev(self, interaction: discord.Interaction, button: Button[View]):
37 | if self.page > 0:
38 | self.page -= 1
39 | await self.update_message(interaction)
40 | else:
41 | await interaction.response.defer()
42 |
43 | @discord.ui.button(label="Next", style=discord.ButtonStyle.secondary, custom_id="next")
44 | async def next(self, interaction: discord.Interaction, button: Button[View]):
45 | if self.page < len(self.pages) - 1:
46 | self.page += 1
47 | await self.update_message(interaction)
48 | else:
49 | await interaction.response.defer()
50 |
51 | async def update_message(self, interaction: discord.Interaction):
52 | embed = EmbedCreator.create_embed(
53 | bot=self.bot,
54 | embed_type=EmbedCreator.INFO,
55 | user_name=self.user.name,
56 | user_display_avatar=self.user.display_avatar.url,
57 | title=f"{self.title} (Page {self.page + 1}/{len(self.pages)})",
58 | description=self.pages[self.page],
59 | )
60 | await interaction.response.edit_message(embed=embed, view=self)
61 |
--------------------------------------------------------------------------------
/tux/utils/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/tux/utils/__init__.py
--------------------------------------------------------------------------------
/tux/utils/ascii.py:
--------------------------------------------------------------------------------
1 | """ASCII art for Tux bot."""
2 |
3 | TUX = r""" .--.
4 | |o_o |
5 | |:_/ |
6 | // \ \
7 | (| | )
8 | /'\_ _/`\
9 | \___)=(___/
10 | """
11 |
--------------------------------------------------------------------------------
/tux/utils/constants.py:
--------------------------------------------------------------------------------
1 | from typing import Final
2 |
3 |
4 | class Constants:
5 | # Color constants
6 | EMBED_COLORS: Final[dict[str, int]] = {
7 | "DEFAULT": 16044058,
8 | "INFO": 12634869,
9 | "WARNING": 16634507,
10 | "ERROR": 16067173,
11 | "SUCCESS": 10407530,
12 | "POLL": 14724968,
13 | "CASE": 16217742,
14 | "NOTE": 16752228,
15 | }
16 |
17 | # Icon constants
18 | EMBED_ICONS: Final[dict[str, str]] = {
19 | "DEFAULT": "https://i.imgur.com/owW4EZk.png",
20 | "INFO": "https://i.imgur.com/8GRtR2G.png",
21 | "SUCCESS": "https://i.imgur.com/JsNbN7D.png",
22 | "ERROR": "https://i.imgur.com/zZjuWaU.png",
23 | "CASE": "https://i.imgur.com/c43cwnV.png",
24 | "NOTE": "https://i.imgur.com/VqPFbil.png",
25 | "POLL": "https://i.imgur.com/pkPeG5q.png",
26 | "ACTIVE_CASE": "https://github.com/allthingslinux/tux/blob/main/assets/embeds/active_case.png?raw=true",
27 | "INACTIVE_CASE": "https://github.com/allthingslinux/tux/blob/main/assets/embeds/inactive_case.png?raw=true",
28 | "ADD": "https://github.com/allthingslinux/tux/blob/main/assets/emojis/added.png?raw=true",
29 | "REMOVE": "https://github.com/allthingslinux/tux/blob/main/assets/emojis/removed.png?raw=true",
30 | "BAN": "https://github.com/allthingslinux/tux/blob/main/assets/emojis/ban.png?raw=true",
31 | "JAIL": "https://github.com/allthingslinux/tux/blob/main/assets/emojis/jail.png?raw=true",
32 | "KICK": "https://github.com/allthingslinux/tux/blob/main/assets/emojis/kick.png?raw=true",
33 | "TIMEOUT": "https://github.com/allthingslinux/tux/blob/main/assets/emojis/timeout.png?raw=true",
34 | "WARN": "https://github.com/allthingslinux/tux/blob/main/assets/emojis/warn.png?raw=true",
35 | }
36 |
37 | # Embed limit constants
38 | EMBED_MAX_NAME_LENGTH = 256
39 | EMBED_MAX_DESC_LENGTH = 4096
40 | EMBED_MAX_FIELDS = 25
41 | EMBED_TOTAL_MAX = 6000
42 | EMBED_FIELD_VALUE_LENGTH = 1024
43 |
44 | NICKNAME_MAX_LENGTH = 32
45 |
46 | # Interaction constants
47 | ACTION_ROW_MAX_ITEMS = 5
48 | SELECTS_MAX_OPTIONS = 25
49 | SELECT_MAX_NAME_LENGTH = 100
50 |
51 | # App commands constants
52 | CONTEXT_MENU_NAME_LENGTH = 32
53 | SLASH_CMD_NAME_LENGTH = 32
54 | SLASH_CMD_MAX_DESC_LENGTH = 100
55 | SLASH_CMD_MAX_OPTIONS = 25
56 | SLASH_OPTION_NAME_LENGTH = 100
57 |
58 | DEFAULT_REASON = "No reason provided"
59 |
60 | # Snippet constants
61 | SNIPPET_MAX_NAME_LENGTH = 20
62 | SNIPPET_ALLOWED_CHARS_REGEX = r"^[a-zA-Z0-9-]+$"
63 | SNIPPET_PAGINATION_LIMIT = 10
64 |
65 | # Message timings
66 | DEFAULT_DELETE_AFTER = 30
67 |
68 | # AFK constants
69 | AFK_PREFIX = "[AFK] "
70 | AFK_TRUNCATION_SUFFIX = "..."
71 |
72 | # 8ball constants
73 | EIGHT_BALL_QUESTION_LENGTH_LIMIT = 120
74 | EIGHT_BALL_RESPONSE_WRAP_WIDTH = 30
75 |
76 |
77 | CONST = Constants()
78 |
--------------------------------------------------------------------------------
/tux/utils/regex.py:
--------------------------------------------------------------------------------
1 | import re
2 |
3 | DISCORD_ID = re.compile(r"(\d{15,20})$")
4 |
5 | DISCORD_USER_MENTION = re.compile(r"<@!?(\d{15,20})>$")
6 | DISCORD_CHANNEL_MENTION = re.compile(r"<#(\d{15,20})>$")
7 | DISCORD_ROLE_MENTION = re.compile(r"<@&(\d{15,20})>$")
8 |
9 | DISCORD_INVITE = re.compile(
10 | r"(?:https?://)?discord(?:app)?\.(?:com/invite|gg)/[a-zA-Z0-9]+/?",
11 | flags=re.IGNORECASE,
12 | )
13 |
14 | DISCORD_FILE = re.compile(
15 | r"(https://|http://)?(cdn\.|media\.)discord(app)?\.(com|net)/(attachments|avatars|icons|banners|splashes)/[0-9]{17,22}/([0-9]{17,22}/(?P.{1,256})|(?P.{32}))\.(?P[0-9a-zA-Z]{2,4})?",
16 | )
17 |
18 | DISCORD_MESSAGE = re.compile(
19 | r"(?:https?://)?(?:canary\.|ptb\.|www\.)?discord(?:app)?.(?:com/channels|gg)/(?P[0-9]{17,22})/(?P[0-9]{17,22})/(?P[0-9]{17,22})",
20 | )
21 |
22 | CUSTOM_EMOJI = re.compile(r"<(a)?:([a-zA-Z0-9_]{2,32}):([0-9]{18,22})>")
23 |
24 | MULTILINE_CODEBLOCK = re.compile(r"```(?P[a-z]*)\n*(?P[\s\S]+)\n*```")
25 | SINGLE_LINE_CODEBLOCK = re.compile(r"^`(?P[\s\S]+)`$")
26 |
27 | TENOR_PAGE_URL = re.compile(r"https?://(www\.)?tenor\.com/view/\S+/?")
28 | TENOR_GIF_URL = re.compile(r"https?://(www\.)?c\.tenor\.com/\S+/\S+\.gif/?")
29 |
30 | IMGUR_PAGE_URL = re.compile(r"https?://(www\.)?imgur.com/(\S+)/?")
31 |
32 | URL = re.compile(
33 | r"((http|https)\:\/\/)?[a-zA-Z0-9\.\/\?\:@\-_=#]+\.([a-zA-Z]){2,6}([a-zA-Z0-9\.\&\/\?\:@\-_=#])*",
34 | flags=re.IGNORECASE,
35 | )
36 |
37 | URL_NO_PROTOCOL = re.compile(
38 | r"[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)",
39 | flags=re.IGNORECASE,
40 | )
41 |
--------------------------------------------------------------------------------
/tux/wrappers/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allthingslinux/tux/bb4f1f3a238acc0499cc8d1773bf578ddc032bf3/tux/wrappers/__init__.py
--------------------------------------------------------------------------------
/tux/wrappers/wandbox.py:
--------------------------------------------------------------------------------
1 | from typing import Any
2 |
3 | import httpx
4 |
5 | from tux.utils.exceptions import (
6 | APIConnectionError,
7 | APIRequestError,
8 | APIResourceNotFoundError,
9 | )
10 |
11 | client = httpx.Client(timeout=15)
12 | url = "https://wandbox.org/api/compile.json"
13 |
14 |
15 | def getoutput(code: str, compiler: str, options: str | None) -> dict[str, Any] | None:
16 | """
17 | Compile and execute code using a specified compiler and return the output.
18 |
19 | Parameters
20 | ----------
21 | code : str
22 | The source code to be compiled and executed.
23 | compiler : str
24 | The identifier or name of the compiler to use.
25 | options : str or None
26 | Additional compiler options or flags. If None, an empty string is used.
27 |
28 | Returns
29 | -------
30 | dict[str, Any] or None
31 | A dictionary containing the compiler output if the request is successful,
32 | otherwise `None`. Returns `None` on HTTP errors or read timeout.
33 | """
34 |
35 | copt = options if options is not None else ""
36 | headers = {
37 | "Content-Type": "application/json",
38 | }
39 | payload = {"compiler": compiler, "code": code, "options": copt}
40 |
41 | try:
42 | uri = client.post(url, json=payload, headers=headers)
43 | uri.raise_for_status()
44 | except httpx.ReadTimeout as e:
45 | # Changed to raise APIConnectionError for timeouts
46 | raise APIConnectionError(service_name="Wandbox", original_error=e) from e
47 | except httpx.RequestError as e:
48 | # General connection/request error
49 | raise APIConnectionError(service_name="Wandbox", original_error=e) from e
50 | except httpx.HTTPStatusError as e:
51 | # Specific HTTP status errors
52 | if e.response.status_code == 404:
53 | raise APIResourceNotFoundError(
54 | service_name="Wandbox",
55 | resource_identifier=compiler,
56 | ) from e # Using compiler as resource identifier
57 | raise APIRequestError(service_name="Wandbox", status_code=e.response.status_code, reason=e.response.text) from e
58 | else:
59 | return uri.json() if uri.status_code == httpx.codes.OK else None
60 |
--------------------------------------------------------------------------------
/typings/emojis/__init__.pyi:
--------------------------------------------------------------------------------
1 | """
2 | This type stub file was generated by pyright.
3 | """
4 |
5 | from .emojis import count, decode, encode, get, iter
6 |
7 | '''
8 | Emojis for Python 🐍
9 | '''
10 | __all__ = ['encode', 'decode', 'get', 'count', 'iter']
11 |
--------------------------------------------------------------------------------
/typings/emojis/db/__init__.pyi:
--------------------------------------------------------------------------------
1 | """
2 | This type stub file was generated by pyright.
3 | """
4 |
5 | from .db import Emoji
6 | from .utils import get_categories, get_emoji_aliases, get_emoji_by_alias, get_emoji_by_code, get_emojis_by_category, get_emojis_by_tag, get_tags
7 |
8 | '''
9 | Emoji database.
10 | '''
11 | __all__ = ['Emoji', 'get_emoji_aliases', 'get_emoji_by_code', 'get_emoji_by_alias', 'get_emojis_by_tag', 'get_emojis_by_category', 'get_tags', 'get_categories']
12 |
--------------------------------------------------------------------------------
/typings/emojis/db/db.pyi:
--------------------------------------------------------------------------------
1 | """
2 | This type stub file was generated by pyright.
3 | """
4 |
5 | Emoji = ...
6 | EMOJI_DB = ...
7 |
--------------------------------------------------------------------------------
/typings/emojis/db/utils.pyi:
--------------------------------------------------------------------------------
1 | """
2 | This type stub file was generated by pyright.
3 | """
4 |
5 | def get_emoji_aliases(): # -> dict[Any, Any]:
6 | '''
7 | Returns all Emojis as a dict (key = alias, value = unicode).
8 |
9 | :rtype: dict
10 | '''
11 | ...
12 |
13 | def get_emoji_by_code(code): # -> None:
14 | '''
15 | Returns Emoji by Unicode code.
16 |
17 | :param code: Emoji Unicode code.
18 | :rtype: emojis.db.Emoji
19 | '''
20 | ...
21 |
22 | def get_emoji_by_alias(alias): # -> Emoji | None:
23 | '''
24 | Returns Emoji by alias.
25 |
26 | :param alias: Emoji alias.
27 | :rtype: emojis.db.Emoji
28 | '''
29 | ...
30 |
31 | def get_emojis_by_tag(tag): # -> filter[Emoji]:
32 | '''
33 | Returns all Emojis from selected tag.
34 |
35 | :param tag: Tag name to filter (case-insensitive).
36 | :rtype: iter
37 | '''
38 | ...
39 |
40 | def get_emojis_by_category(category): # -> filter[Any]:
41 | '''
42 | Returns all Emojis from selected category.
43 |
44 | :param tag: Category name to filter (case-insensitive).
45 | :rtype: iter
46 | '''
47 | ...
48 |
49 | def get_tags(): # -> set[Any]:
50 | '''
51 | Returns all tags available.
52 |
53 | :rtype: set
54 | '''
55 | ...
56 |
57 | def get_categories(): # -> set[Any]:
58 | '''
59 | Returns all categories available.
60 |
61 | :rtype: set
62 | '''
63 | ...
64 |
65 |
--------------------------------------------------------------------------------
/typings/emojis/emojis.pyi:
--------------------------------------------------------------------------------
1 | """
2 | This type stub file was generated by pyright.
3 | """
4 |
5 | ALIAS_TO_EMOJI = ...
6 | EMOJI_TO_ALIAS = ...
7 | EMOJI_TO_ALIAS_SORTED = ...
8 | RE_TEXT_TO_EMOJI_GROUP = ...
9 | RE_TEXT_TO_EMOJI = ...
10 | RE_EMOJI_TO_TEXT_GROUP = ...
11 | RE_EMOJI_TO_TEXT = ...
12 | def encode(msg): # -> str:
13 | '''
14 | Encode Emoji aliases into unicode Emoji values.
15 |
16 | :param msg: String to encode.
17 | :rtype: str
18 |
19 | Usage::
20 |
21 | >>> import emojis
22 | >>> emojis.encode('This is a message with emojis :smile: :snake:')
23 | 'This is a message with emojis 😄 🐍'
24 | '''
25 | ...
26 |
27 | def decode(msg): # -> str:
28 | '''
29 | Decode unicode Emoji values into Emoji aliases.
30 |
31 | :param msg: String to decode.
32 | :rtype: str
33 |
34 | Usage::
35 |
36 | >>> import emojis
37 | >>> emojis.decode('This is a message with emojis 😄 🐍')
38 | 'This is a message with emojis :smile: :snake:'
39 | '''
40 | ...
41 |
42 | def get(msg): # -> set[str]:
43 | '''
44 | Returns unique Emojis in the given string.
45 |
46 | :param msg: String to search for Emojis.
47 | :rtype: set
48 | '''
49 | ...
50 |
51 | def iter(msg): # -> Generator[str, None, None]:
52 | '''
53 | Iterates over all Emojis found in the message.
54 |
55 | :param msg: String to search for Emojis.
56 | :rtype: iterator
57 | '''
58 | ...
59 |
60 | def count(msg, unique=...): # -> int:
61 | '''
62 | Returns Emoji count in the given string.
63 |
64 | :param msg: String to search for Emojis.
65 | :param unique: (optional) Boolean, return unique values only.
66 | :rtype: int
67 | '''
68 | ...
69 |
70 |
--------------------------------------------------------------------------------
/typings/reactionmenu/__init__.pyi:
--------------------------------------------------------------------------------
1 | """
2 | This type stub file was generated by pyright.
3 | """
4 |
5 | from .abc import Page
6 | from .buttons import ReactionButton, ViewButton
7 | from .core import ReactionMenu
8 | from .views_menu import ViewMenu, ViewSelect
9 |
10 | """
11 | reactionmenu • discord pagination
12 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
13 |
14 | A library to create a discord.py 2.0+ paginator. Supports pagination with buttons, reactions, and category selection using selects.
15 |
16 | :copyright: (c) 2021-present @defxult
17 | :license: MIT
18 |
19 | """
20 | __source__ = ...
21 | __all__ = ("ReactionMenu", "ReactionButton", "ViewMenu", "ViewButton", "ViewSelect", "Page")
22 |
--------------------------------------------------------------------------------
/typings/reactionmenu/decorators.pyi:
--------------------------------------------------------------------------------
1 | """
2 | This type stub file was generated by pyright.
3 | """
4 |
5 | """
6 | MIT License
7 |
8 | Copyright (c) 2021-present @defxult
9 |
10 | Permission is hereby granted, free of charge, to any person obtaining a
11 | copy of this software and associated documentation files (the "Software"),
12 | to deal in the Software without restriction, including without limitation
13 | the rights to use, copy, modify, merge, publish, distribute, sublicense,
14 | and/or sell copies of the Software, and to permit persons to whom the
15 | Software is furnished to do so, subject to the following conditions:
16 |
17 | The above copyright notice and this permission notice shall be included in
18 | all copies or substantial portions of the Software.
19 |
20 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
21 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
25 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
26 | DEALINGS IN THE SOFTWARE.
27 | """
28 |
29 | def ensure_not_primed(
30 | func,
31 | ): # -> _Wrapped[Callable[..., Any], Any, Callable[..., Any], Coroutine[Any, Any, Any]] | _Wrapped[Callable[..., Any], Any, Callable[..., Any], Any]:
32 | """Check to make sure certain methods cannot be ran once the menu has been fully started"""
33 |
--------------------------------------------------------------------------------