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:
a 2022-11-21 09:29:03 -06:00
parent 5a0dff92b0
commit 4ac878fecb
12 changed files with 1292 additions and 76 deletions

7
.env.example Normal file
View 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
View File

@ -1,6 +1,9 @@
.env
.venv
.cache/*
.logs/*
sounds/originals/*
sounds/normalized/*
cogs/__pycache__/*
.pdm.toml
__pypackages__/

51
README.md Normal file
View 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
View File

@ -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 =======================================

View File

@ -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")

View File

@ -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)

View File

@ -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))

View File

@ -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")

View File

@ -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])

1141
pdm.lock Normal file

File diff suppressed because it is too large Load Diff

46
pyproject.toml Normal file
View 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"

View File

@ -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