Modern packaging
- add pyproject.toml, README, and recommend using pdm - cleanup dependencies and switch to better ones - remove mumble stub fix missing env var call fix null env var
This commit is contained in:
parent
5a0dff92b0
commit
4ac878fecb
12 changed files with 1292 additions and 76 deletions
7
.env.example
Normal file
7
.env.example
Normal file
|
@ -0,0 +1,7 @@
|
|||
DISCORD_BOT_TOKEN =
|
||||
DISCORD_GUILD_ID =
|
||||
|
||||
DISCORD_BOT_ADMIN_ROLE_ID =
|
||||
DISCORD_BOT_ADMIN_USER_ID =
|
||||
|
||||
DISCORD_VC_CHANNEL =
|
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -1,6 +1,9 @@
|
|||
.env
|
||||
.venv
|
||||
.cache/*
|
||||
.logs/*
|
||||
sounds/originals/*
|
||||
sounds/normalized/*
|
||||
cogs/__pycache__/*
|
||||
.pdm.toml
|
||||
__pypackages__/
|
51
README.md
Normal file
51
README.md
Normal file
|
@ -0,0 +1,51 @@
|
|||
# umi
|
||||
|
||||
a self-hostable single-guild discord bot for playing music and announcing vc channel joins/leaves
|
||||
|
||||
## Pre-requisites
|
||||
|
||||
Install dependencies:
|
||||
|
||||
- FFI: `libffi`
|
||||
- NaCl: `libnacl` or `libsodium`
|
||||
|
||||
Copy `.env.example` to `.env`:
|
||||
|
||||
- `DISCORD_BOT_TOKEN` is obtained from the [Discord Developer Dashboard](https://discord.com/developers/applications)
|
||||
- Click or create an application
|
||||
- Get your token from the "Bot" section in the sidebar menu
|
||||
|
||||
- `DISCORD_GUILD_ID` is obtained from within the Discord app
|
||||
- Settings > Advanced > Developer Mode: enabled
|
||||
- Right click the guild and select "Copy ID"
|
||||
|
||||
- `DISCORD_BOT_ADMIN_ROLE_ID` for reloading cogs
|
||||
- `DISCORD_BOT_ADMIN_USER_ID` for reloading cogs
|
||||
- `DISCORD_VC_CHANNEL` for the VCJoin cog
|
||||
|
||||
## Setup
|
||||
|
||||
### ...with pdm
|
||||
|
||||
```sh
|
||||
pdm install --prod
|
||||
pdm run python app.py
|
||||
```
|
||||
|
||||
### ...with requirements.txt
|
||||
|
||||
#### ....and pipenv
|
||||
|
||||
```sh
|
||||
pipenv install
|
||||
pipenv run python app.py
|
||||
```
|
||||
|
||||
#### ....and virtualenv
|
||||
|
||||
```sh
|
||||
virtualenv .venv
|
||||
source .venv/bin/activate
|
||||
pip install -r requirements.txt
|
||||
python app.py
|
||||
```
|
40
app.py
40
app.py
|
@ -1,5 +1,4 @@
|
|||
import discord
|
||||
import pymumble_py3 as pymumble
|
||||
|
||||
""" Declare intents that the bot will use """
|
||||
intents = discord.Intents.default()
|
||||
|
@ -14,15 +13,13 @@ intents.reactions = True
|
|||
intents.voice_states = True
|
||||
|
||||
""" Load the bot token """
|
||||
from dotenv import load_dotenv
|
||||
from os import getenv
|
||||
load_dotenv()
|
||||
DISCORD_TOKEN = getenv("DISCORD_BOT_TOKEN")
|
||||
DISCORD_GUILD = int(getenv("DISCORD_GUILD_ID"))
|
||||
from decouple import config
|
||||
DISCORD_TOKEN: str = config("DISCORD_BOT_TOKEN")
|
||||
DISCORD_GUILD: int = config("DISCORD_GUILD_ID", cast=int)
|
||||
|
||||
""" Initialize the Discord bot """
|
||||
from discord.ext.commands import Bot, when_mentioned_or
|
||||
bot = Bot(
|
||||
bot: Bot = Bot(
|
||||
command_prefix=when_mentioned_or('..', '>', '.'),
|
||||
description="A list of commands available",
|
||||
intents=intents,
|
||||
|
@ -37,6 +34,7 @@ async def on_ready():
|
|||
|
||||
""" Load cogs """
|
||||
from os import listdir
|
||||
|
||||
def list_all_cogs() -> list[str]:
|
||||
return [file[:-3] for file in listdir("cogs") if file.endswith(".py")]
|
||||
|
||||
|
@ -56,19 +54,17 @@ from discord.commands import Option
|
|||
from discord.ext.commands import Context
|
||||
from discord import AutocompleteContext, ApplicationContext
|
||||
|
||||
async def cog_autocomplete(ctx: AutocompleteContext):
|
||||
async def cog_autocomplete(ctx: AutocompleteContext) -> list[str]:
|
||||
return [cog for cog in list_all_cogs() if cog.lower().startswith( ctx.value.lower() )]
|
||||
|
||||
ROLE_ADMIN = 518625964763119616
|
||||
ROLE_ADMIN_SFW = 727205354353721374
|
||||
ME = 201046736565829632
|
||||
ADMIN_ROLE: int = config("DISCORD_BOT_ADMIN_ROLE_ID", cast=int, default=0)
|
||||
ADMIN_USER: int = config("DISCORD_BOT_ADMIN_USER_ID", cast=int)
|
||||
|
||||
def allowed_to_reload(ctx: Context | ApplicationContext):
|
||||
roles = [role.id for role in ctx.author.roles]
|
||||
admin = ROLE_ADMIN in roles
|
||||
sfw_admin = ROLE_ADMIN_SFW in roles
|
||||
owner = ctx.author.id == ME
|
||||
return any([admin, sfw_admin, owner])
|
||||
def allowed_to_reload(ctx: Context | ApplicationContext) -> bool:
|
||||
roles: set[int] = set([role.id for role in ctx.author.roles])
|
||||
admin: bool = ADMIN_ROLE in roles
|
||||
owner: bool = ctx.author.id == ADMIN_USER
|
||||
return any([admin, owner])
|
||||
|
||||
from cogs.music import Music
|
||||
def reload_music(ctx):
|
||||
|
@ -89,19 +85,17 @@ def reload_music(ctx):
|
|||
music.search_results = search_results
|
||||
|
||||
@bot.command(name='reload')
|
||||
async def reload_prefix(ctx: Context, cog: str = None):
|
||||
async def reload_prefix(ctx: Context, cog: str = "") -> None:
|
||||
"""Reload an extension (admin command)"""
|
||||
if not allowed_to_reload(ctx):
|
||||
return await ctx.send(
|
||||
"You must be an admin or bot owner to use this command"
|
||||
)
|
||||
return await ctx.send("You must be an admin or bot owner to use this command")
|
||||
if not cog:
|
||||
return await ctx.send("Please specify a cog to reload")
|
||||
elif cog.lower() == "music":
|
||||
reload_music(ctx)
|
||||
else:
|
||||
bot.reload_extension(f"cogs.{cog}")
|
||||
await ctx.send(f"Reloaded `{cog}` extension")
|
||||
return await ctx.send(f"Reloaded `{cog}` extension")
|
||||
|
||||
@bot.slash_command(
|
||||
name='reload',
|
||||
|
@ -121,7 +115,7 @@ async def reload_slash(
|
|||
reload_music(ctx)
|
||||
else:
|
||||
bot.reload_extension(f"cogs.{cog}")
|
||||
await ctx.respond(f"Reloaded `{cog}` extension", ephemeral=True)
|
||||
return await ctx.respond(f"Reloaded `{cog}` extension", ephemeral=True)
|
||||
|
||||
# ================================== END =======================================
|
||||
|
||||
|
|
|
@ -1,25 +0,0 @@
|
|||
import discord
|
||||
from discord.ext.commands import Cog
|
||||
from os import getenv, path, makedirs
|
||||
import logging
|
||||
|
||||
if not path.exists('.logs'):
|
||||
makedirs('.logs')
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.setLevel(logging.WARNING)
|
||||
fh = logging.FileHandler('.logs/mumble.log')
|
||||
formatter = logging.Formatter('%(asctime)s | %(name)s | [%(levelname)s] %(message)s', '%Y-%m-%d %H:%M:%S')
|
||||
fh.setFormatter(formatter)
|
||||
if not len(logger.handlers):
|
||||
logger.addHandler(fh)
|
||||
|
||||
def setup(bot: discord.Bot):
|
||||
bot.add_cog(Mumble(bot))
|
||||
|
||||
class Mumble(Cog):
|
||||
"""Bridge to Mumble"""
|
||||
|
||||
def __init__(self, bot: discord.Bot):
|
||||
self.bot: discord.Bot = bot
|
||||
print("Initialized Music cog")
|
|
@ -3,7 +3,8 @@ from discord.ext.commands import Cog, command, Context
|
|||
from discord.ext.pages import Paginator
|
||||
import asyncio # used to run async functions within regular functions
|
||||
import subprocess # for running ffprobe and getting duration of files
|
||||
from os import getenv, path, makedirs
|
||||
from os import path, makedirs
|
||||
from decouple import config
|
||||
from time import time # performance tracking
|
||||
import random # for shuffling the queue
|
||||
import math # for ceiling function in queue pages
|
||||
|
@ -831,6 +832,6 @@ ytdl_format_options = {
|
|||
# "source_address": "0.0.0.0", # Bind to ipv4 since ipv6 addresses cause issues
|
||||
"extract_flat": True, # massive speedup for fetching metadata, at the cost of no upload date
|
||||
}
|
||||
username = getenv("YOUTUBE_USERNAME")
|
||||
password = getenv("YOUTUBE_PASSWORD")
|
||||
# username = config("YOUTUBE_USERNAME")
|
||||
# password = config("YOUTUBE_PASSWORD")
|
||||
ytdl = youtube_dl.YoutubeDL(ytdl_format_options)
|
|
@ -1,12 +1,12 @@
|
|||
import discord
|
||||
from discord.ext.commands import Cog, command, Context
|
||||
from discord.commands import slash_command, Option
|
||||
import requests
|
||||
from os import getenv
|
||||
import httpx
|
||||
from decouple import config
|
||||
from collections import Counter
|
||||
import re
|
||||
|
||||
GUILD = int(getenv("DISCORD_GUILD_ID"))
|
||||
GUILD = config("DISCORD_GUILD_ID", cast=int)
|
||||
|
||||
def setup(bot: discord.Bot):
|
||||
bot.add_cog(Random(bot))
|
||||
|
@ -23,7 +23,7 @@ class Random(Cog):
|
|||
def roll(faces: int = 6, number: int = 1) -> str:
|
||||
"""Roll die(s) using random.org integer generation"""
|
||||
columns = number if number < 11 else 10
|
||||
return requests.get(f'https://www.random.org/integers/?num={number}&min=1&max={faces}&col={columns}&base=10&format=plain&rnd=new').text
|
||||
return httpx.get(f'https://www.random.org/integers/?num={number}&min=1&max={faces}&col={columns}&base=10&format=plain&rnd=new').text
|
||||
|
||||
@staticmethod
|
||||
def roll_results(faces: int = 6, number: int = 1) -> str:
|
||||
|
@ -43,7 +43,7 @@ class Random(Cog):
|
|||
number, faces = query.split('d')
|
||||
if not number: # handle raw dN rolls, e.g. 'd6' or 'd20'
|
||||
number = 1
|
||||
await ctx.send(Random.roll_results(faces=int(faces), number=int(number)))
|
||||
return await ctx.send(Random.roll_results(faces=int(faces), number=int(number)))
|
||||
|
||||
@slash_command(
|
||||
name='roll',
|
||||
|
@ -55,14 +55,16 @@ class Random(Cog):
|
|||
number: Option(int, "How many dies should be rolled?", min_value=1, max_value=10_000, default=1)
|
||||
):
|
||||
"""Roll some dice (default 1d6)"""
|
||||
await ctx.respond(Random.roll_results(faces=faces, number=number))
|
||||
return await ctx.respond(Random.roll_results(faces=faces, number=number))
|
||||
|
||||
# ==============================================================================
|
||||
|
||||
@staticmethod
|
||||
def flip(number: int = 1) -> str:
|
||||
"""Returns a raw list of Heads or Tails outcomes"""
|
||||
result = requests.get(f'https://www.random.org/integers/?num={number}&min=1&max=2&col=1&base=10&format=plain&rnd=new').text
|
||||
result: str = httpx.get(
|
||||
f'https://www.random.org/integers/?num={number}&min=1&max=2&col=1&base=10&format=plain&rnd=new'
|
||||
).text
|
||||
result = result.replace('1', 'Heads')
|
||||
result = result.replace('2', 'Tails')
|
||||
return result
|
||||
|
@ -70,7 +72,7 @@ class Random(Cog):
|
|||
@staticmethod
|
||||
def flip_results(number: int = 1) -> str:
|
||||
"""Returns a formatted result of Heads and Tails counts"""
|
||||
result = Random.flip(number)
|
||||
result: str = Random.flip(number)
|
||||
if len(result) == 6: # \n counts as a character and we don't strip it
|
||||
return (
|
||||
f"*You flipped {number} coins and got:*\n"
|
||||
|
@ -89,7 +91,7 @@ class Random(Cog):
|
|||
)
|
||||
async def flip_prefix(self, ctx: Context, number: str = 1):
|
||||
"""Flip a coin (default 1)"""
|
||||
await ctx.send(self.flip_results(number))
|
||||
return await ctx.send(self.flip_results(number))
|
||||
|
||||
@slash_command(
|
||||
name='flip',
|
||||
|
@ -100,4 +102,4 @@ class Random(Cog):
|
|||
number: Option(int, "How many coins should be flipped?", min_value=1, max_value=10_000, default=1)
|
||||
):
|
||||
"""Flip a coin (default 1)"""
|
||||
await ctx.respond(self.flip_results(number))
|
||||
return await ctx.respond(self.flip_results(number))
|
|
@ -36,4 +36,6 @@ class Unpin(Cog):
|
|||
embed=discord.Embed(title="Message unpinned", url=url),
|
||||
reference=message,
|
||||
mention_author=False
|
||||
)
|
||||
)
|
||||
if msg:
|
||||
print("Message unpin sent successfully")
|
|
@ -1,5 +1,6 @@
|
|||
import discord
|
||||
from discord.ext.commands import Cog
|
||||
from decouple import config
|
||||
|
||||
def setup(bot: discord.Bot):
|
||||
bot.add_cog(VCJoin(bot))
|
||||
|
@ -7,7 +8,7 @@ def setup(bot: discord.Bot):
|
|||
class VCJoin(Cog):
|
||||
"""Log when a member joins VC, for pinging purposes"""
|
||||
|
||||
CHANNEL = 949516670630760499
|
||||
CHANNEL: int = config("DISCORD_VC_CHANNEL", cast=int)
|
||||
|
||||
def __init__(self, bot: discord.Bot):
|
||||
self.bot: discord.Bot = bot
|
||||
|
@ -44,4 +45,6 @@ class VCJoin(Cog):
|
|||
name = f"{member.display_name} ({member})",
|
||||
icon_url = member.display_avatar.url,
|
||||
)
|
||||
return await self.channel.send(embed=embed)
|
||||
msg = await self.channel.send(embed=embed)
|
||||
if msg:
|
||||
print(embed.description[2:-2])
|
46
pyproject.toml
Normal file
46
pyproject.toml
Normal file
|
@ -0,0 +1,46 @@
|
|||
[project]
|
||||
name = "umi"
|
||||
version = "rolling-waves"
|
||||
description = "a self-hostable single-guild discord bot for playing music and announcing vc channel joins/leaves"
|
||||
authors = [
|
||||
{name = "a", email = "a@trwnh.com"},
|
||||
]
|
||||
dependencies = [
|
||||
"py-cord[voice,speed]>=2.0.0rc1",
|
||||
"asyncio",
|
||||
"yt-dlp",
|
||||
"python-decouple",
|
||||
"httpx",
|
||||
]
|
||||
requires-python = ">=3.10"
|
||||
license = {text = "AGPL"}
|
||||
readme = "README.md"
|
||||
|
||||
[project.optional-dependencies]
|
||||
|
||||
[tool.pdm.scripts]
|
||||
format = "yapf --in-place --recursive ./umi"
|
||||
|
||||
[tool.pdm.dev-dependencies]
|
||||
dev = [
|
||||
"yapf>=0.32.0",
|
||||
"toml>=0.10.2",
|
||||
]
|
||||
|
||||
[tool.yapf]
|
||||
"use_tabs" = true
|
||||
"indent_width" = 1
|
||||
"blank_lines_between_top_level_imports_and_variables" = 0
|
||||
"dedent_closing_brackets" = true
|
||||
"coalesce_brackets" = false
|
||||
"continuation_align_style" = "fixed"
|
||||
"continuation_indent_width" = 1
|
||||
"blank_lines_around_top_level_definition" = 1
|
||||
"indent_blank_lines" = true
|
||||
"spaces_before_comment" = 1
|
||||
"split_arguments_when_comma_terminated" = true
|
||||
"allow_split_before_dict_value" = false
|
||||
|
||||
[build-system]
|
||||
requires = ["pdm-pep517>=1.0.0"]
|
||||
build-backend = "pdm.pep517.api"
|
|
@ -1,14 +1,5 @@
|
|||
pip
|
||||
requests
|
||||
pymumble
|
||||
py-cord>=2.0.0rc1
|
||||
py-cord[voice]>=2.0.0rc1
|
||||
py-cord[speed]>=2.0.0rc1
|
||||
py-cord[voice,speed]>=2.0.0rc1
|
||||
asyncio
|
||||
yt-dlp
|
||||
pynacl
|
||||
aiodns
|
||||
brotlipy
|
||||
cchardet
|
||||
orjson
|
||||
python-dotenv
|
||||
python-decouple
|
||||
httpx
|
Loading…
Reference in a new issue