├── .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 = "" 9 | red = "" 10 | green = "" 11 | yellow = "" 12 | blue = "" 13 | pink = "" 14 | cyan = "" 15 | white = "" 16 | reset = "" 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 | Tux 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 | Server 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 | --------------------------------------------------------------------------------