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
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
|
.env
|
||||||
|
.venv
|
||||||
.cache/*
|
.cache/*
|
||||||
.logs/*
|
.logs/*
|
||||||
sounds/originals/*
|
sounds/originals/*
|
||||||
sounds/normalized/*
|
sounds/normalized/*
|
||||||
cogs/__pycache__/*
|
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 discord
|
||||||
import pymumble_py3 as pymumble
|
|
||||||
|
|
||||||
""" Declare intents that the bot will use """
|
""" Declare intents that the bot will use """
|
||||||
intents = discord.Intents.default()
|
intents = discord.Intents.default()
|
||||||
|
@ -14,15 +13,13 @@ intents.reactions = True
|
||||||
intents.voice_states = True
|
intents.voice_states = True
|
||||||
|
|
||||||
""" Load the bot token """
|
""" Load the bot token """
|
||||||
from dotenv import load_dotenv
|
from decouple import config
|
||||||
from os import getenv
|
DISCORD_TOKEN: str = config("DISCORD_BOT_TOKEN")
|
||||||
load_dotenv()
|
DISCORD_GUILD: int = config("DISCORD_GUILD_ID", cast=int)
|
||||||
DISCORD_TOKEN = getenv("DISCORD_BOT_TOKEN")
|
|
||||||
DISCORD_GUILD = int(getenv("DISCORD_GUILD_ID"))
|
|
||||||
|
|
||||||
""" Initialize the Discord bot """
|
""" Initialize the Discord bot """
|
||||||
from discord.ext.commands import Bot, when_mentioned_or
|
from discord.ext.commands import Bot, when_mentioned_or
|
||||||
bot = Bot(
|
bot: Bot = Bot(
|
||||||
command_prefix=when_mentioned_or('..', '>', '.'),
|
command_prefix=when_mentioned_or('..', '>', '.'),
|
||||||
description="A list of commands available",
|
description="A list of commands available",
|
||||||
intents=intents,
|
intents=intents,
|
||||||
|
@ -37,6 +34,7 @@ async def on_ready():
|
||||||
|
|
||||||
""" Load cogs """
|
""" Load cogs """
|
||||||
from os import listdir
|
from os import listdir
|
||||||
|
|
||||||
def list_all_cogs() -> list[str]:
|
def list_all_cogs() -> list[str]:
|
||||||
return [file[:-3] for file in listdir("cogs") if file.endswith(".py")]
|
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.ext.commands import Context
|
||||||
from discord import AutocompleteContext, ApplicationContext
|
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() )]
|
return [cog for cog in list_all_cogs() if cog.lower().startswith( ctx.value.lower() )]
|
||||||
|
|
||||||
ROLE_ADMIN = 518625964763119616
|
ADMIN_ROLE: int = config("DISCORD_BOT_ADMIN_ROLE_ID", cast=int, default=0)
|
||||||
ROLE_ADMIN_SFW = 727205354353721374
|
ADMIN_USER: int = config("DISCORD_BOT_ADMIN_USER_ID", cast=int)
|
||||||
ME = 201046736565829632
|
|
||||||
|
|
||||||
def allowed_to_reload(ctx: Context | ApplicationContext):
|
def allowed_to_reload(ctx: Context | ApplicationContext) -> bool:
|
||||||
roles = [role.id for role in ctx.author.roles]
|
roles: set[int] = set([role.id for role in ctx.author.roles])
|
||||||
admin = ROLE_ADMIN in roles
|
admin: bool = ADMIN_ROLE in roles
|
||||||
sfw_admin = ROLE_ADMIN_SFW in roles
|
owner: bool = ctx.author.id == ADMIN_USER
|
||||||
owner = ctx.author.id == ME
|
return any([admin, owner])
|
||||||
return any([admin, sfw_admin, owner])
|
|
||||||
|
|
||||||
from cogs.music import Music
|
from cogs.music import Music
|
||||||
def reload_music(ctx):
|
def reload_music(ctx):
|
||||||
|
@ -89,19 +85,17 @@ def reload_music(ctx):
|
||||||
music.search_results = search_results
|
music.search_results = search_results
|
||||||
|
|
||||||
@bot.command(name='reload')
|
@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)"""
|
"""Reload an extension (admin command)"""
|
||||||
if not allowed_to_reload(ctx):
|
if not allowed_to_reload(ctx):
|
||||||
return await ctx.send(
|
return await ctx.send("You must be an admin or bot owner to use this command")
|
||||||
"You must be an admin or bot owner to use this command"
|
|
||||||
)
|
|
||||||
if not cog:
|
if not cog:
|
||||||
return await ctx.send("Please specify a cog to reload")
|
return await ctx.send("Please specify a cog to reload")
|
||||||
elif cog.lower() == "music":
|
elif cog.lower() == "music":
|
||||||
reload_music(ctx)
|
reload_music(ctx)
|
||||||
else:
|
else:
|
||||||
bot.reload_extension(f"cogs.{cog}")
|
bot.reload_extension(f"cogs.{cog}")
|
||||||
await ctx.send(f"Reloaded `{cog}` extension")
|
return await ctx.send(f"Reloaded `{cog}` extension")
|
||||||
|
|
||||||
@bot.slash_command(
|
@bot.slash_command(
|
||||||
name='reload',
|
name='reload',
|
||||||
|
@ -121,7 +115,7 @@ async def reload_slash(
|
||||||
reload_music(ctx)
|
reload_music(ctx)
|
||||||
else:
|
else:
|
||||||
bot.reload_extension(f"cogs.{cog}")
|
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 =======================================
|
# ================================== 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
|
from discord.ext.pages import Paginator
|
||||||
import asyncio # used to run async functions within regular functions
|
import asyncio # used to run async functions within regular functions
|
||||||
import subprocess # for running ffprobe and getting duration of files
|
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
|
from time import time # performance tracking
|
||||||
import random # for shuffling the queue
|
import random # for shuffling the queue
|
||||||
import math # for ceiling function in queue pages
|
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
|
# "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
|
"extract_flat": True, # massive speedup for fetching metadata, at the cost of no upload date
|
||||||
}
|
}
|
||||||
username = getenv("YOUTUBE_USERNAME")
|
# username = config("YOUTUBE_USERNAME")
|
||||||
password = getenv("YOUTUBE_PASSWORD")
|
# password = config("YOUTUBE_PASSWORD")
|
||||||
ytdl = youtube_dl.YoutubeDL(ytdl_format_options)
|
ytdl = youtube_dl.YoutubeDL(ytdl_format_options)
|
|
@ -1,12 +1,12 @@
|
||||||
import discord
|
import discord
|
||||||
from discord.ext.commands import Cog, command, Context
|
from discord.ext.commands import Cog, command, Context
|
||||||
from discord.commands import slash_command, Option
|
from discord.commands import slash_command, Option
|
||||||
import requests
|
import httpx
|
||||||
from os import getenv
|
from decouple import config
|
||||||
from collections import Counter
|
from collections import Counter
|
||||||
import re
|
import re
|
||||||
|
|
||||||
GUILD = int(getenv("DISCORD_GUILD_ID"))
|
GUILD = config("DISCORD_GUILD_ID", cast=int)
|
||||||
|
|
||||||
def setup(bot: discord.Bot):
|
def setup(bot: discord.Bot):
|
||||||
bot.add_cog(Random(bot))
|
bot.add_cog(Random(bot))
|
||||||
|
@ -23,7 +23,7 @@ class Random(Cog):
|
||||||
def roll(faces: int = 6, number: int = 1) -> str:
|
def roll(faces: int = 6, number: int = 1) -> str:
|
||||||
"""Roll die(s) using random.org integer generation"""
|
"""Roll die(s) using random.org integer generation"""
|
||||||
columns = number if number < 11 else 10
|
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
|
@staticmethod
|
||||||
def roll_results(faces: int = 6, number: int = 1) -> str:
|
def roll_results(faces: int = 6, number: int = 1) -> str:
|
||||||
|
@ -43,7 +43,7 @@ class Random(Cog):
|
||||||
number, faces = query.split('d')
|
number, faces = query.split('d')
|
||||||
if not number: # handle raw dN rolls, e.g. 'd6' or 'd20'
|
if not number: # handle raw dN rolls, e.g. 'd6' or 'd20'
|
||||||
number = 1
|
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(
|
@slash_command(
|
||||||
name='roll',
|
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)
|
number: Option(int, "How many dies should be rolled?", min_value=1, max_value=10_000, default=1)
|
||||||
):
|
):
|
||||||
"""Roll some dice (default 1d6)"""
|
"""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
|
@staticmethod
|
||||||
def flip(number: int = 1) -> str:
|
def flip(number: int = 1) -> str:
|
||||||
"""Returns a raw list of Heads or Tails outcomes"""
|
"""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('1', 'Heads')
|
||||||
result = result.replace('2', 'Tails')
|
result = result.replace('2', 'Tails')
|
||||||
return result
|
return result
|
||||||
|
@ -70,7 +72,7 @@ class Random(Cog):
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def flip_results(number: int = 1) -> str:
|
def flip_results(number: int = 1) -> str:
|
||||||
"""Returns a formatted result of Heads and Tails counts"""
|
"""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
|
if len(result) == 6: # \n counts as a character and we don't strip it
|
||||||
return (
|
return (
|
||||||
f"*You flipped {number} coins and got:*\n"
|
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):
|
async def flip_prefix(self, ctx: Context, number: str = 1):
|
||||||
"""Flip a coin (default 1)"""
|
"""Flip a coin (default 1)"""
|
||||||
await ctx.send(self.flip_results(number))
|
return await ctx.send(self.flip_results(number))
|
||||||
|
|
||||||
@slash_command(
|
@slash_command(
|
||||||
name='flip',
|
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)
|
number: Option(int, "How many coins should be flipped?", min_value=1, max_value=10_000, default=1)
|
||||||
):
|
):
|
||||||
"""Flip a coin (default 1)"""
|
"""Flip a coin (default 1)"""
|
||||||
await ctx.respond(self.flip_results(number))
|
return await ctx.respond(self.flip_results(number))
|
|
@ -37,3 +37,5 @@ class Unpin(Cog):
|
||||||
reference=message,
|
reference=message,
|
||||||
mention_author=False
|
mention_author=False
|
||||||
)
|
)
|
||||||
|
if msg:
|
||||||
|
print("Message unpin sent successfully")
|
|
@ -1,5 +1,6 @@
|
||||||
import discord
|
import discord
|
||||||
from discord.ext.commands import Cog
|
from discord.ext.commands import Cog
|
||||||
|
from decouple import config
|
||||||
|
|
||||||
def setup(bot: discord.Bot):
|
def setup(bot: discord.Bot):
|
||||||
bot.add_cog(VCJoin(bot))
|
bot.add_cog(VCJoin(bot))
|
||||||
|
@ -7,7 +8,7 @@ def setup(bot: discord.Bot):
|
||||||
class VCJoin(Cog):
|
class VCJoin(Cog):
|
||||||
"""Log when a member joins VC, for pinging purposes"""
|
"""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):
|
def __init__(self, bot: discord.Bot):
|
||||||
self.bot: discord.Bot = bot
|
self.bot: discord.Bot = bot
|
||||||
|
@ -44,4 +45,6 @@ class VCJoin(Cog):
|
||||||
name = f"{member.display_name} ({member})",
|
name = f"{member.display_name} ({member})",
|
||||||
icon_url = member.display_avatar.url,
|
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
|
py-cord[voice,speed]>=2.0.0rc1
|
||||||
requests
|
|
||||||
pymumble
|
|
||||||
py-cord>=2.0.0rc1
|
|
||||||
py-cord[voice]>=2.0.0rc1
|
|
||||||
py-cord[speed]>=2.0.0rc1
|
|
||||||
asyncio
|
asyncio
|
||||||
yt-dlp
|
yt-dlp
|
||||||
pynacl
|
python-decouple
|
||||||
aiodns
|
httpx
|
||||||
brotlipy
|
|
||||||
cchardet
|
|
||||||
orjson
|
|
||||||
python-dotenv
|
|
Loading…
Reference in a new issue