7 Commits

Author SHA1 Message Date
c2847de7dd Add instantbuttons command + make responses ephemeral
All checks were successful
Build Heidi Docker image / build-docker (push) Successful in 14s
/instantbuttons displays a soundboard via a button ui
2023-12-09 19:51:28 +01:00
08230eb3de Enforce heidi_spam channel for commands
All checks were successful
Build Heidi Docker image / build-docker (push) Successful in 14s
2023-12-09 18:44:16 +01:00
f2ddb4ab66 Only play entrance sound when other is present + reformat
All checks were successful
Build Heidi Docker image / build-docker (push) Successful in 14s
2023-12-09 18:04:24 +01:00
876232f674 Ignore user config file 2023-12-09 18:03:42 +01:00
d7c3a7c740 Allow sounds with different file extensions
All checks were successful
Build Heidi Docker image / build-docker (push) Successful in 14s
Before only .mkv files could be played, as the extension was hardcoded
2023-12-09 17:55:21 +01:00
bdcd5208a7 Untrack Heidi_User.conf 2023-12-09 17:54:56 +01:00
79fcf0142a Some more options for randomly selected answers 2023-12-09 17:48:27 +01:00
6 changed files with 133 additions and 40 deletions

1
.gitignore vendored
View File

@ -12,3 +12,4 @@ Pipfile.lock
/disabled_voicelines/
*.svg
.vscode
Heidi_User.conf

View File

@ -1,2 +0,0 @@
[ENTRANCE.SOUND]

72
bot.py
View File

@ -120,8 +120,7 @@ def user_entrance_sound_autocomplete(
"""
boards: List[str] = os.listdir(SOUNDDIR)
all_sounds: Dict[str, List[str]] = {
board: list(map(lambda x: x.split(".")[0], os.listdir(f"{SOUNDDIR}/{board}/")))
for board in boards
board: os.listdir(f"{SOUNDDIR}/{board}/") for board in boards
} # These are all sounds, organized per board
# @todo Initially only suggest boards, because there are too many sounds to show them all
@ -133,7 +132,7 @@ def user_entrance_sound_autocomplete(
for sound in board_sounds: # Iterate over board specific sounds
soundpath = f"{board}/{sound}"
if soundpath.lower().startswith(current.lower()):
completions += [Choice(name=soundpath, value=soundpath)]
completions += [Choice(name=soundpath.split(".")[0], value=soundpath)]
return completions
@ -150,6 +149,7 @@ def user_entrance_sound_autocomplete(
config_value="Der Wert, auf welche die Option gesetzt werden soll."
)
@app_commands.autocomplete(config_value=user_config_value_autocomplete)
@enforce_channel(HEIDI_SPAM_ID)
async def user_config(
interaction: Interaction, config_key: str, config_value: str
) -> None:
@ -159,7 +159,7 @@ async def user_config(
# Only Members can set settings
if not isinstance(interaction.user, Member):
print("User not a member")
await interaction.response.send_message("Heidi sagt: Komm in die Gruppe!")
await interaction.response.send_message("Heidi sagt: Komm in die Gruppe!", ephemeral=True)
return
member: Member = interaction.user
@ -168,7 +168,8 @@ async def user_config(
client.write_user_config()
await interaction.response.send_message(
f"Ok, ich schreibe {member.name}={config_value} in mein fettes Gehirn!"
f"Ok, ich schreibe {member.name}={config_value} in mein fettes Gehirn!",
ephemeral=True
)
@ -176,6 +177,7 @@ async def user_config(
@client.tree.command(name="heidi", description="Heidi!")
@enforce_channel(HEIDI_SPAM_ID)
async def heidi_exclaim(interaction: Interaction) -> None:
"""
Print a random Heidi quote.
@ -188,6 +190,10 @@ async def heidi_exclaim(interaction: Interaction) -> None:
"Warum denn so schüchtern?",
"Im TV ist das legal!",
"Das Stroh ist nur fürs Shooting!",
"Jetzt sei doch mal sexy!",
"Stell dich nicht so an!",
"Models müssen da halt durch!",
"Heul doch nicht!",
]
await interaction.response.send_message(random.choice(messages))
@ -195,6 +201,7 @@ async def heidi_exclaim(interaction: Interaction) -> None:
@client.tree.command(name="miesmuschel", description="Was denkt Heidi?")
@app_commands.rename(question="frage")
@app_commands.describe(question="Heidi wird es beantworten!")
@enforce_channel(HEIDI_SPAM_ID)
async def magic_shell(interaction: Interaction, question: str) -> None:
"""
Answer a yes/no question.
@ -208,14 +215,13 @@ async def magic_shell(interaction: Interaction, question: str) -> None:
"Klaro Karo",
"Offensichtlich Sherlock",
"Tom sagt Ja",
"Nein!",
"Nö.",
"Nä.",
"Niemals!",
"Nur über meine Leiche du Hurensohn!",
"In deinen Träumen.",
"Tom sagt Nein"
"Tom sagt Nein",
]
question = question.strip()
question_mark = "" if question[-1] == "?" else "?"
@ -230,6 +236,7 @@ async def magic_shell(interaction: Interaction, question: str) -> None:
@app_commands.describe(option_a="Ist es vielleicht dies?")
@app_commands.rename(option_b="oder")
@app_commands.describe(option_b="Oder doch eher das?")
@enforce_channel(HEIDI_SPAM_ID)
async def choose(interaction: Interaction, option_a: str, option_b: str) -> None:
"""
Select an answer from two options.
@ -265,12 +272,10 @@ async def sound_autocomplete(
Suggest a sound from an already selected board.
"""
board: str = interaction.namespace.board
sounds: List[str] = list(
map(lambda x: x.split(".")[0], os.listdir(f"{SOUNDDIR}/{board}/"))
)
sounds: List[str] = os.listdir(f"{SOUNDDIR}/{board}/")
return [
Choice(name=sound, value=sound)
Choice(name=sound.split(".")[0], value=sound)
for sound in sounds
if sound.lower().startswith(current.lower())
]
@ -282,6 +287,7 @@ async def sound_autocomplete(
@app_commands.describe(sound="Was soll Heidi sagen?")
@app_commands.autocomplete(board=board_autocomplete)
@app_commands.autocomplete(sound=sound_autocomplete)
@enforce_channel(HEIDI_SPAM_ID)
async def say_voiceline(interaction: Interaction, board: str, sound: str) -> None:
"""
Play a voiceline in the calling member's current voice channel.
@ -289,7 +295,7 @@ async def say_voiceline(interaction: Interaction, board: str, sound: str) -> Non
# Only Members can access voice channels
if not isinstance(interaction.user, Member):
print("User not a member")
await interaction.response.send_message("Heidi sagt: Komm in die Gruppe!")
await interaction.response.send_message("Heidi sagt: Komm in die Gruppe!", ephemeral=True)
return
member: Member = interaction.user
@ -297,6 +303,43 @@ async def say_voiceline(interaction: Interaction, board: str, sound: str) -> Non
await play_voice_line_for_member(interaction, member, board, sound)
class InstantButton(discord.ui.Button):
def __init__(self, label: str, board: str, sound: str):
super().__init__(style=discord.ButtonStyle.red, label=label)
self.board = board
self.sound = sound
async def callback(self, interaction: Interaction):
"""
Handle a press of the button.
"""
if not isinstance(interaction.user, Member):
await interaction.response.send_message("Heidi mag keine discord.User, nur discord.Member!", ephemeral=True)
return
await play_voice_line_for_member(interaction, interaction.user, self.board, self.sound)
class InstantButtons(discord.ui.View):
def __init__(self, board: str, timeout=None):
super().__init__(timeout=timeout)
sounds = os.listdir(f"{SOUNDDIR}/{board}")
for sound in sounds:
self.add_item(InstantButton(sound.split(".")[0], board, sound))
@client.tree.command(
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.autocomplete(board=board_autocomplete)
@enforce_channel(HEIDI_SPAM_ID)
async def soundboard_buttons(interaction: Interaction, board: str) -> None:
await interaction.response.send_message(f"Soundboard: {board.capitalize()}", view=InstantButtons(board))
# Contextmenu ------------------------------------------------------------------------------------
@ -313,7 +356,7 @@ async def insult(
if not member.dm_channel:
print("Error creating DMChannel!")
await interaction.response.send_message("Heidi sagt: Gib mal DM Nummer süße*r!")
await interaction.response.send_message("Heidi sagt: Gib mal DM Nummer süße*r!", ephemeral=True)
return
insults = [
@ -334,7 +377,8 @@ async def insult(
await member.dm_channel.send(random.choice(insults))
await interaction.response.send_message(
"Anzeige ist raus!"
"Anzeige ist raus!",
ephemeral=True
) # with ephemeral = True only the caller can see the answer

View File

@ -115,9 +115,20 @@ class HeidiClient(discord.Client):
after: VoiceState,
) -> None:
"""
Play a sound when a member joins a voice channel.
Play a sound when a member joins a voice channel (and another member is present).
This function is set in on_voice_state_triggers and triggered by the on_voice_state_update event.
"""
# Don't play anything when no other users are present
if (
member is not None
and member.voice is not None
and member.voice.channel is not None
and len(member.voice.channel.members) <= 1
):
print("Not playing entrance sound, as no other members are present")
return
soundpath: Union[str, None] = self.user_config["ENTRANCE.SOUND"].get(
member.name, None
)

View File

@ -24,3 +24,6 @@ SOUNDDIR: str = "/sounds" if DOCKER else "./heidi-sounds"
# IDs of the servers Heidi is used on
LINUS_GUILD = discord.Object(id=431154792308408340)
TEST_GUILD = discord.Object(id=821511861178204161)
# Channel IDs
HEIDI_SPAM_ID = 822223476101742682

View File

@ -1,4 +1,5 @@
import asyncio
import functools
from typing import Union
import discord
@ -8,6 +9,40 @@ from heidi_constants import *
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):
"""
Only run a function if called from the correct channel.
"""
def decorate(function):
@functools.wraps(function)
async def wrapped(*args, **kwargs):
"""
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
# Sounds -----------------------------------------------------------------------------------------
# @todo Normalize volume when playing
async def play_voice_line(
interaction: Union[Interaction, None],
@ -19,20 +54,21 @@ async def play_voice_line(
Play a voice line in the specified channel.
"""
try:
open(f"{SOUNDDIR}/{board}/{sound}.mkv")
open(f"{SOUNDDIR}/{board}/{sound}")
except IOError:
print("Error: Invalid soundfile!")
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'
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}"')
await interaction.response.send_message(f'Heidi sagt: "{board}/{sound}"', ephemeral=True)
audio_source = discord.FFmpegPCMAudio(
f"{SOUNDDIR}/{board}/{sound}.mkv"
f"{SOUNDDIR}/{board}/{sound}"
) # only works from docker
voice_client = await voice_channel.connect()
voice_client.play(audio_source)
@ -61,7 +97,7 @@ async def play_voice_line_for_member(
):
print("User not in (valid) voice channel!")
if interaction is not None:
await interaction.response.send_message("Heidi sagt: Komm in den Channel!")
await interaction.response.send_message("Heidi sagt: Komm in den Channel!", ephemeral=True)
return
voice_channel: VoiceChannel = member.voice.channel