diff --git a/templates/discord-bot/.envrc b/templates/discord-bot/.envrc new file mode 100644 index 0000000..944d27b --- /dev/null +++ b/templates/discord-bot/.envrc @@ -0,0 +1,2 @@ +watch_file "flake.nix" "pyproject.toml" +use flake diff --git a/templates/discord-bot/.gitignore b/templates/discord-bot/.gitignore new file mode 100644 index 0000000..d116ea8 --- /dev/null +++ b/templates/discord-bot/.gitignore @@ -0,0 +1,14 @@ +# Secrets +.env + +# Nix +.direnv/ +result + +# Python +.ruff_cache +.ropeproject + +# Runtime +rename.sqlite3 +rename.sqlite3-journal diff --git a/templates/discord-bot/flake.nix b/templates/discord-bot/flake.nix new file mode 100644 index 0000000..7310bf1 --- /dev/null +++ b/templates/discord-bot/flake.nix @@ -0,0 +1,66 @@ +{ + description = "Template for Python-based Discord bots"; + + inputs = { + nixpkgs.url = "github:Infinidoge/nixpkgs/feat/disnake"; + flake-parts.url = "github:hercules-ci/flake-parts"; + devshell.url = "github:numtide/devshell"; + pyproject-nix.url = "github:nix-community/pyproject.nix"; + + flake-parts.inputs.nixpkgs-lib.follows = "nixpkgs"; + devshell.inputs.nixpkgs.follows = "nixpkgs"; + pyproject-nix.inputs.nixpkgs.follows = "nixpkgs"; + }; + + outputs = inputs: inputs.flake-parts.lib.mkFlake { inherit inputs; } { + systems = [ "x86_64-linux" ]; + + imports = with inputs; [ + devshell.flakeModule + ]; + + perSystem = { config, pkgs, ... }: + let + project = inputs.pyproject-nix.lib.project.loadPyproject { + projectRoot = ./.; + }; + + python = pkgs.python3; + + in + { + packages.default = (python.pkgs.buildPythonPackage ( + project.renderers.buildPythonPackage { inherit python; } + )).overrideAttrs { allowSubstitutes = false; preferLocalBuild = true; }; + + devshells.default = + let + env = python.withPackages ( + project.renderers.withPackages { + inherit python; + extraPackages = p: with p; [ + python-lsp-server + python-lsp-ruff + pylsp-rope + isort + black + ]; + } + ); + in + { + devshell = { + name = "rename"; + motd = ""; + + packages = [ + env + ]; + }; + env = [ + { name = "PYTHONPATH"; prefix = "${env}/${env.sitePackages}"; } + ]; + }; + }; + }; +} diff --git a/templates/discord-bot/pyproject.toml b/templates/discord-bot/pyproject.toml new file mode 100644 index 0000000..21f1d60 --- /dev/null +++ b/templates/discord-bot/pyproject.toml @@ -0,0 +1,21 @@ +[project] +name = "rename" +version = "0.0.1" +dependencies = [ + "disnake", + "python-dotenv", + "aiosqlite" +] + +[project.scripts] +rename = "rename:run" + +[tool.setuptools.packages] +find = {} + +[build-system] +requires = ["setuptools"] +build-backend = "setuptools.build_meta" + +[tool.ruff] +line-length = 120 diff --git a/templates/discord-bot/rename/__init__.py b/templates/discord-bot/rename/__init__.py new file mode 100644 index 0000000..86966d1 --- /dev/null +++ b/templates/discord-bot/rename/__init__.py @@ -0,0 +1 @@ +from .main import run diff --git a/templates/discord-bot/rename/db.py b/templates/discord-bot/rename/db.py new file mode 100644 index 0000000..0276cff --- /dev/null +++ b/templates/discord-bot/rename/db.py @@ -0,0 +1,25 @@ +import aiosqlite + + +async def setup_db(db_file): + db = await aiosqlite.connect(db_file) + + await db.executescript(""" + BEGIN; + + CREATE TABLE IF NOT EXISTS guilds ( + guild_id INTEGER NOT NULL PRIMARY KEY, + prefix TEXT NOT NULL DEFAULT ">" + ) + WITHOUT ROWID; + + # DB Setup here + + COMMIT; + + PRAGMA optimize(0x10002); + PRAGMA main.synchronous = NORMAL; + """) + + return db + diff --git a/templates/discord-bot/rename/main.py b/templates/discord-bot/rename/main.py new file mode 100644 index 0000000..54e1fe2 --- /dev/null +++ b/templates/discord-bot/rename/main.py @@ -0,0 +1,154 @@ +import logging +import os +import sys + +from disnake import Guild, Intents, Message +from disnake.ext import commands +from disnake.ext.commands import Bot +from dotenv import find_dotenv, load_dotenv + +from .db import setup_db +from .notifications import Notifications +from .settings import Settings + +# Logger setup +logger_disnake = logging.getLogger("disnake") +logger_disnake.setLevel(logging.WARNING) + +log = logging.getLogger("rename") +log.setLevel(logging.DEBUG) + +formatter = logging.Formatter("%(asctime)s:%(levelname)s:%(name)s: %(message)s") +handler = logging.StreamHandler(sys.stdout) +handler.setFormatter(formatter) +logger_disnake.addHandler(handler) +log.addHandler(handler) + + +if load_dotenv(find_dotenv(usecwd=True)): + log.debug("Loaded .env") +else: + log.debug("Didn't find .env") + +TOKEN = os.getenv("TOKEN") +DB_FILE = os.getenv("DB_FILE") or "rename.sqlite3" +DEFAULT_PREFIX = os.getenv("DEFAULT_PREFIX") or ">>>" + + +async def get_prefix(the_bot, message: Message): + if not message.guild: + return commands.when_mentioned_or(DEFAULT_PREFIX)(the_bot, message) + + gp = await the_bot.get_guild_prefix(message.guild) + + if gp == "": + return commands.when_mentioned(the_bot, message) + + return commands.when_mentioned_or(gp)(the_bot, message) + + +class Rename(Bot): + def __init__(self, prefix, description=None, **options): + super().__init__( + prefix, + description=description, + command_sync_flags=options.get("sync_flags"), + allowed_mentions=options.get("allowed_mentions"), + intents=options.get("intents"), + ) + + self.db = self.loop.run_until_complete(setup_db(DB_FILE)) + self.prefixes = {} + + async def get_guild_prefix(self, guild: Guild): + if guild.id in self.prefixes: + return self.prefixes[guild.id] + + cur = await self.db.execute( + "SELECT prefix FROM guilds WHERE guild_id=? LIMIT 1", + [guild.id], + ) + prefix = await cur.fetchone() or [DEFAULT_PREFIX] + prefix = prefix[0] + + self.prefixes[guild.id] = prefix + return prefix + + async def set_guild_prefix(self, guild: Guild, prefix): + await self.db.execute( + "REPLACE INTO guilds VALUES(?, ?)", + (guild.id, prefix), + ) + self.prefixes[guild.id] = prefix + + async def close(self): + await super().close() + await self.db.close() + + +bot = Rename( + prefix=get_prefix, + description="@description@", + intents=Intents( + guilds=True, + messages=True, + message_content=True, + ), + sync_flags=commands.CommandSyncFlags( + sync_commands=False, + ), +) + + +@bot.event +async def on_ready(): + log.info("Logged in as:") + log.info(bot.user) + log.info(bot.user.id) + log.info("-------------") + + +@bot.event +async def on_command_error(ctx, error): + if isinstance(error, commands.CommandNotFound): + return + + if isinstance(error, commands.NoPrivateMessage): + await ctx.send(error) + return + + log.exception(error) + await ctx.send(error) + + +@bot.command() +@commands.is_owner() +async def echo(ctx, arg): + await ctx.send(arg) + + +@bot.command() +async def ping(ctx): + await ctx.send("Pong") + + +@bot.command() +@commands.guild_only() +@commands.is_owner() +async def prefix(ctx, prefix=None): + if prefix is None: + prefix = await ctx.bot.get_guild_prefix(ctx.guild) + if prefix == "": + await ctx.send(f"{ctx.guild.name} prefix is mentioning me only") + else: + await ctx.send(f"{ctx.guild.name} prefix is `{prefix}`") + else: + await ctx.bot.set_guild_prefix(ctx.guild, prefix) + if prefix == "": + await ctx.send(f"Set {ctx.guild.name} prefix to mentioning me only") + else: + await ctx.send(f"Set {ctx.guild.name} prefix to `{prefix}`") + + +def run(): + bot.run(TOKEN) diff --git a/templates/discord-bot/setup b/templates/discord-bot/setup new file mode 100755 index 0000000..f2f74fb --- /dev/null +++ b/templates/discord-bot/setup @@ -0,0 +1,30 @@ +#!/usr/bin/env bash +# vim: set ft=bash: + +set -e + +if [ $# -lt 3 ]; then + echo "Not enough arguments" + echo 'usage: ./setup basename ClassName "description here"' + exit 1 +fi + +nameBase=$1 +nameClass=$2 +description=$3 +files=( + "flake.nix" + ".gitignore" + "pyproject.toml" + "rename/main.py" +) + +for file in ${files[@]}; do + sed -i " + s/rename/$nameBase/g; + s/Rename/$nameClass/g; + s/@description@/$description/g; + " $file +done + +mv rename $nameBase