Files
discord-heidi/heidi_helpers.py
Christoph Urlacher 6659079d7a
Some checks failed
Build Heidi Docker image / build-docker (push) Failing after 28s
Clean up type hints everywhere, overhaul bot configuration
2025-09-23 20:39:11 +02:00

129 lines
4.0 KiB
Python

import asyncio
import functools
from types import CoroutineType
from typing import Any, Callable
from discord import Interaction, Message, VoiceChannel, Member, VoiceClient
from discord.player import FFmpegPCMAudio
from heidi_constants import SOUNDDIR
print("Debug: Importing heidi_helpers.py")
# Checks -----------------------------------------------------------------------------------------
# 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),
def enforce_channel(channel_id: int):
"""
Only run a function if called from the specified voice channel.
"""
def decorate(function: Callable[..., CoroutineType[Any, Any, None]]):
@functools.wraps(function)
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.
"""
interaction: Interaction = args[0]
# Do not call the decorated function if the channel_id doesn't match
if not interaction.channel_id == channel_id:
_ = await interaction.response.send_message("Heidi sagt: Geh in heidi_spam du dulli", ephemeral=True)
return
await function(*args, **kwargs)
return wrapped
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 -----------------------------------------------------------------------------------------
async def play_voice_line(
interaction: Interaction | None,
voice_channel: VoiceChannel,
board: str,
sound: str,
) -> None:
"""
Play a voice line in the specified channel.
"""
try:
# Check if the file exists
_ = open(f"{SOUNDDIR}/{board}/{sound}")
except IOError:
print(f"Error: Invalid soundfile {SOUNDDIR}/{board}/{sound}!")
if interaction is not None:
_ = await interaction.response.send_message(
f'Heidi sagt: "{board}/{sound}" kanninich finden bruder', ephemeral=True
)
return
if interaction is not None:
_ = await interaction.response.send_message(f'Heidi sagt: "{board}/{sound}"', ephemeral=True)
# TODO: Normalize volume when playing
audio_source: FFmpegPCMAudio = FFmpegPCMAudio(f"{SOUNDDIR}/{board}/{sound}") # only works from docker
voice_client: VoiceClient = await voice_channel.connect()
voice_client.play(audio_source)
while voice_client.is_playing():
await asyncio.sleep(1)
await voice_client.disconnect()
async def play_voice_line_for_member(
interaction: Interaction | None,
member: Member,
board: str,
sound: str,
) -> None:
"""
Play a voice line in the member's current channel.
"""
# Member needs to be in voice channel to hear audio (Heidi needs to know the channel to join)
if (
member is None
or member.voice is None
or member.voice.channel is None
or not isinstance(member.voice.channel, VoiceChannel)
):
print("User not in (valid) voice channel!")
if interaction is not None:
_ = await interaction.response.send_message("Heidi sagt: Komm in den Channel!", ephemeral=True)
return
voice_channel: VoiceChannel = member.voice.channel
await play_voice_line(interaction, voice_channel, board, sound)