Compare commits

...

5 Commits

Author SHA1 Message Date
6659079d7a Clean up type hints everywhere, overhaul bot configuration
Some checks failed
Build Heidi Docker image / build-docker (push) Failing after 28s
2025-09-23 20:39:11 +02:00
591f4ea191 Add line-length configuration 2025-09-23 20:37:38 +02:00
f167b23dcc Update development dependencies 2025-09-23 20:37:27 +02:00
d319fa21f5 Update python interpreter version in dockerfile 2025-09-23 20:37:17 +02:00
44a1ea2c83 Add sounds 2025-09-23 20:34:28 +02:00
60 changed files with 538 additions and 261 deletions

View File

@ -1,10 +1,10 @@
# syntax=docker/dockerfile:1 FROM python:3.13.7-slim-buster
RUN apt-get update -y && apt-get install -y ffmpeg libopus0
FROM python:3.10.1-slim-buster
RUN apt-get update -y
RUN apt-get install -y ffmpeg libopus0
WORKDIR /app WORKDIR /app
COPY requirements.txt requirements.txt
RUN pip3 install -r requirements.txt
COPY . . COPY . .
RUN pip3 install -r requirements.txt
CMD ["python3", "-u", "bot.py"] CMD ["python3", "-u", "bot.py"]

308
bot.py
View File

@ -1,26 +1,35 @@
# Example: https://github.com/Rapptz/discord.py/blob/master/examples/app_commands/basic.py # Example: https://github.com/Rapptz/discord.py/blob/master/examples/app_commands/basic.py
from ast import Call import os
import random, logging import random
from discord import DMChannel import logging
from types import CoroutineType
from typing import Any, Callable, override
from discord import app_commands, ui
from discord.app_commands import Choice from discord.app_commands import Choice
from typing import Awaitable, Dict, List, Optional, Union, Callable, Any
from rich.traceback import install
from heidi_client import * from discord.client import Client
from discord.components import SelectOption
from discord.enums import ButtonStyle
from discord.flags import Intents
from discord.interactions import Interaction
from discord.member import Member, VoiceState
from discord.message import Message
# Install rich traceback from heidi_client import HeidiClient
install(show_locals=True) from heidi_config import ConfigSection, FlagsConfigKey
from heidi_constants import DOCKER, HEIDI_SPAM_ID, SOUNDDIR
from heidi_helpers import enforce_channel, play_voice_line_for_member
# @todo yt-dlp music support # TODO: yt-dlp music support
# Log to file # Log to file
handler = logging.FileHandler(filename="discord.log", encoding="utf-8", mode="w") handler = logging.FileHandler(filename="discord.log", encoding="utf-8", mode="w")
# Intents specification is no longer optional # Intents specification is no longer optional
intents = discord.Intents.default() intents: Intents = Intents.default()
intents.members = True # Allow to react to member join/leave etc intents.members = True # Allow to react to member join/leave etc
intents.message_content = True # Allow to read message content from arbitrary messages intents.message_content = True # Allow to read message content from arbitrary messages
intents.voice_states = True # Allow to process on_voice_state_update intents.voice_states = True # Allow to process on_voice_state_update
@ -63,9 +72,7 @@ async def on_message(message: Message) -> None:
@client.event @client.event
async def on_voice_state_update( async def on_voice_state_update(member: Member, before: VoiceState, after: VoiceState) -> None:
member: Member, before: VoiceState, after: VoiceState
) -> None:
""" """
This event triggers when a member joins/changes/leaves a voice channel or mutes/unmutes. This event triggers when a member joins/changes/leaves a voice channel or mutes/unmutes.
""" """
@ -77,7 +84,9 @@ async def on_voice_state_update(
# python iterates over the keys of a map # python iterates over the keys of a map
for predicate in client.on_voice_state_triggers: for predicate in client.on_voice_state_triggers:
if predicate(member, before, after): if predicate(member, before, after):
action: Callable = client.on_voice_state_triggers[predicate] action: Callable[[Member, VoiceState, VoiceState], CoroutineType[Any, Any, None]] = (
client.on_voice_state_triggers[predicate]
)
print(f"on_voice_state_update: calling {action.__name__}") print(f"on_voice_state_update: calling {action.__name__}")
await action(member, before, after) await action(member, before, after)
@ -85,114 +94,194 @@ async def on_voice_state_update(
# Config Commands -------------------------------------------------------------------------------- # Config Commands --------------------------------------------------------------------------------
class EntranceSoundSoundSelect(discord.ui.Select): class FlagValueSelect(ui.Select[ui.View]):
def __init__(self, board: str, on_sound_select_callback): def __init__(
self.board = board self, flag: str, on_flag_select_callback: Callable[[Interaction, str, str], CoroutineType[Any, Any, None]]
self.on_sound_select_callback = on_sound_select_callback ) -> None:
self.flag: str = flag
options: List[discord.SelectOption] = [ self.on_flag_select_callback: Callable[[Interaction, str, str], CoroutineType[Any, Any, None]] = (
discord.SelectOption(label=sound.split(".")[0], value=sound) on_flag_select_callback
for sound in os.listdir(f"{SOUNDDIR}/{board}")
]
super().__init__(
placeholder="Select Sound", min_values=1, max_values=1, options=options
) )
async def callback(self, interaction: Interaction): options: list[SelectOption] = [SelectOption(label=val, value=val) for val in ["True", "False"]]
await self.on_sound_select_callback(interaction, self.board, self.values[0])
super().__init__(placeholder="Select Value", min_values=1, max_values=1, options=options)
@override
async def callback(self, interaction: Interaction) -> None:
await self.on_flag_select_callback(interaction, self.flag, self.values[0])
class EntranceSoundSoundView(discord.ui.View): class FlagValueView(ui.View):
def __init__(self, board: str, on_sound_select_callback): def __init__(
self, flag: str, on_flag_select_callback: Callable[[Interaction, str, str], CoroutineType[Any, Any, None]]
) -> None:
super().__init__(timeout=600) super().__init__(timeout=600)
self.add_item(EntranceSoundSoundSelect(board, on_sound_select_callback)) _ = self.add_item(FlagValueSelect(flag, on_flag_select_callback))
class EntranceSoundBoardSelect(discord.ui.Select): class FlagsSelect(ui.Select[ui.View]):
def __init__(self, on_sound_select_callback): def __init__(
self.on_sound_select_callback = on_sound_select_callback self, on_flag_select_callback: Callable[[Interaction, str, str], CoroutineType[Any, Any, None]]
) -> None:
options: List[discord.SelectOption] = [ self.on_flag_select_callback: Callable[[Interaction, str, str], CoroutineType[Any, Any, None]] = (
discord.SelectOption(label=board, value=board) on_flag_select_callback
for board in os.listdir(f"{SOUNDDIR}")
]
super().__init__(
placeholder="Select Board", min_values=1, max_values=1, options=options
) )
async def callback(self, interaction: Interaction): options: list[SelectOption] = [SelectOption(label=flag.value, value=flag.value) for flag in FlagsConfigKey]
await interaction.response.send_message(
f"Welchen sound willst du?", super().__init__(placeholder="Select Flag", min_values=1, max_values=1, options=options)
@override
async def callback(self, interaction: Interaction) -> None:
_ = await interaction.response.send_message(
"Welchen Wert willst du setzen?",
view=FlagValueView(self.values[0], self.on_flag_select_callback),
ephemeral=True,
)
class FlagsView(ui.View):
def __init__(
self, on_flag_select_callback: Callable[[Interaction, str, str], CoroutineType[Any, Any, None]]
) -> None:
super().__init__(timeout=600)
_ = self.add_item(FlagsSelect(on_flag_select_callback))
class EntranceSoundBoardSelect(ui.Select[ui.View]):
def __init__(
self, on_sound_select_callback: Callable[[Interaction, str, str], CoroutineType[Any, Any, None]]
) -> None:
self.on_sound_select_callback: Callable[[Interaction, str, str], CoroutineType[Any, Any, None]] = (
on_sound_select_callback
)
options: list[SelectOption] = [SelectOption(label=board, value=board) for board in os.listdir(f"{SOUNDDIR}")]
super().__init__(placeholder="Select Board", min_values=1, max_values=1, options=options)
@override
async def callback(self, interaction: Interaction) -> None:
_ = await interaction.response.send_message(
"Welchen sound willst du?",
view=EntranceSoundSoundView(self.values[0], self.on_sound_select_callback), view=EntranceSoundSoundView(self.values[0], self.on_sound_select_callback),
ephemeral=True, ephemeral=True,
) )
class EntranceSoundBoardView(discord.ui.View): class EntranceSoundBoardView(ui.View):
def __init__(self, on_sound_select_callback): def __init__(
self, on_sound_select_callback: Callable[[Interaction, str, str], CoroutineType[Any, Any, None]]
) -> None:
super().__init__(timeout=600) super().__init__(timeout=600)
self.add_item(EntranceSoundBoardSelect(on_sound_select_callback)) _ = self.add_item(EntranceSoundBoardSelect(on_sound_select_callback))
async def user_config_key_autocomplete( class EntranceSoundSoundSelect(ui.Select[ui.View]):
interaction: Interaction, current: str def __init__(
) -> List[Choice[str]]: self, board: str, on_sound_select_callback: Callable[[Interaction, str, str], CoroutineType[Any, Any, None]]
) -> None:
self.board: str = board
self.on_sound_select_callback: Callable[[Interaction, str, str], CoroutineType[Any, Any, None]] = (
on_sound_select_callback
)
options: list[SelectOption] = [
SelectOption(label=sound.split(".")[0], value=sound) for sound in os.listdir(f"{SOUNDDIR}/{board}")
]
super().__init__(placeholder="Select Sound", min_values=1, max_values=1, options=options)
@override
async def callback(self, interaction: Interaction) -> None:
await self.on_sound_select_callback(interaction, self.board, self.values[0])
class EntranceSoundSoundView(ui.View):
def __init__(
self, board: str, on_sound_select_callback: Callable[[Interaction, str, str], CoroutineType[Any, Any, None]]
) -> None:
super().__init__(timeout=600)
_ = self.add_item(EntranceSoundSoundSelect(board, on_sound_select_callback))
async def user_config_key_autocomplete(interaction: Interaction, current: str) -> list[Choice[str]]:
""" """
Suggest a value from the user config keys (each .conf section is a key). Suggest a value from the user config keys (each .conf section is a key).
""" """
return [ return [
Choice(name=key, value=key) Choice(name=key, value=key) for key in client.user_config.sections() if key.lower().startswith(current.lower())
for key in client.user_config.sections()
if key.lower().startswith(current.lower())
] ]
@client.tree.command( @client.tree.command(
name="userconfig", name="heidiconfig",
description="User-spezifische Heidi-Einstellungen (Heidi merkt sie sich in ihrem riesigen Gehirn).", description="User-spezifische Heidi-Einstellungen (Heidi merkt sie sich in ihrem riesigen Gehirn).",
) )
@app_commands.rename(config_key="option") @app_commands.rename(config_key="option")
@app_commands.describe(config_key="Die Option, welche du ändern willst.") @app_commands.describe(config_key="Die Option, welche du ändern willst.")
@app_commands.autocomplete(config_key=user_config_key_autocomplete) @app_commands.autocomplete(config_key=user_config_key_autocomplete)
@enforce_channel(HEIDI_SPAM_ID) @enforce_channel(HEIDI_SPAM_ID)
async def user_config(interaction: Interaction, config_key: str) -> None: async def user_config(interaction: Interaction[Client], config_key: str) -> None:
""" """
Set a user config value for the calling user. Set a user config value for the calling user.
""" """
# Only Members can set settings # Only Members can set settings
if not isinstance(interaction.user, Member): if not isinstance(interaction.user, Member):
print("User not a member") print("User not a member")
await interaction.response.send_message( _ = await interaction.response.send_message("Heidi sagt: Komm in die Gruppe!", ephemeral=True)
"Heidi sagt: Komm in die Gruppe!", ephemeral=True
)
return return
member: Member = interaction.user member: Member = interaction.user
async def on_sound_select_callback(interaction, board: str, sound: str): async def on_flag_select_callback(interaction: Interaction, flag: str, value: str) -> None:
""" """
This function is called, when an EntrySoundSoundSelect option is selected. This function is called when an FlagValueSelect option is selected.
"""
client.user_config[ConfigSection.FLAGS.value][flag] = value
client.write_user_config()
_ = await interaction.response.send_message(
f"Ok, ich schreibe {flag}={value} in mein fettes Gehirn!",
ephemeral=True,
)
async def on_sound_select_callback(interaction: Interaction, board: str, sound: str) -> None:
"""
This function is called when an EntrySoundSoundSelect option is selected.
""" """
client.user_config[config_key][member.name] = f"{board}/{sound}" client.user_config[config_key][member.name] = f"{board}/{sound}"
client.write_user_config() client.write_user_config()
await interaction.response.send_message( _ = await interaction.response.send_message(
f"Ok, ich schreibe {member.name}={board}/{sound} in mein fettes Gehirn!", f"Ok, ich schreibe {member.name}={board}/{sound} in mein fettes Gehirn!",
ephemeral=True, ephemeral=True,
) )
# Views for different user config options are defined here # Views for different user config options are defined here
views = {"ENTRANCE.SOUND": (EntranceSoundBoardView, on_sound_select_callback)} views: dict[str, tuple[type[ui.View], Callable[..., CoroutineType[Any, Any, None]], str]] = {
ConfigSection.FLAGS.value: (
FlagsView,
on_flag_select_callback,
"Welches Setting möchtest du ändern?",
),
ConfigSection.ENTRANCE_SOUND.value: (
EntranceSoundBoardView,
on_sound_select_callback,
"Aus welchem Soundboard soll dein Sound sein?",
),
}
view, select_callback = views[config_key] view, select_callback, description = views[config_key]
await interaction.response.send_message( _ = await interaction.response.send_message(
f"Aus welchem Soundboard soll dein sound sein?", description,
view=view(select_callback), view=view(select_callback), # pyright: ignore[reportCallIssue]
ephemeral=True, ephemeral=True,
) )
@ -219,7 +308,8 @@ async def heidi_exclaim(interaction: Interaction) -> None:
"Models müssen da halt durch!", "Models müssen da halt durch!",
"Heul doch nicht!", "Heul doch nicht!",
] ]
await interaction.response.send_message(random.choice(messages))
_ = await interaction.response.send_message(random.choice(messages))
@client.tree.command(name="miesmuschel", description="Was denkt Heidi?") @client.tree.command(name="miesmuschel", description="Was denkt Heidi?")
@ -247,14 +337,13 @@ async def magic_shell(interaction: Interaction, question: str) -> None:
"In deinen Träumen.", "In deinen Träumen.",
"Tom sagt Nein", "Tom sagt Nein",
] ]
question = question.strip() question = question.strip()
question_mark = "" if question[-1] == "?" else "?" question_mark = "" if question[-1] == "?" else "?"
await interaction.response.send_message( _ = await interaction.response.send_message(f"{question}{question_mark}\nHeidi sagt: {random.choice(choices)}")
f"{question}{question_mark}\nHeidi sagt: {random.choice(choices)}"
)
# @todo Allow , separated varargs, need to parse manually as slash commands don't support varargs # TODO: Allow separated varargs, need to parse manually as slash commands don't support varargs
@client.tree.command(name="wähle", description="Heidi trifft die Wahl!") @client.tree.command(name="wähle", description="Heidi trifft die Wahl!")
@app_commands.rename(option_a="entweder") @app_commands.rename(option_a="entweder")
@app_commands.describe(option_a="Ist es vielleicht dies?") @app_commands.describe(option_a="Ist es vielleicht dies?")
@ -266,7 +355,7 @@ async def choose(interaction: Interaction, option_a: str, option_b: str) -> None
Select an answer from two options. Select an answer from two options.
""" """
options = [option_a.strip(), option_b.strip()] options = [option_a.strip(), option_b.strip()]
await interaction.response.send_message( _ = await interaction.response.send_message(
f"{options[0]} oder {options[1]}?\nHeidi sagt: {random.choice(options)}" f"{options[0]} oder {options[1]}?\nHeidi sagt: {random.choice(options)}"
) )
@ -274,40 +363,28 @@ async def choose(interaction: Interaction, option_a: str, option_b: str) -> None
# Sounds ----------------------------------------------------------------------------------------- # Sounds -----------------------------------------------------------------------------------------
async def board_autocomplete( async def board_autocomplete(interaction: Interaction, current: str) -> list[Choice[str]]:
interaction: Interaction, current: str
) -> List[Choice[str]]:
""" """
Suggest a sound board. Suggest a sound board.
""" """
boards: List[str] = os.listdir(SOUNDDIR) boards: list[str] = os.listdir(SOUNDDIR)
return [ return [Choice(name=board, value=board) for board in boards if board.lower().startswith(current.lower())]
Choice(name=board, value=board)
for board in boards
if board.lower().startswith(current.lower())
]
async def sound_autocomplete( async def sound_autocomplete(interaction: Interaction, current: str) -> list[Choice[str]]:
interaction: Interaction, current: str
) -> List[Choice[str]]:
""" """
Suggest a sound from an already selected board. Suggest a sound from an already selected board.
""" """
board: str = interaction.namespace.board board: str = interaction.namespace.board
sounds: List[str] = os.listdir(f"{SOUNDDIR}/{board}/") sounds: list[str] = os.listdir(f"{SOUNDDIR}/{board}/")
return [ return [
Choice(name=sound.split(".")[0], value=sound) Choice(name=sound.split(".")[0], value=sound) for sound in sounds if sound.lower().startswith(current.lower())
for sound in sounds
if sound.lower().startswith(current.lower())
] ]
@client.tree.command( @client.tree.command(name="sag", description="Heidi drückt den Knopf auf dem Soundboard.")
name="sag", description="Heidi drückt den Knopf auf dem Soundboard."
)
@app_commands.describe(sound="Was soll Heidi sagen?") @app_commands.describe(sound="Was soll Heidi sagen?")
@app_commands.autocomplete(board=board_autocomplete) @app_commands.autocomplete(board=board_autocomplete)
@app_commands.autocomplete(sound=sound_autocomplete) @app_commands.autocomplete(sound=sound_autocomplete)
@ -319,9 +396,7 @@ async def say_voiceline(interaction: Interaction, board: str, sound: str) -> Non
# Only Members can access voice channels # Only Members can access voice channels
if not isinstance(interaction.user, Member): if not isinstance(interaction.user, Member):
print("User not a member") print("User not a member")
await interaction.response.send_message( _ = await interaction.response.send_message("Heidi sagt: Komm in die Gruppe!", ephemeral=True)
"Heidi sagt: Komm in die Gruppe!", ephemeral=True
)
return return
member: Member = interaction.user member: Member = interaction.user
@ -329,47 +404,42 @@ async def say_voiceline(interaction: Interaction, board: str, sound: str) -> Non
await play_voice_line_for_member(interaction, member, board, sound) await play_voice_line_for_member(interaction, member, board, sound)
class InstantButton(discord.ui.Button): class InstantButton(ui.Button[ui.View]):
def __init__(self, label: str, board: str, sound: str): def __init__(self, label: str, board: str, sound: str) -> None:
super().__init__(style=discord.ButtonStyle.red, label=label) super().__init__(style=ButtonStyle.red, label=label)
self.board = board self.board: str = board
self.sound = sound self.sound: str = sound
async def callback(self, interaction: Interaction): @override
async def callback(self, interaction: Interaction) -> None:
""" """
Handle a press of the button. Handle a press of the button.
""" """
if not isinstance(interaction.user, Member): if not isinstance(interaction.user, Member):
await interaction.response.send_message( _ = await interaction.response.send_message(
"Heidi mag keine discord.User, nur discord.Member!", ephemeral=True "Heidi mag keine discord.User, nur discord.Member!", ephemeral=True
) )
return return
await play_voice_line_for_member( await play_voice_line_for_member(interaction, interaction.user, self.board, self.sound)
interaction, interaction.user, self.board, self.sound
)
class InstantButtonsView(discord.ui.View): class InstantButtonsView(ui.View):
def __init__(self, board: str, timeout=None): def __init__(self, board: str, timeout: float | None = None) -> None:
super().__init__(timeout=timeout) super().__init__(timeout=timeout)
sounds = os.listdir(f"{SOUNDDIR}/{board}") sounds = os.listdir(f"{SOUNDDIR}/{board}")
for sound in sounds: for sound in sounds:
self.add_item(InstantButton(sound.split(".")[0], board, sound)) _ = self.add_item(InstantButton(sound.split(".")[0], board, sound))
@client.tree.command( @client.tree.command(name="instantbuttons", description="Heidi malt Knöpfe für Sounds in den Chat.")
name="instantbuttons", description="Heidi malt Knöpfe für Sounds in den Chat."
)
@app_commands.describe(board="Welches Soundboard soll knöpfe bekommen?") @app_commands.describe(board="Welches Soundboard soll knöpfe bekommen?")
@app_commands.autocomplete(board=board_autocomplete) @app_commands.autocomplete(board=board_autocomplete)
@enforce_channel(HEIDI_SPAM_ID) @enforce_channel(HEIDI_SPAM_ID)
async def soundboard_buttons(interaction: Interaction, board: str) -> None: async def soundboard_buttons(interaction: Interaction, board: str) -> None:
await interaction.response.send_message( _ = await interaction.response.send_message(f"Soundboard: {board.capitalize()}", view=InstantButtonsView(board))
f"Soundboard: {board.capitalize()}", view=InstantButtonsView(board)
)
# Contextmenu ------------------------------------------------------------------------------------ # Contextmenu ------------------------------------------------------------------------------------
@ -384,13 +454,11 @@ async def insult(
Send an insult to a member via direct message. Send an insult to a member via direct message.
""" """
if not member.dm_channel: if not member.dm_channel:
await member.create_dm() _ = await member.create_dm()
if not member.dm_channel: if not member.dm_channel:
print("Error creating DMChannel!") print("Error creating DMChannel!")
await interaction.response.send_message( _ = await interaction.response.send_message("Heidi sagt: Gib mal DM Nummer süße*r!", ephemeral=True)
"Heidi sagt: Gib mal DM Nummer süße*r!", ephemeral=True
)
return return
insults = [ insults = [
@ -409,8 +477,8 @@ async def insult(
"Richtiger Gesichtsgünther ey!", "Richtiger Gesichtsgünther ey!",
] ]
await member.dm_channel.send(random.choice(insults)) _ = await member.dm_channel.send(random.choice(insults))
await interaction.response.send_message( _ = await interaction.response.send_message(
"Anzeige ist raus!", ephemeral=True "Anzeige ist raus!", ephemeral=True
) # with ephemeral = True only the caller can see the answer ) # with ephemeral = True only the caller can see the answer

44
flake.lock generated
View File

@ -2,15 +2,14 @@
"nodes": { "nodes": {
"devshell": { "devshell": {
"inputs": { "inputs": {
"nixpkgs": "nixpkgs", "nixpkgs": "nixpkgs"
"systems": "systems"
}, },
"locked": { "locked": {
"lastModified": 1701787589, "lastModified": 1741473158,
"narHash": "sha256-ce+oQR4Zq9VOsLoh9bZT8Ip9PaMLcjjBUHVPzW5d7Cw=", "narHash": "sha256-kWNaq6wQUbUMlPgw8Y+9/9wP0F8SHkjy24/mN3UAppg=",
"owner": "numtide", "owner": "numtide",
"repo": "devshell", "repo": "devshell",
"rev": "44ddedcbcfc2d52a76b64fb6122f209881bd3e1e", "rev": "7c9e793ebe66bcba8292989a68c0419b737a22a0",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -21,14 +20,14 @@
}, },
"flake-utils": { "flake-utils": {
"inputs": { "inputs": {
"systems": "systems_2" "systems": "systems"
}, },
"locked": { "locked": {
"lastModified": 1701680307, "lastModified": 1731533236,
"narHash": "sha256-kAuep2h5ajznlPMD9rnQyffWG8EM/C73lejGofXvdM8=", "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide", "owner": "numtide",
"repo": "flake-utils", "repo": "flake-utils",
"rev": "4022d587cbbfd70fe950c1e2083a02621806a725", "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -39,11 +38,11 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1677383253, "lastModified": 1722073938,
"narHash": "sha256-UfpzWfSxkfXHnb4boXZNaKsAcUrZT9Hw+tao1oZxd08=", "narHash": "sha256-OpX0StkL8vpXyWOGUD6G+MA26wAXK6SpT94kLJXo6B4=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "9952d6bc395f5841262b006fbace8dd7e143b634", "rev": "e36e9f57337d0ff0cf77aceb58af4c805472bfae",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -55,11 +54,11 @@
}, },
"nixpkgs_2": { "nixpkgs_2": {
"locked": { "locked": {
"lastModified": 1701693815, "lastModified": 1758446476,
"narHash": "sha256-7BkrXykVWfkn6+c1EhFA3ko4MLi3gVG0p9G96PNnKTM=", "narHash": "sha256-5rdAi7CTvM/kSs6fHe1bREIva5W3TbImsto+dxG4mBo=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "09ec6a0881e1a36c29d67497693a67a16f4da573", "rev": "a1f79a1770d05af18111fbbe2a3ab2c42c0f6cd0",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -90,21 +89,6 @@
"repo": "default", "repo": "default",
"type": "github" "type": "github"
} }
},
"systems_2": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
} }
}, },
"root": "root", "root": "root",

View File

@ -5,52 +5,43 @@
inputs.flake-utils.url = "github:numtide/flake-utils"; inputs.flake-utils.url = "github:numtide/flake-utils";
inputs.devshell.url = "github:numtide/devshell"; inputs.devshell.url = "github:numtide/devshell";
outputs = { self, nixpkgs, flake-utils, devshell }: outputs = {
flake-utils.lib.eachDefaultSystem (system: self,
let nixpkgs,
pkgs = import nixpkgs { flake-utils,
inherit system; devshell,
config.allowUnfree = true; }:
overlays = [ devshell.overlays.default ]; flake-utils.lib.eachDefaultSystem (system: let
}; pkgs = import nixpkgs {
inherit system;
config.allowUnfree = true;
overlays = [devshell.overlays.default];
};
myPython = pkgs.python311.withPackages (p: with p; [ python = pkgs.python313.withPackages (p:
# Basic with p; [
rich
# Discord
discordpy
python-dotenv python-dotenv
pynacl
# Scraping discordpy
# beautifulsoup4 pynacl # DiscordPy Voice Support
# requests
# MachineLearning
# torch-rocm
# torchvision-rocm
# numpy
# matplotlib
# nltk
]); ]);
in { in {
devShell = pkgs.devshell.mkShell { devShell = pkgs.devshell.mkShell {
name = "HeidiBot"; name = "HeidiBot";
packages = with pkgs; [ packages = with pkgs; [
myPython python
# nodePackages.pyright # LSP # nodePackages.pyright # LSP
]; ];
# Use $1 for positional args # Use $1 for positional args
commands = [ commands = [
# { # {
# name = ""; # name = "";
# help = ""; # help = "";
# command = ""; # command = "";
# } # }
]; ];
}; };
}); });
} }

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b8cc7d0eafc46aa981616128e1e86d1a27f36054aef4059076716d0480c96f00
size 66590

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:513a8c09c497af17832cf97dd927588ead6eae176e17cdcea07ffb0af517da4e
size 1918439

BIN
heidi-sounds/bg3/Disgusting.mp3 (Stored with Git LFS) Normal file

Binary file not shown.

BIN
heidi-sounds/bg3/Hahaha.mp3 (Stored with Git LFS) Normal file

Binary file not shown.

BIN
heidi-sounds/bg3/Honk.mp3 (Stored with Git LFS) Normal file

Binary file not shown.

BIN
heidi-sounds/bg3/Start talking.mp3 (Stored with Git LFS) Normal file

Binary file not shown.

BIN
heidi-sounds/bg3/This group is full of weirdos.mp3 (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:75cc48c3a5dc85a8304b1194065ca2ab4c6c6b6295f0a55e2889c9925d005e38
size 1194504

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a4cb6bf7b7bfdd050ecf60bf05bc2a6a5ce9c0077e673a4cf97dbfb2ecafb9a1
size 688896

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:81e355325f1e0c54ad187ef6c3873f3dadc19b1dee42b65ee5c210696480e733
size 812079

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:81fb57e99dae06e799be43135acf2eb400096cf6d06fbdbf282b8825e93e3972
size 657071

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0d6d41e2e33cde49b5c4a3e55a52f18506330b172a99ae708a58d60509151d6f
size 1054582

BIN
heidi-sounds/henri/Dusch dich.mkv (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:9bbd10f725147df249ec92718b4559bdd8be7eca97e5d976f936b9d825afb5aa
size 1034217

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:fb66307f990103230f9c63a27a21b2ebda924135ab330f5b595f5f37639e12b0
size 1144977

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0ede6a0d526f181785322cb87d1a27688a99f6f71fbd37d784e28c6641ebb70c
size 743647

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b0c9e2a5fc2e3529abd0e680b7a139b9239eae27f1e991b39f78187bb1a0fb88
size 939420

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ee3fdbcafcba7e23cc60a45a5b5cc433068d8ca97b0e4d4be668148b9dab167c
size 753935

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e4f057443b96e7a8e38e5cf7186083e375378f54f2aa314891a64625b9b1ba61
size 1466235

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:be7a6ba0f90793ff595dd56d06196b9029984976a3d61e928cfc7fcec51741e6
size 240103

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0d333644b63155bb732937aadd4147fb67c77d623dc55af862ef5e8218b2ea61
size 591141

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c0d8740ef2a7dfea55bdabf7fb43c78ae63d2998f117bb49371e90c2897cfa65
size 1608532

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:1f224f5643e99e4515a1d377295272a3f7e044db20b49ce246b50709ad632ff0
size 928159

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7d17a0f74141389e73a644546d050b57aa7ad7aec2a707f57dfebfed5256d565
size 1899825

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:1cd9cf0792ed7bafd38c760606d28ab366ff4fb320e41151161bd95b5ecc6848
size 3682445

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:da44b79bef932c3387962d11534a49d6a53d90574891d7f96fa780a836ea9b71
size 1911644

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:44b776ebde62c761dcd60dbfb9d90eb7b4f76861a06a14876d55f6f41585dbd6
size 2104870

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:cfbb90b495f0006302b94d23081959aa90881cd7c8326963d99bd7971b16053d
size 871686

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:269b4b0b2d0e7d6b830de04128eb9dc42a20e27f4ca025e43bde20c3f4b84ff9
size 18344

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:1f3f55803a1933ec15d60471cc0f3513d0ba009f1b5d398cca27fcfea5f2fbf4
size 49007

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b9fe1a53ec2f0970c2a3f58e0734bbb5f4515d5be42ea4fb0f1062f1731e3d1e
size 2294454

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:41f86b71a9da251552f9aefd2603b8221c0066ae2a9156ee2642285a0a48c3bf
size 21591

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0e66e31216df98dc166528576f9d69f1a1b64ebb49b3790a56b285fee73b7a2e
size 1512452

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:804d0bba869c6961d6b9d53ad3cdceea3c4b5108b2cb3d10fb8525f9f93738b7
size 3277223

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0976ab71b0e76a100317e6fa98d3cf13d71f082ddaebcf482b0c73d35869add2
size 1827568

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:5f3dc5640813d6948acddb0c7ac64af301afb55e7a9cc5b7b350a8a23e7c2328
size 1659634

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:db6440955702bb50f4325b6192335b36ba15d66bfb4d95de96cd2ffa044cb437
size 1373947

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d16638c23a33d57cbd3b039db6361ff1a2c01acd2e6d4b348aece0db9ef0b092
size 2051380

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e645dba27b5c2b63a2647396c8f6af8bf7c84c0c9a01e417fea95098244362a5
size 1342773

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a29dc73ec6a2dd9bf5ad4a0b906c2136601217fbff9659c544b03e5c5e2ba8de
size 1705496

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:4fb141ec779838b335ed99fd5efd5a8bee940e38154d29bf8cee96b69176b025
size 658379

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7f149a7235c98c81279bd0c8196563fa4ba3fe35f1fcdb7b7ce7570d45224ea1
size 802502

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:11f2ea7cfcfc2470c62a868d504037eba69746b6eaf9449071ebba49933ede2f
size 1587716

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:bb6f7a6126ab28541a38e01a19458a02ae9c3ca66d704931de0373d1a366034e
size 3222195

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:cf17f143fefe52227a9f60f2ca8a12f85288c6ccf9136053302392379d0b038f
size 1851666

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f5bb768b34bd64fb1804880c14b86f88a0960d581832f8b293ca191591a4b0f7
size 1381026

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:388600e942c443f545ed62172f1ac238c6a0046a67cb7478e56082902ad0f93a
size 1369564

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:370ce2efbbd1976b4ea6b7949f9b1c973fd4d2a13a291a7cba8458baf545935d
size 855748

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ead05e566688064e5339b21de1853dc4ae826bb5a37d071e4f92115ac901e21e
size 1096640

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:128296c98fadfcad218b03e4e682acfe37235b1bbfc3c4542db36adec0be443a
size 30877

View File

@ -1,22 +1,49 @@
import configparser import asyncio
from discord import app_commands, Message, VoiceState from configparser import ConfigParser
import os
from types import CoroutineType
from typing import Any, Callable, override
from discord import Message, VoiceState
from discord.activity import Activity
from discord.app_commands.tree import CommandTree
from discord.client import Client
from discord.enums import ActivityType, Status, StatusDisplayType
from discord.flags import Intents
from discord.member import Member
from discord.state import VoiceChannel
from heidi_constants import * from heidi_config import USER_CONFIG_SCHEME, ConfigSection, FlagsConfigKey
from heidi_helpers import * from heidi_constants import CONFIGPATH, LINUS_GUILD, TEST_GUILD, USERCONFIGNAME
from heidi_helpers import create_author_based_message_predicate, play_voice_line_for_member
class HeidiClient(discord.Client): class HeidiClient(Client):
def __init__(self, *, intents: discord.Intents): def __init__(self, *, intents: Intents):
super().__init__(status="Nur eine kann GNTM werden!", intents=intents) client_activity: Activity = Activity(
name="GNTM",
url="https://www.joyn.de/serien/germanys-next-topmodel",
type=ActivityType.competing,
state="Nur eine kann es werden!",
# details="Details",
# platform="Platform",
status_display_type=StatusDisplayType.state,
)
super().__init__(
activity=client_activity,
status=Status.online,
intents=intents,
)
# Separate object that keeps all application command state # Separate object that keeps all application command state
self.tree = app_commands.CommandTree(self) self.tree: CommandTree = CommandTree(self)
# Handle persistent user configuration # Handle persistent user configuration
self.user_config = configparser.ConfigParser() self.user_config: ConfigParser = ConfigParser()
if not os.path.exists(f"{CONFIGPATH}/{USERCONFIGNAME}"): if not os.path.exists(f"{CONFIGPATH}/{USERCONFIGNAME}"):
open(f"{CONFIGPATH}/{USERCONFIGNAME}", "x") # Create the file
self.user_config.read(f"{CONFIGPATH}/{USERCONFIGNAME}") _ = open(f"{CONFIGPATH}/{USERCONFIGNAME}", "x")
_ = self.user_config.read(f"{CONFIGPATH}/{USERCONFIGNAME}")
self.update_to_default_user_config() self.update_to_default_user_config()
self.print_user_config() self.print_user_config()
@ -24,43 +51,53 @@ class HeidiClient(discord.Client):
# on_message_triggers is a map with tuples of two functions: (predicate, action) # on_message_triggers is a map with tuples of two functions: (predicate, action)
# the predicate receives the message as argument # the predicate receives the message as argument
# if the predicate is true the action is performed # if the predicate is true the action is performed
self.on_message_triggers = { self.on_message_triggers: dict[
Callable[[Message], bool],
Callable[[Message], CoroutineType[Any, Any, None]],
] = {
# lambda m: m.author.nick.lower() in self.models.get_in_names(): self.autoreact_to_girls, # lambda m: m.author.nick.lower() in self.models.get_in_names(): self.autoreact_to_girls,
lambda m: "jeremy" in m.author.nick.lower(): self._autoreact_to_jeremy, create_author_based_message_predicate(["jeremy"]): self._autoreact_to_jeremy,
lambda m: "kardashian" in m.author.nick.lower() create_author_based_message_predicate(["kardashian", "jenner"]): self._autoreact_to_kardashian,
or "jenner" in m.author.nick.lower(): self._autoreact_to_kardashian,
} }
# automatic actions on voice state changes # automatic actions on voice state changes
# on_voice_state_triggers is a map with tuples of two functions: (predicate, action) # on_voice_state_triggers is a map with tuples of two functions: (predicate, action)
# the predicate receives the member, before- and after-state as arguments # the predicate receives the member, before- and after-state as arguments
# if the predicate is true, the action is performed # if the predicate is true, the action is performed
self.on_voice_state_triggers = { self.on_voice_state_triggers: dict[
lambda m, b, a: b.channel != a.channel Callable[[Member, VoiceState, VoiceState], bool],
and a.channel is not None Callable[[Member, VoiceState, VoiceState], CoroutineType[Any, Any, None]],
and isinstance(a.channel, VoiceChannel): self._play_entrance_sound, ] = {
lambda member, before, after: before.channel != after.channel
and after.channel is not None
and isinstance(after.channel, VoiceChannel): self._play_entrance_sound,
} }
# Synchronize commands to guilds # Synchronize commands to guilds
async def setup_hook(self): @override
async def setup_hook(self) -> None:
self.tree.copy_global_to(guild=LINUS_GUILD) self.tree.copy_global_to(guild=LINUS_GUILD)
await self.tree.sync(guild=LINUS_GUILD) _ = await self.tree.sync(guild=LINUS_GUILD)
self.tree.copy_global_to(guild=TEST_GUILD) self.tree.copy_global_to(guild=TEST_GUILD)
await self.tree.sync(guild=TEST_GUILD) _ = await self.tree.sync(guild=TEST_GUILD)
def update_to_default_user_config(self) -> None: def update_to_default_user_config(self) -> None:
""" """
Adds config keys to the config, if they don't exist yet. Adds config keys to the config, if they don't exist yet.
This writes the user config file. This writes the user config file.
""" """
user_config_sections = ["ENTRANCE.SOUND"]
for section in user_config_sections: for section, keys in USER_CONFIG_SCHEME.items():
if section not in self.user_config: if section not in self.user_config:
print(f"Adding section {section} to {CONFIGPATH}/{USERCONFIGNAME}") print(f"Adding section {section} to {CONFIGPATH}/{USERCONFIGNAME}")
self.user_config[section] = dict() self.user_config[section] = dict()
for key, default in keys:
if key not in self.user_config[section].keys():
print(f"Adding key {key} with default value {default} to section {section}")
self.user_config[section][key] = default
self.write_user_config() self.write_user_config()
def print_user_config(self) -> None: def print_user_config(self) -> None:
@ -119,9 +156,13 @@ class HeidiClient(discord.Client):
This function is set in on_voice_state_triggers and triggered by the on_voice_state_update event. This function is set in on_voice_state_triggers and triggered by the on_voice_state_update event.
""" """
disable_join_sound_if_alone: bool = self.user_config[ConfigSection.FLAGS.value][
FlagsConfigKey.DISABLE_JOIN_SOUND_IF_ALONE.value
] == str(True)
# Don't play anything when no other users are present # Don't play anything when no other users are present
if ( if (
member is not None disable_join_sound_if_alone
and member.voice is not None and member.voice is not None
and member.voice.channel is not None and member.voice.channel is not None
and len(member.voice.channel.members) <= 1 and len(member.voice.channel.members) <= 1
@ -129,9 +170,7 @@ class HeidiClient(discord.Client):
print("Not playing entrance sound, as no other members are present") print("Not playing entrance sound, as no other members are present")
return return
soundpath: Union[str, None] = self.user_config["ENTRANCE.SOUND"].get( soundpath: str | None = self.user_config["ENTRANCE.SOUND"].get(member.name, None)
member.name, None
)
if soundpath is None: if soundpath is None:
print(f"User {member.name} has not set an entrance sound") print(f"User {member.name} has not set an entrance sound")

19
heidi_config.py Normal file
View File

@ -0,0 +1,19 @@
from enum import Enum
class ConfigSection(Enum):
FLAGS = "FLAGS"
ENTRANCE_SOUND = "ENTRANCE.SOUND"
class FlagsConfigKey(Enum):
DISABLE_JOIN_SOUND_IF_ALONE = "disable_join_sound_if_alone"
# NOTE: This is the default configuration scheme
USER_CONFIG_SCHEME: dict[str, list[tuple[str, str]]] = {
ConfigSection.FLAGS.value: [
(FlagsConfigKey.DISABLE_JOIN_SOUND_IF_ALONE.value, str(True)),
],
ConfigSection.ENTRANCE_SOUND.value: [],
}

View File

@ -1,29 +1,28 @@
import os import os
import discord from discord.object import Object
from dotenv import load_dotenv from dotenv import load_dotenv
# This is run when this file is imported # This is run when this file is imported
load_dotenv() HAS_VARIABLES: bool = load_dotenv()
print("Debug: Importing heidi_constants.py") print("Debug: Importing heidi_constants.py")
# ================================================================================================ # # =========================================================================== #
# ================================================================================================ # # =========================================================================== #
# NOTE: Always set this correctly: DOCKER: bool = os.getenv("DOCKER") == str(True)
DOCKER = os.getenv("DOCKER") == "True" # =========================================================================== #
# ================================================================================================ # # =========================================================================== #
# ================================================================================================ #
# Constants # Constants
CONFIGPATH = "/config" if DOCKER else "." CONFIGPATH: str = "/config" if DOCKER else "."
USERCONFIGNAME = "Heidi_User.conf" USERCONFIGNAME: str = "Heidi_User.conf"
SOUNDDIR: str = "/sounds" if DOCKER else "./heidi-sounds" SOUNDDIR: str = "/sounds" if DOCKER else "./heidi-sounds"
# IDs of the servers Heidi is used on # IDs of the servers Heidi is used on
LINUS_GUILD = discord.Object(id=431154792308408340) LINUS_GUILD: Object = Object(id=431154792308408340)
TEST_GUILD = discord.Object(id=821511861178204161) TEST_GUILD: Object = Object(id=821511861178204161)
# Channel IDs # Channel IDs
HEIDI_SPAM_ID = 822223476101742682 HEIDI_SPAM_ID: int = 822223476101742682

View File

@ -1,11 +1,12 @@
import asyncio import asyncio
import functools import functools
from typing import Union from types import CoroutineType
from typing import Any, Callable
import discord from discord import Interaction, Message, VoiceChannel, Member, VoiceClient
from discord import Interaction, VoiceChannel, Member from discord.player import FFmpegPCMAudio
from heidi_constants import * from heidi_constants import SOUNDDIR
print("Debug: Importing heidi_helpers.py") print("Debug: Importing heidi_helpers.py")
@ -15,14 +16,15 @@ print("Debug: Importing heidi_helpers.py")
# 1. @enforce_channel(ID) is added to a function, which evaluates to decorate with the channel_id in its closure # 1. @enforce_channel(ID) is added to a function, which evaluates to decorate with the channel_id in its closure
# 2. The function is passed to decorate(function), # 2. The function is passed to decorate(function),
def enforce_channel(channel_id): def enforce_channel(channel_id: int):
""" """
Only run a function if called from the correct channel. Only run a function if called from the specified voice channel.
""" """
def decorate(function):
def decorate(function: Callable[..., CoroutineType[Any, Any, None]]):
@functools.wraps(function) @functools.wraps(function)
async def wrapped(*args, **kwargs): async def wrapped(*args: *tuple[Interaction, *tuple[Any, ...]], **kwargs: int):
""" """
Sends an interaction response if the interaction is not triggered from the heidi_spam channel. Sends an interaction response if the interaction is not triggered from the heidi_spam channel.
""" """
@ -30,7 +32,7 @@ def enforce_channel(channel_id):
# Do not call the decorated function if the channel_id doesn't match # Do not call the decorated function if the channel_id doesn't match
if not interaction.channel_id == channel_id: if not interaction.channel_id == channel_id:
await interaction.response.send_message("Heidi sagt: Geh in heidi_spam du dulli", ephemeral=True) _ = await interaction.response.send_message("Heidi sagt: Geh in heidi_spam du dulli", ephemeral=True)
return return
await function(*args, **kwargs) await function(*args, **kwargs)
@ -40,12 +42,34 @@ def enforce_channel(channel_id):
return decorate return decorate
# Reactions --------------------------------------------------------------------------------------
def create_author_based_message_predicate(names: list[str]) -> Callable[[Message], bool]:
"""
Create a predicate that determines if a message was written by a certain author.
For usage with on_message_triggers.
"""
def handler(message: Message) -> bool:
for name in names:
if (
isinstance(message.author, Member)
and message.author.nick is not None
and name.lower() in message.author.nick.lower()
):
return True
return False
return handler
# Sounds ----------------------------------------------------------------------------------------- # Sounds -----------------------------------------------------------------------------------------
# @todo Normalize volume when playing
async def play_voice_line( async def play_voice_line(
interaction: Union[Interaction, None], interaction: Interaction | None,
voice_channel: VoiceChannel, voice_channel: VoiceChannel,
board: str, board: str,
sound: str, sound: str,
@ -54,23 +78,22 @@ async def play_voice_line(
Play a voice line in the specified channel. Play a voice line in the specified channel.
""" """
try: try:
open(f"{SOUNDDIR}/{board}/{sound}") # Check if the file exists
_ = open(f"{SOUNDDIR}/{board}/{sound}")
except IOError: except IOError:
print(f"Error: Invalid soundfile {SOUNDDIR}/{board}/{sound}!") print(f"Error: Invalid soundfile {SOUNDDIR}/{board}/{sound}!")
if interaction is not None: if interaction is not None:
await interaction.response.send_message( _ = await interaction.response.send_message(
f'Heidi sagt: "{board}/{sound}" kanninich finden bruder', f'Heidi sagt: "{board}/{sound}" kanninich finden bruder', ephemeral=True
ephemeral=True
) )
return return
if interaction is not None: if interaction is not None:
await interaction.response.send_message(f'Heidi sagt: "{board}/{sound}"', ephemeral=True) _ = await interaction.response.send_message(f'Heidi sagt: "{board}/{sound}"', ephemeral=True)
audio_source = discord.FFmpegPCMAudio( # TODO: Normalize volume when playing
f"{SOUNDDIR}/{board}/{sound}" audio_source: FFmpegPCMAudio = FFmpegPCMAudio(f"{SOUNDDIR}/{board}/{sound}") # only works from docker
) # only works from docker voice_client: VoiceClient = await voice_channel.connect()
voice_client = await voice_channel.connect()
voice_client.play(audio_source) voice_client.play(audio_source)
while voice_client.is_playing(): while voice_client.is_playing():
@ -80,7 +103,7 @@ async def play_voice_line(
async def play_voice_line_for_member( async def play_voice_line_for_member(
interaction: Union[Interaction, None], interaction: Interaction | None,
member: Member, member: Member,
board: str, board: str,
sound: str, sound: str,
@ -97,7 +120,7 @@ async def play_voice_line_for_member(
): ):
print("User not in (valid) voice channel!") print("User not in (valid) voice channel!")
if interaction is not None: if interaction is not None:
await interaction.response.send_message("Heidi sagt: Komm in den Channel!", ephemeral=True) _ = await interaction.response.send_message("Heidi sagt: Komm in den Channel!", ephemeral=True)
return return
voice_channel: VoiceChannel = member.voice.channel voice_channel: VoiceChannel = member.voice.channel

2
pyproject.toml Normal file
View File

@ -0,0 +1,2 @@
[tool.black]
line-length = 120

2
setup.cfg Normal file
View File

@ -0,0 +1,2 @@
[flake8]
max-line-length = 120