4 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
5 changed files with 124 additions and 30 deletions

1
.gitignore vendored
View File

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

61
bot.py
View File

@ -120,9 +120,8 @@ def user_entrance_sound_autocomplete(
""" """
boards: List[str] = os.listdir(SOUNDDIR) boards: List[str] = os.listdir(SOUNDDIR)
all_sounds: Dict[str, List[str]] = { all_sounds: Dict[str, List[str]] = {
board: os.listdir(f"{SOUNDDIR}/{board}/") board: os.listdir(f"{SOUNDDIR}/{board}/") for board in boards
for board in boards } # These are all sounds, organized per board
} # These are all sounds, organized per board, without file extension
# @todo Initially only suggest boards, because there are too many sounds to show them all # @todo Initially only suggest boards, because there are too many sounds to show them all
completions: List[Choice[str]] = [] completions: List[Choice[str]] = []
@ -150,6 +149,7 @@ def user_entrance_sound_autocomplete(
config_value="Der Wert, auf welche die Option gesetzt werden soll." config_value="Der Wert, auf welche die Option gesetzt werden soll."
) )
@app_commands.autocomplete(config_value=user_config_value_autocomplete) @app_commands.autocomplete(config_value=user_config_value_autocomplete)
@enforce_channel(HEIDI_SPAM_ID)
async def user_config( async def user_config(
interaction: Interaction, config_key: str, config_value: str interaction: Interaction, config_key: str, config_value: str
) -> None: ) -> None:
@ -159,7 +159,7 @@ async def user_config(
# 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("Heidi sagt: Komm in die Gruppe!") await interaction.response.send_message("Heidi sagt: Komm in die Gruppe!", ephemeral=True)
return return
member: Member = interaction.user member: Member = interaction.user
@ -168,7 +168,8 @@ async def user_config(
client.write_user_config() client.write_user_config()
await interaction.response.send_message( 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!") @client.tree.command(name="heidi", description="Heidi!")
@enforce_channel(HEIDI_SPAM_ID)
async def heidi_exclaim(interaction: Interaction) -> None: async def heidi_exclaim(interaction: Interaction) -> None:
""" """
Print a random Heidi quote. Print a random Heidi quote.
@ -191,7 +193,7 @@ async def heidi_exclaim(interaction: Interaction) -> None:
"Jetzt sei doch mal sexy!", "Jetzt sei doch mal sexy!",
"Stell dich nicht so an!", "Stell dich nicht so an!",
"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))
@ -199,6 +201,7 @@ async def heidi_exclaim(interaction: Interaction) -> None:
@client.tree.command(name="miesmuschel", description="Was denkt Heidi?") @client.tree.command(name="miesmuschel", description="Was denkt Heidi?")
@app_commands.rename(question="frage") @app_commands.rename(question="frage")
@app_commands.describe(question="Heidi wird es beantworten!") @app_commands.describe(question="Heidi wird es beantworten!")
@enforce_channel(HEIDI_SPAM_ID)
async def magic_shell(interaction: Interaction, question: str) -> None: async def magic_shell(interaction: Interaction, question: str) -> None:
""" """
Answer a yes/no question. Answer a yes/no question.
@ -233,6 +236,7 @@ async def magic_shell(interaction: Interaction, question: str) -> None:
@app_commands.describe(option_a="Ist es vielleicht dies?") @app_commands.describe(option_a="Ist es vielleicht dies?")
@app_commands.rename(option_b="oder") @app_commands.rename(option_b="oder")
@app_commands.describe(option_b="Oder doch eher das?") @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: async def choose(interaction: Interaction, option_a: str, option_b: str) -> None:
""" """
Select an answer from two options. Select an answer from two options.
@ -283,6 +287,7 @@ async def sound_autocomplete(
@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)
@enforce_channel(HEIDI_SPAM_ID)
async def say_voiceline(interaction: Interaction, board: str, sound: str) -> None: async def say_voiceline(interaction: Interaction, board: str, sound: str) -> None:
""" """
Play a voiceline in the calling member's current voice channel. Play a voiceline in the calling member's current voice channel.
@ -290,7 +295,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("Heidi sagt: Komm in die Gruppe!") await interaction.response.send_message("Heidi sagt: Komm in die Gruppe!", ephemeral=True)
return return
member: Member = interaction.user member: Member = interaction.user
@ -298,6 +303,43 @@ 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):
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 ------------------------------------------------------------------------------------ # Contextmenu ------------------------------------------------------------------------------------
@ -314,7 +356,7 @@ async def insult(
if not member.dm_channel: if not member.dm_channel:
print("Error creating DMChannel!") 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 return
insults = [ insults = [
@ -335,7 +377,8 @@ async def insult(
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!" "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

View File

@ -28,7 +28,7 @@ class HeidiClient(discord.Client):
# 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, lambda m: "jeremy" in m.author.nick.lower(): self._autoreact_to_jeremy,
lambda m: "kardashian" in m.author.nick.lower() lambda m: "kardashian" in m.author.nick.lower()
or "jenner" in m.author.nick.lower(): 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
@ -109,15 +109,26 @@ class HeidiClient(discord.Client):
await message.add_reaction("💄") await message.add_reaction("💄")
async def _play_entrance_sound( async def _play_entrance_sound(
self, self,
member: Member, member: Member,
before: VoiceState, before: VoiceState,
after: VoiceState, after: VoiceState,
) -> None: ) -> 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. 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( soundpath: Union[str, None] = self.user_config["ENTRANCE.SOUND"].get(
member.name, None 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 # IDs of the servers Heidi is used on
LINUS_GUILD = discord.Object(id=431154792308408340) LINUS_GUILD = discord.Object(id=431154792308408340)
TEST_GUILD = discord.Object(id=821511861178204161) TEST_GUILD = discord.Object(id=821511861178204161)
# Channel IDs
HEIDI_SPAM_ID = 822223476101742682

View File

@ -1,4 +1,5 @@
import asyncio import asyncio
import functools
from typing import Union from typing import Union
import discord import discord
@ -8,12 +9,46 @@ from heidi_constants import *
print("Debug: Importing heidi_helpers.py") 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 # @todo Normalize volume when playing
async def play_voice_line( async def play_voice_line(
interaction: Union[Interaction, None], interaction: Union[Interaction, None],
voice_channel: VoiceChannel, voice_channel: VoiceChannel,
board: str, board: str,
sound: str, sound: str,
) -> None: ) -> None:
""" """
Play a voice line in the specified channel. Play a voice line in the specified channel.
@ -24,12 +59,13 @@ async def play_voice_line(
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
) )
return return
if interaction is not None: 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( audio_source = discord.FFmpegPCMAudio(
f"{SOUNDDIR}/{board}/{sound}" f"{SOUNDDIR}/{board}/{sound}"
@ -44,24 +80,24 @@ async def play_voice_line(
async def play_voice_line_for_member( async def play_voice_line_for_member(
interaction: Union[Interaction, None], interaction: Union[Interaction, None],
member: Member, member: Member,
board: str, board: str,
sound: str, sound: str,
) -> None: ) -> None:
""" """
Play a voice line in the member's current channel. 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) # Member needs to be in voice channel to hear audio (Heidi needs to know the channel to join)
if ( if (
member is None member is None
or member.voice is None or member.voice is None
or member.voice.channel is None or member.voice.channel is None
or not isinstance(member.voice.channel, VoiceChannel) or not isinstance(member.voice.channel, VoiceChannel)
): ):
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!") 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