46 Commits

Author SHA1 Message Date
75fb627361 Clean up type hints everywhere, overhaul bot configuration
All checks were successful
Build Heidi Docker image / build-docker (push) Successful in 2m12s
2025-09-23 20:43:42 +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
d04244221b Replace ENTRANCE.SOUND menu with dropdowns
All checks were successful
Build Heidi Docker image / build-docker (push) Successful in 14s
2023-12-09 23:01:24 +01:00
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
0f6cc12182 Delete orphaned code
All checks were successful
Build Heidi Docker image / build-docker (push) Successful in 27s
2023-12-09 17:36:53 +01:00
9b66061ee7 Reformat TODO comments
All checks were successful
Build Heidi Docker image / build-docker (push) Successful in 15s
2023-12-09 17:35:04 +01:00
c6608e4695 Remove rocm stuff from flake 2023-12-09 17:34:54 +01:00
3335009692 Fix SOUNDDIR being in the wrong file
Some checks failed
Build Heidi Docker image / build-docker (push) Failing after 17s
2023-12-09 17:28:09 +01:00
d7604b6604 Update flake.lock 2023-12-09 17:20:46 +01:00
2e493e404b Split Heidi into multiple parts
All checks were successful
Build Heidi Docker image / build-docker (push) Successful in 28s
2023-12-09 17:14:05 +01:00
16822e0212 Remove commented out code + add docstrings 2023-12-09 16:31:00 +01:00
9d78352ea5 Update handling of "None" 2023-12-09 15:48:19 +01:00
13b3e9910a Add sounds 2023-12-09 15:28:26 +01:00
1b89d2ef3b Update flake.lock 2023-12-09 15:28:20 +01:00
6debffbd77 Add Suiii sound 2023-11-27 20:50:58 +01:00
e08c1c0204 Add joko sounds 2023-11-26 12:18:13 +01:00
82f0387675 Add yakari sound 2023-11-26 00:59:04 +01:00
b6fbed1c49 Update workflow registry path
All checks were successful
Build Heidi Docker image / build-docker (push) Successful in 8s
2023-11-26 00:41:53 +01:00
e430b99e35 Update workflow registry path
Some checks failed
Build Heidi Docker image / build-docker (push) Failing after 9s
2023-11-26 00:41:17 +01:00
626611f5a8 Update dockerfile for unbuffered print
All checks were successful
Build Heidi Docker image / build-docker (push) Successful in 7s
2023-11-26 00:39:46 +01:00
56b625b5b8 Set DOCKER using env
All checks were successful
Build Heidi Docker image / build-docker (push) Successful in 8s
2023-11-26 00:36:25 +01:00
e3e42dc235 Add kaptn sounds 2023-11-26 00:33:20 +01:00
1c8a6c3945 Add /userconfig command response
All checks were successful
Build Heidi Docker image / build-docker (push) Successful in 9s
2023-11-26 00:16:07 +01:00
e94fbdd716 Implement initial entrance sounds
All checks were successful
Build Heidi Docker image / build-docker (push) Successful in 8s
2023-11-26 00:08:26 +01:00
361de0f5eb Delete Heidi.conf 2023-11-26 00:08:18 +01:00
d8c1ca48a9 Add mp3 to LFS 2023-11-25 21:40:50 +01:00
0fa5807442 Add default Heidi.conf
All checks were successful
Build Heidi Docker image / build-docker (push) Successful in 8s
2023-11-25 21:20:01 +01:00
222b5b99f3 Add ENTRANCE.SOUND config section + config file writing 2023-11-25 21:19:53 +01:00
a34e4fc01b Only compare lower case strings for autocomplete + userconfig command
All checks were successful
Build Heidi Docker image / build-docker (push) Successful in 8s
2023-11-25 20:21:42 +01:00
7250d7bb7d Autoreact to kardashian/jenner
All checks were successful
Build Heidi Docker image / build-docker (push) Successful in 7s
2023-11-25 20:12:36 +01:00
48e47f081a Cleanup + start persistant config features
All checks were successful
Build Heidi Docker image / build-docker (push) Successful in 9s
2023-11-25 20:08:32 +01:00
a0e184f2fc Take current input into account for autocomplete + reformat
All checks were successful
Build Heidi Docker image / build-docker (push) Successful in 17s
2023-11-25 19:37:02 +01:00
273b3ff406 Update flake 2023-11-25 19:36:33 +01:00
29d4dff673 Update .gitignore 2023-11-25 19:36:28 +01:00
51f560f686 Update pushvoice script 2023-11-25 19:17:19 +01:00
9836ae72c4 Add sounds 2023-11-25 19:13:30 +01:00
b66c10bacb Setup LFS 2023-11-25 19:10:46 +01:00
159 changed files with 1243 additions and 846 deletions

2
.gitattributes vendored Normal file
View File

@ -0,0 +1,2 @@
*.mkv filter=lfs diff=lfs merge=lfs -text
*.mp3 filter=lfs diff=lfs merge=lfs -text

View File

@ -4,6 +4,7 @@ on:
push:
branches: [master]
paths:
- ".gitea/workflows/*"
- "Dockerfile"
- "*.py"
- "requirements.txt"
@ -17,10 +18,10 @@ jobs:
- name: Login to container registry
uses: docker/login-action@v3
with:
registry: gitea.local.chriphost.de
registry: gitea.vps.chriphost.de
username: ${{ secrets.CONTAINER_REGISTRY_USER }}
password: ${{ secrets.CONTAINER_REGISTRY_TOKEN }}
- name: Build Docker image
run: docker build . --file Dockerfile --tag gitea.local.chriphost.de/christoph/discord-heidi:latest
run: docker build . --file Dockerfile --tag gitea.vps.chriphost.de/christoph/discord-heidi:latest
- name: Push Docker image
run: docker push gitea.local.chriphost.de/christoph/discord-heidi:latest
run: docker push gitea.vps.chriphost.de/christoph/discord-heidi:latest

2
.gitignore vendored
View File

@ -11,3 +11,5 @@ Pipfile.lock
/discord.log
/disabled_voicelines/
*.svg
.vscode
Heidi_User.conf

View File

@ -1,38 +0,0 @@
workflow: # for entire pipeline
rules:
- if: '$CI_COMMIT_REF_NAME == "master"' # only run on master...
changes: # ...and when these files have changed
- "*.py"
- "Dockerfile"
docker-build:
stage: build
image: docker:20 # provides the docker toolset (but without an active daemon)
services: # configure images that run during jobs linked to the image (above)
- docker:dind # dind build on docker and starts up the dockerdaemon (docker itself doesn't do that), which is needed to call docker build etc.
before_script:
- docker login -u $CI_REGISTRY_USER -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY
script:
- docker pull $CI_REGISTRY_IMAGE:latest || true # latest image for cache (not failing if image is not found)
- >
docker build
--pull
--cache-from $CI_REGISTRY_IMAGE:latest
--label "org.opencontainers.image.title=$CI_PROJECT_TITLE"
--label "org.opencontainers.image.url=$CI_PROJECT_URL"
--label "org.opencontainers.image.created=$CI_JOB_STARTED_AT"
--label "org.opencontainers.image.revision=$CI_COMMIT_SHA"
--label "org.opencontainers.image.version=$CI_COMMIT_REF_NAME"
--tag $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
.
- docker tag $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA $CI_REGISTRY_IMAGE:latest
- docker push $CI_REGISTRY_IMAGE:latest
docker-deploy:
stage: deploy
image: alpine:3.15
needs: ["docker-build"]
script:
- chmod og= $ID_RSA
- apk update && apk add openssh-client
- ssh -i $ID_RSA -o StrictHostKeyChecking=no $SERVER_USER@$SERVER_IP "/home/christoph/$CI_PROJECT_TITLE/launch.sh"

View File

@ -1,10 +1,10 @@
# syntax=docker/dockerfile:1
FROM python:3.13.7-slim-trixie
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
COPY requirements.txt requirements.txt
RUN pip3 install -r requirements.txt
COPY . .
CMD ["python3", "bot.py"]
RUN pip3 install -r requirements.txt
CMD ["python3", "-u", "bot.py"]

611
bot.py
View File

@ -1,180 +1,300 @@
# Example: https://github.com/Rapptz/discord.py/blob/master/examples/app_commands/basic.py
import os, re, random, logging, asyncio, discord
from discord import app_commands
import os
import random
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 functools import reduce
from dotenv import load_dotenv
from typing import Optional, Union
# TODO: Reenable + extend textgen
# from textgen import textgen
# from textgen_markov import MarkovTextGenerator
# from textgen_lstm import LSTMTextGenerator
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
# TODO: Reenable + extend scraper
# from models import Models
from heidi_client import HeidiClient
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
# We're fancy today
from rich.traceback import install
install(show_locals=True)
# ================================================================================================ #
# ================================================================================================ #
# NOTE: Always set this correctly: #
DOCKER = True #
# ================================================================================================ #
# ================================================================================================ #
# DONE: Migrate back to discord.py
# DONE: Rewrite bot with slash commands (and making actual use of discord.py)
# TODO: Insult statistics (you have insulted 20 times)
# TODO: Only post in heidi-spam channel
# TODO: yt-dlp music support
# TODO: Somehow upload voicelines more easily (from discord voice message?), also need to be distributed to folders so no more than 25 lines per folder
# TODO: Reenable text/quote generation, allow uploading of training text files, allow switching "personalities" (/elon generates elon quote?)
# TODO: Zalgo generator
# IDs of the servers Heidi is used on
LINUS_GUILD = discord.Object(id=431154792308408340)
TEST_GUILD = discord.Object(id=821511861178204161)
class HeidiClient(discord.Client):
def __init__(self, *, intents: discord.Intents):
super().__init__(status="Nur eine kann GNTM werden!", intents=intents)
# Separate object that keeps all application command state
self.tree = app_commands.CommandTree(self)
# self.models = Models() # scraped model list
# automatic actions on all messages
# auto_triggers is a map with tuples of two functions: (predicate, action)
# if the predicate is true the action is performed
self.auto_triggers = {
# 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
}
# Textgen
self.textgen_models: dict[str, textgen] = {
# The name must correspond to the name of the training text file
# "kommunistisches_manifest": LSTMTextGenerator(10),
# "musk": LSTMTextGenerator(10),
# "bibel": LSTMTextGenerator(10)
# "bibel": MarkovTextGenerator(3), # Prefix length of 3
# "kommunistisches_manifest": MarkovTextGenerator(3),
# "musk": MarkovTextGenerator(3)
}
for name, model in self.textgen_models.items():
model.init(name) # Loads the textfile
if os.path.exists(f"weights/{name}_lstm_model.pt"):
model.load()
elif not DOCKER:
model.train()
else:
print("Error: Can't load model", name)
print("Generating test sentence for", name)
self.textgen_models[name].generate_sentence()
# Synchronize commands to guilds
async def setup_hook(self):
self.tree.copy_global_to(guild=LINUS_GUILD)
await self.tree.sync(guild=LINUS_GUILD)
self.tree.copy_global_to(guild=TEST_GUILD)
await self.tree.sync(guild=TEST_GUILD)
# Commands -----------------------------------------------------------------------------------
# async def list_models_in(self, message):
# """
# wer ist dabei?
# """
# await message.channel.send("\n".join(self.models.get_in_names()))
# async def list_models_out(self, message):
# """
# wer ist raus? (Liste der Keks welche ge*ickt wurden)
# """
# await message.channel.send("\n".join(self.models.get_out_names()))
# async def show_model_picture(self, message):
# """
# gib Bild von <Name>
# """
# name = message.content.split()[-1]
# picture = discord.Embed()
# picture.set_image(url=self.models.get_image(name))
# picture.set_footer(text=name)
# await message.channel.send(embed=picture)
# Automatic Actions --------------------------------------------------------------------------
# @staticmethod
# async def autoreact_to_girls(message):
# """
# ❤ aktives Model
# """
# await message.add_reaction("❤")
@staticmethod
async def _autoreact_to_jeremy(message):
"""
🧀 Jeremy
"""
await message.add_reaction("🧀")
# ------------------------------------------------------------------------------------------------
# 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 = discord.Intents.default()
intents.members = True # Allow to react to member join/leave etc
intents.message_content = True # Allow to read message content from arbitrary messages
intents: Intents = Intents.default()
intents.members = True # Allow to react to member join/leave etc
intents.message_content = True # Allow to read message content from arbitrary messages
intents.voice_states = True # Allow to process on_voice_state_update
# Setup our client
# Set up our client
client = HeidiClient(intents=intents)
# Events -----------------------------------------------------------------------------------------
# NOTE: I defined the events outside of the Client class, don't know if I like it or not...
# NOTE: I defined the events outside the Client class, don't know if I like it or not...
@client.event
async def on_ready():
print(f"{client.user} (id: {client.user.id}) has connected to Discord!")
async def on_ready() -> None:
"""
This event triggers when the Heidi client has finished connecting.
"""
if client.user is not None:
print(f"{client.user} (id: {client.user.id}) has connected to Discord!")
else:
print("client.user is None!")
@client.event
async def on_message(message):
async def on_message(message: Message) -> None:
"""
This event triggers when a message is sent in any text channel.
"""
# Skip Heidis own messages
if message.author == client.user:
return
# Automatic actions for all messages
# python iterates over the keys of a map
for predicate in client.auto_triggers:
for predicate in client.on_message_triggers:
if predicate(message):
action = client.auto_triggers[predicate]
action = client.on_message_triggers[predicate]
print(f"on_message: calling {action.__name__}")
await action(message)
break
@client.event
async def on_voice_state_update(member: Member, before: VoiceState, after: VoiceState) -> None:
"""
This event triggers when a member joins/changes/leaves a voice channel or mutes/unmutes.
"""
# Skip Heidis own voice state updates (e.g. on /say)
if member._user == client.user:
return
# Automatic acions for all voice state changes
# python iterates over the keys of a map
for predicate in client.on_voice_state_triggers:
if predicate(member, before, after):
action: Callable[[Member, VoiceState, VoiceState], CoroutineType[Any, Any, None]] = (
client.on_voice_state_triggers[predicate]
)
print(f"on_voice_state_update: calling {action.__name__}")
await action(member, before, after)
# Config Commands --------------------------------------------------------------------------------
class FlagValueSelect(ui.Select[ui.View]):
def __init__(
self, flag: str, on_flag_select_callback: Callable[[Interaction, str, str], CoroutineType[Any, Any, None]]
) -> None:
self.flag: str = flag
self.on_flag_select_callback: Callable[[Interaction, str, str], CoroutineType[Any, Any, None]] = (
on_flag_select_callback
)
options: list[SelectOption] = [SelectOption(label=val, value=val) for val in ["True", "False"]]
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 FlagValueView(ui.View):
def __init__(
self, flag: str, on_flag_select_callback: Callable[[Interaction, str, str], CoroutineType[Any, Any, None]]
) -> None:
super().__init__(timeout=600)
_ = self.add_item(FlagValueSelect(flag, on_flag_select_callback))
class FlagsSelect(ui.Select[ui.View]):
def __init__(
self, on_flag_select_callback: Callable[[Interaction, str, str], CoroutineType[Any, Any, None]]
) -> None:
self.on_flag_select_callback: Callable[[Interaction, str, str], CoroutineType[Any, Any, None]] = (
on_flag_select_callback
)
options: list[SelectOption] = [SelectOption(label=flag.value, value=flag.value) for flag in FlagsConfigKey]
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),
ephemeral=True,
)
class EntranceSoundBoardView(ui.View):
def __init__(
self, on_sound_select_callback: Callable[[Interaction, str, str], CoroutineType[Any, Any, None]]
) -> None:
super().__init__(timeout=600)
_ = self.add_item(EntranceSoundBoardSelect(on_sound_select_callback))
class EntranceSoundSoundSelect(ui.Select[ui.View]):
def __init__(
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).
"""
return [
Choice(name=key, value=key) for key in client.user_config.sections() if key.lower().startswith(current.lower())
]
@client.tree.command(
name="heidiconfig",
description="User-spezifische Heidi-Einstellungen (Heidi merkt sie sich in ihrem riesigen Gehirn).",
)
@app_commands.rename(config_key="option")
@app_commands.describe(config_key="Die Option, welche du ändern willst.")
@app_commands.autocomplete(config_key=user_config_key_autocomplete)
@enforce_channel(HEIDI_SPAM_ID)
async def user_config(interaction: Interaction[Client], config_key: str) -> None:
"""
Set a user config value for the calling user.
"""
# 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!", ephemeral=True)
return
member: Member = interaction.user
async def on_flag_select_callback(interaction: Interaction, flag: str, value: str) -> None:
"""
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.write_user_config()
_ = await interaction.response.send_message(
f"Ok, ich schreibe {member.name}={board}/{sound} in mein fettes Gehirn!",
ephemeral=True,
)
# Views for different user config options are defined here
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, description = views[config_key]
_ = await interaction.response.send_message(
description,
view=view(select_callback), # pyright: ignore[reportCallIssue]
ephemeral=True,
)
# Commands ---------------------------------------------------------------------------------------
@client.tree.command(name = "giblinkbruder", description = "Heidi hilft mit dem Link zu deiner Lieblingsshow im Qualitätsfernsehen.")
async def show_link(interaction: discord.Interaction):
link_pro7 = "https://www.prosieben.de/tv/germanys-next-topmodel/livestream"
link_joyn = "https://www.joyn.de/serien/germanys-next-topmodel"
await interaction.response.send_message(f"ProSieben: {link_pro7}\nJoyn: {link_joyn}")
@client.tree.command(name="heidi", description="Heidi!")
async def heidi_exclaim(interaction: discord.Interaction):
@enforce_channel(HEIDI_SPAM_ID)
async def heidi_exclaim(interaction: Interaction) -> None:
"""
Print a random Heidi quote.
"""
messages = [
"Die sind doch fast 18!",
"Heidi!",
@ -182,126 +302,163 @@ async def heidi_exclaim(interaction: discord.Interaction):
"Dann zieh dich mal aus!",
"Warum denn so schüchtern?",
"Im TV ist das legal!",
"Das Stroh ist nur fürs Shooting!"
"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))
_ = await interaction.response.send_message(random.choice(messages))
@client.tree.command(name="miesmuschel", description="Was denkt Heidi?")
@app_commands.rename(question = "frage")
@app_commands.describe(question = "Heidi wird es beantworten!")
async def magic_shell(interaction: discord.Interaction, question: str):
@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.
"""
# Should be equal amounts of yes/no answers, to have a 50/50 chance.
choices = [
"Ja!",
"Jo.",
"Jo",
"Total!",
"Natürlich.",
"Natürlich",
"Klaro Karo",
"Offensichtlich Sherlock",
"Tom sagt Ja",
"Nein!",
"Nö.",
"Nä.",
"Niemals!",
"Nur über meine Leiche du Hurensohn!",
"In deinen Träumen.",
"Tom sagt Nein",
]
question = question.strip()
question_mark = "" if question[-1] == "?" else "?"
await interaction.response.send_message(f"{question}{question_mark}\nHeidi sagt: {random.choice(choices)}")
_ = await interaction.response.send_message(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!")
@app_commands.rename(option_a = "entweder")
@app_commands.describe(option_a = "Ist es vielleicht dies?")
@app_commands.rename(option_b = "oder")
@app_commands.describe(option_b = "Oder doch eher das?")
async def choose(interaction: discord.Interaction, option_a: str, option_b: str):
@app_commands.rename(option_a="entweder")
@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.
"""
options = [option_a.strip(), option_b.strip()]
await interaction.response.send_message(f"{options[0]} oder {options[1]}?\nHeidi sagt: {random.choice(options)}")
_ = await interaction.response.send_message(
f"{options[0]} oder {options[1]}?\nHeidi sagt: {random.choice(options)}"
)
# async def quote_model_autocomplete(interaction: discord.Interaction, current: str) -> list[Choice[str]]:
# models = client.textgen_models.keys()
# return [Choice(name=model, value=model) for model in models]
# @client.tree.command(name="zitat", description="Heidi zitiert!")
# @app_commands.rename(quote_model = "style")
# @app_commands.describe(quote_model = "Woraus soll Heidi zitieren?")
# @app_commands.autocomplete(quote_model = quote_model_autocomplete)
# async def quote(interaction: discord.Interaction, quote_model: str):
# generated_quote = client.textgen_models[quote_model].generate_sentence()
# joined_quote = " ".join(generated_quote)
# await interaction.response.send_message(f"Heidi zitiert: \"{joined_quote}\"")
# Sounds -----------------------------------------------------------------------------------------
# @client.tree.command(name="vervollständige", description="Heidi beendet den Satz!")
# @app_commands.rename(prompt = "satzanfang")
# @app_commands.describe(prompt = "Der Satzanfang wird vervollständigt.")
# @app_commands.rename(quote_model = "style")
# @app_commands.describe(quote_model = "Woraus soll Heidi vervollständigen?")
# @app_commands.autocomplete(quote_model = quote_model_autocomplete)
# async def complete(interaction: discord.Interaction, prompt: str, quote_model: str):
# prompt = re.sub(r"[^a-zäöüß'.,]+", " ", prompt.lower()) # only keep valid chars
# generated_quote = client.textgen_models[quote_model].complete_sentence(prompt.split())
# joined_quote = " ".join(generated_quote)
# await interaction.response.send_message(f"Heidi sagt: \"{joined_quote}\"")
SOUNDDIR: str = "/sounds/" if DOCKER else "./voicelines/"
async def board_autocomplete(interaction: Interaction, current: str) -> list[Choice[str]]:
"""
Suggest a sound board.
"""
boards: list[str] = os.listdir(SOUNDDIR)
# Example: https://discordpy.readthedocs.io/en/latest/interactions/api.html?highlight=autocomplete#discord.app_commands.autocomplete
async def board_autocomplete(interaction: discord.Interaction, current: str) -> list[Choice[str]]:
boards = os.listdir(SOUNDDIR)
return [Choice(name=board, value=board) for board in boards]
async def sound_autocomplete(interaction: discord.Interaction, current: str) -> list[Choice[str]]:
board = interaction.namespace.board # TODO: Can't work?
sounds = map(lambda x: x.split(".")[0], os.listdir(SOUNDDIR + board + "/"))
return [Choice(name=sound, value=sound) for sound in sounds]
return [Choice(name=board, value=board) for board in boards if board.lower().startswith(current.lower())]
@client.tree.command(name = "sag", description = "Heidi drückt den Knopf auf dem Soundboard.")
@app_commands.describe(sound = "Was soll Heidi sagen?")
@app_commands.autocomplete(board = board_autocomplete)
@app_commands.autocomplete(sound = sound_autocomplete)
async def say_voiceline(interaction: discord.Interaction, board: str, sound: str):
async def sound_autocomplete(interaction: Interaction, current: str) -> list[Choice[str]]:
"""
Suggest a sound from an already selected board.
"""
board: str = interaction.namespace.board
sounds: list[str] = os.listdir(f"{SOUNDDIR}/{board}/")
return [
Choice(name=sound.split(".")[0], value=sound) for sound in sounds if sound.lower().startswith(current.lower())
]
@client.tree.command(name="sag", description="Heidi drückt den Knopf auf dem Soundboard.")
@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.
"""
# Only Members can access voice channels
if not isinstance(interaction.user, discord.Member):
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: discord.Member = interaction.user
member: Member = interaction.user
# Member needs to be in voice channel to hear audio (Heidi needs to know the channel to join)
if (not member.voice) or (not member.voice.channel) or (not isinstance(member.voice.channel, discord.VoiceChannel)):
print("User not in (valid) voice channel!")
await interaction.response.send_message("Heidi sagt: Komm in den Channel!")
return
await play_voice_line_for_member(interaction, member, board, sound)
voice_channel: discord.VoiceChannel = member.voice.channel
try:
open(SOUNDDIR + board + "/" + sound + ".mkv")
except IOError:
print("Error: Invalid soundfile!")
await interaction.response.send_message(f"Heidi sagt: \"{board}/{sound}\" kanninich finden bruder")
return
class InstantButton(ui.Button[ui.View]):
def __init__(self, label: str, board: str, sound: str) -> None:
super().__init__(style=ButtonStyle.red, label=label)
await interaction.response.send_message(f"Heidi sagt: \"{board}/{sound}\"")
self.board: str = board
self.sound: str = sound
audio_source = discord.FFmpegPCMAudio(SOUNDDIR + board + "/" + sound + ".mkv") # only works from docker
voice_client = await voice_channel.connect()
voice_client.play(audio_source)
@override
async def callback(self, interaction: Interaction) -> None:
"""
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
while voice_client.is_playing():
await asyncio.sleep(1)
await play_voice_line_for_member(interaction, interaction.user, self.board, self.sound)
class InstantButtonsView(ui.View):
def __init__(self, board: str, timeout: float | None = None) -> 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=InstantButtonsView(board))
await voice_client.disconnect()
# Contextmenu ------------------------------------------------------------------------------------
# TODO: More insults
# Callable on members
@client.tree.context_menu(name="beleidigen")
async def insult(interaction: discord.Interaction, member: discord.Member): # with message: discord.Message this can be called on a message
async def insult(
interaction: Interaction, member: Member
) -> None: # with message: discord.Message this can be called on a message
"""
Send an insult to a member via direct message.
"""
if not member.dm_channel:
await member.create_dm()
_ = await member.create_dm()
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 = [
@ -317,20 +474,24 @@ async def insult(interaction: discord.Interaction, member: discord.Member): # wi
"Opfer!",
"Du miese Raupe!",
"Geh Steckdosen befruchten!",
"Richtiger Gesichtsgünther ey!"
"Richtiger Gesichtsgünther ey!",
]
await member.dm_channel.send(random.choice(insults))
await interaction.response.send_message("Anzeige ist raus!") # with ephemeral = True only the caller can see the answer
_ = await member.dm_channel.send(random.choice(insults))
_ = await interaction.response.send_message(
"Anzeige ist raus!", ephemeral=True
) # with ephemeral = True only the caller can see the answer
# ------------------------------------------------------------------------------------------------
# Used to start the bot locally, for docker the variables have to be set when the container is run
load_dotenv()
TOKEN: str = os.getenv("DISCORD_TOKEN") or ""
# Start client if TOKEN valid
if TOKEN != "":
print(f"Running client with DOCKER={DOCKER}")
client.run(TOKEN, log_handler=handler)
else:
print("DISCORD_TOKEN not found, exiting...")

58
flake.lock generated
View File

@ -2,15 +2,14 @@
"nodes": {
"devshell": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs"
},
"locked": {
"lastModified": 1666548262,
"narHash": "sha256-4DyN4KXqQQsCw0vCXkMThw4b5Q4/q87ZZgRb4st8COc=",
"lastModified": 1741473158,
"narHash": "sha256-kWNaq6wQUbUMlPgw8Y+9/9wP0F8SHkjy24/mN3UAppg=",
"owner": "numtide",
"repo": "devshell",
"rev": "c8ce8ed81726079c398f5f29c4b68a7d6a3c2fa2",
"rev": "7c9e793ebe66bcba8292989a68c0419b737a22a0",
"type": "github"
},
"original": {
@ -20,27 +19,15 @@
}
},
"flake-utils": {
"locked": {
"lastModified": 1642700792,
"narHash": "sha256-XqHrk7hFb+zBvRg6Ghl+AZDq03ov6OshJLiSWOoX5es=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "846b2ae0fc4cc943637d3d1def4454213e203cba",
"type": "github"
"inputs": {
"systems": "systems"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"flake-utils_2": {
"locked": {
"lastModified": 1659877975,
"narHash": "sha256-zllb8aq3YO3h8B/U0/J1WBgAL8EX5yWf5pMj3G0NAmc=",
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "c0e246b9b83f637f4681389ecabcb2681b4f3af0",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
@ -51,11 +38,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1643381941,
"narHash": "sha256-pHTwvnN4tTsEKkWlXQ8JMY423epos8wUOhthpwJjtpc=",
"lastModified": 1722073938,
"narHash": "sha256-OpX0StkL8vpXyWOGUD6G+MA26wAXK6SpT94kLJXo6B4=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "5efc8ca954272c4376ac929f4c5ffefcc20551d5",
"rev": "e36e9f57337d0ff0cf77aceb58af4c805472bfae",
"type": "github"
},
"original": {
@ -67,11 +54,11 @@
},
"nixpkgs_2": {
"locked": {
"lastModified": 1666926733,
"narHash": "sha256-+gYfOEnQVISPDRNoWm2VJD5OEuTUySt48RchLpvm61o=",
"lastModified": 1758446476,
"narHash": "sha256-5rdAi7CTvM/kSs6fHe1bREIva5W3TbImsto+dxG4mBo=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "f44ba1be526c8da9e79a5759feca2365204003f6",
"rev": "a1f79a1770d05af18111fbbe2a3ab2c42c0f6cd0",
"type": "github"
},
"original": {
@ -84,9 +71,24 @@
"root": {
"inputs": {
"devshell": "devshell",
"flake-utils": "flake-utils_2",
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs_2"
}
},
"systems": {
"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",

View File

@ -5,73 +5,43 @@
inputs.flake-utils.url = "github:numtide/flake-utils";
inputs.devshell.url = "github:numtide/devshell";
outputs = { self, nixpkgs, flake-utils, devshell }:
flake-utils.lib.eachDefaultSystem (system:
let
pkgs = import nixpkgs {
inherit system;
config.allowUnfree = true;
overlays = [ devshell.overlay ];
};
outputs = {
self,
nixpkgs,
flake-utils,
devshell,
}:
flake-utils.lib.eachDefaultSystem (system: let
pkgs = import nixpkgs {
inherit system;
config.allowUnfree = true;
overlays = [devshell.overlays.default];
};
# TODO: Originally it was nixpkgs.fetchurl but that didn't work, pkgs.fetchurl did...
# Determine the difference between nixpkgs and pkgs
# Taken from: https://github.com/gbtb/nix-stable-diffusion/blob/master/flake.nix
# Overlay: https://nixos.wiki/wiki/Overlays
# FetchURL: https://ryantm.github.io/nixpkgs/builders/fetchers/
torch-rocm = pkgs.hiPrio (pkgs.python310Packages.torch-bin.overrideAttrs (old: {
src = pkgs.fetchurl {
name = "torch-1.12.1+rocm5.1.1-cp310-cp310-linux_x86_64.whl";
url = "https://download.pytorch.org/whl/rocm5.1.1/torch-1.12.1%2Brocm5.1.1-cp310-cp310-linux_x86_64.whl";
hash = "sha256-kNShDx88BZjRQhWgnsaJAT8hXnStVMU1ugPNMEJcgnA=";
};
}));
torchvision-rocm = pkgs.hiPrio (pkgs.python310Packages.torchvision-bin.overrideAttrs (old: {
src = pkgs.fetchurl {
name = "torchvision-0.13.1+rocm5.1.1-cp310-cp310-linux_x86_64.whl";
url = "https://download.pytorch.org/whl/rocm5.1.1/torchvision-0.13.1%2Brocm5.1.1-cp310-cp310-linux_x86_64.whl";
hash = "sha256-mYk4+XNXU6rjpgWfKUDq+5fH/HNPQ5wkEtAgJUDN/Jg=";
};
}));
myPython = pkgs.python310.withPackages (p: with p; [
# Basic
rich
# Discord
discordpy
python = pkgs.python313.withPackages (p:
with p; [
python-dotenv
pynacl
# Scraping
beautifulsoup4
requests
# MachineLearning
torch-rocm
torchvision-rocm
numpy
matplotlib
nltk
discordpy
pynacl # DiscordPy Voice Support
]);
in {
devShell = pkgs.devshell.mkShell {
name = "HeidiBot";
in {
devShell = pkgs.devshell.mkShell {
name = "HeidiBot";
packages = with pkgs; [
myPython
nodePackages.pyright # LSP
];
packages = with pkgs; [
python
# nodePackages.pyright # LSP
];
# Use $1 for positional args
commands = [
# {
# name = "";
# help = "";
# command = "";
# }
];
};
});
# Use $1 for positional args
commands = [
# {
# name = "";
# help = "";
# command = "";
# }
];
};
});
}

View File

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

BIN
heidi-sounds/basic/Alarm.mkv (Stored with Git LFS) Normal file

Binary file not shown.

BIN
heidi-sounds/basic/Bing Chilling.mkv (Stored with Git LFS) Normal file

Binary file not shown.

BIN
heidi-sounds/basic/Bingbong.mkv (Stored with Git LFS) Normal file

Binary file not shown.

BIN
heidi-sounds/basic/Durchfallmann.mkv (Stored with Git LFS) Normal file

Binary file not shown.

BIN
heidi-sounds/basic/Frosch.mkv (Stored with Git LFS) Normal file

Binary file not shown.

BIN
heidi-sounds/basic/Grab them.mkv (Stored with Git LFS) Normal file

Binary file not shown.

BIN
heidi-sounds/basic/Habicht.mkv (Stored with Git LFS) Normal file

Binary file not shown.

BIN
heidi-sounds/basic/Haha nein.mkv (Stored with Git LFS) Normal file

Binary file not shown.

BIN
heidi-sounds/basic/Jajaja.mkv (Stored with Git LFS) Normal file

Binary file not shown.

BIN
heidi-sounds/basic/Kein Foto.mkv (Stored with Git LFS) Normal file

Binary file not shown.

BIN
heidi-sounds/basic/Kurze Trinkpause.mkv (Stored with Git LFS) Normal file

Binary file not shown.

BIN
heidi-sounds/basic/Mach die Baschics.mkv (Stored with Git LFS) Normal file

Binary file not shown.

BIN
heidi-sounds/basic/Ok lets go.mkv (Stored with Git LFS) Normal file

Binary file not shown.

BIN
heidi-sounds/basic/Opfer.mkv (Stored with Git LFS) Normal file

Binary file not shown.

BIN
heidi-sounds/basic/PH.mkv (Stored with Git LFS) Normal file

Binary file not shown.

BIN
heidi-sounds/basic/Shock Dart.mkv (Stored with Git LFS) Normal file

Binary file not shown.

BIN
heidi-sounds/basic/Suiii.mkv (Stored with Git LFS) Normal file

Binary file not shown.

BIN
heidi-sounds/basic/Warum.mkv (Stored with Git LFS) Normal file

Binary file not shown.

BIN
heidi-sounds/basic/Yarak gruessen.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: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

BIN
heidi-sounds/drache/1900er.mkv (Stored with Git LFS) Normal file

Binary file not shown.

BIN
heidi-sounds/drache/Alter was.mkv (Stored with Git LFS) Normal file

Binary file not shown.

BIN
heidi-sounds/drache/Amok laufn.mkv (Stored with Git LFS) Normal file

Binary file not shown.

BIN
heidi-sounds/drache/Erschiessd euch.mkv (Stored with Git LFS) Normal file

Binary file not shown.

BIN
heidi-sounds/drache/Forza.mkv (Stored with Git LFS) Normal file

Binary file not shown.

BIN
heidi-sounds/drache/Gaudi.mkv (Stored with Git LFS) Normal file

Binary file not shown.

BIN
heidi-sounds/drache/Geh von meinem Grundstueck.mkv (Stored with Git LFS) Normal file

Binary file not shown.

BIN
heidi-sounds/drache/Geiles Stueck.mkv (Stored with Git LFS) Normal file

Binary file not shown.

BIN
heidi-sounds/drache/Hagebuddne.mkv (Stored with Git LFS) Normal file

Binary file not shown.

BIN
heidi-sounds/drache/Hurensohn hau ab.mkv (Stored with Git LFS) Normal file

Binary file not shown.

BIN
heidi-sounds/drache/Ich arbeite.mkv (Stored with Git LFS) Normal file

Binary file not shown.

BIN
heidi-sounds/drache/Ich bin ein Dummer.mkv (Stored with Git LFS) Normal file

Binary file not shown.

BIN
heidi-sounds/drache/Ich seh Punkte.mkv (Stored with Git LFS) Normal file

Binary file not shown.

BIN
heidi-sounds/drache/Lusa.mkv (Stored with Git LFS) Normal file

Binary file not shown.

BIN
heidi-sounds/drache/Mann.mkv (Stored with Git LFS) Normal file

Binary file not shown.

BIN
heidi-sounds/drache/Meddl.mkv (Stored with Git LFS) Normal file

Binary file not shown.

BIN
heidi-sounds/drache/Menschen ausloeschen.mkv (Stored with Git LFS) Normal file

Binary file not shown.

BIN
heidi-sounds/drache/Nice Sache.mkv (Stored with Git LFS) Normal file

Binary file not shown.

BIN
heidi-sounds/drache/Oehh.mkv (Stored with Git LFS) Normal file

Binary file not shown.

BIN
heidi-sounds/drache/Pruegel in der Hosne.mkv (Stored with Git LFS) Normal file

Binary file not shown.

BIN
heidi-sounds/drache/Schwaenze im Mund.mkv (Stored with Git LFS) Normal file

Binary file not shown.

BIN
heidi-sounds/drache/Seid ihr bescheuerd.mkv (Stored with Git LFS) Normal file

Binary file not shown.

BIN
heidi-sounds/drache/Wie Bidde.mkv (Stored with Git LFS) Normal file

Binary file not shown.

BIN
heidi-sounds/drache/Wuerde man merken.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: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

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

Binary file not shown.

BIN
heidi-sounds/henri/He turned into me.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:b0c9e2a5fc2e3529abd0e680b7a139b9239eae27f1e991b39f78187bb1a0fb88
size 939420

BIN
heidi-sounds/henri/Ich bin der Pablo.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:ee3fdbcafcba7e23cc60a45a5b5cc433068d8ca97b0e4d4be668148b9dab167c
size 753935

BIN
heidi-sounds/henri/Kann ich behilflich sein.mkv (Stored with Git LFS) Normal file

Binary file not shown.

BIN
heidi-sounds/henri/Koetzlicher Kill.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: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

BIN
heidi-sounds/henri/Tapier.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:1cd9cf0792ed7bafd38c760606d28ab366ff4fb320e41151161bd95b5ecc6848
size 3682445

BIN
heidi-sounds/henri/Yakari.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: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

BIN
heidi-sounds/jungs/Du Nuttensohn.mkv (Stored with Git LFS) Normal file

Binary file not shown.

BIN
heidi-sounds/jungs/Florian du Hurensohn.mkv (Stored with Git LFS) Normal file

Binary file not shown.

BIN
heidi-sounds/jungs/Halts Maul Linus.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: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

BIN
heidi-sounds/jungs/Jajaja.mkv (Stored with Git LFS) Normal file

Binary file not shown.

BIN
heidi-sounds/jungs/Knorke.mkv (Stored with Git LFS) Normal file

Binary file not shown.

BIN
heidi-sounds/jungs/Neinneinnein.mkv (Stored with Git LFS) Normal file

Binary file not shown.

BIN
heidi-sounds/jungs/Sven du Hurensohn.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:41f86b71a9da251552f9aefd2603b8221c0066ae2a9156ee2642285a0a48c3bf
size 21591

BIN
heidi-sounds/kaptn/Discord Call.mkv (Stored with Git LFS) Normal file

Binary file not shown.

BIN
heidi-sounds/kaptn/FBI open up.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:0e66e31216df98dc166528576f9d69f1a1b64ebb49b3790a56b285fee73b7a2e
size 1512452

Some files were not shown because too many files have changed in this diff Show More