Clean up type hints everywhere, overhaul bot configuration
All checks were successful
Build Heidi Docker image / build-docker (push) Successful in 2m12s

This commit is contained in:
2025-09-23 20:39:11 +02:00
parent 591f4ea191
commit 75fb627361
6 changed files with 332 additions and 184 deletions

View File

@ -1,22 +1,49 @@
import configparser
from discord import app_commands, Message, VoiceState
import asyncio
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_helpers import *
from heidi_config import USER_CONFIG_SCHEME, ConfigSection, FlagsConfigKey
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):
def __init__(self, *, intents: discord.Intents):
super().__init__(status="Nur eine kann GNTM werden!", intents=intents)
class HeidiClient(Client):
def __init__(self, *, 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
self.tree = app_commands.CommandTree(self)
self.tree: CommandTree = CommandTree(self)
# Handle persistent user configuration
self.user_config = configparser.ConfigParser()
self.user_config: ConfigParser = ConfigParser()
if not os.path.exists(f"{CONFIGPATH}/{USERCONFIGNAME}"):
open(f"{CONFIGPATH}/{USERCONFIGNAME}", "x")
self.user_config.read(f"{CONFIGPATH}/{USERCONFIGNAME}")
# Create the file
_ = open(f"{CONFIGPATH}/{USERCONFIGNAME}", "x")
_ = self.user_config.read(f"{CONFIGPATH}/{USERCONFIGNAME}")
self.update_to_default_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)
# the predicate receives the message as argument
# 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: "jeremy" in m.author.nick.lower(): self._autoreact_to_jeremy,
lambda m: "kardashian" in m.author.nick.lower()
or "jenner" in m.author.nick.lower(): self._autoreact_to_kardashian,
create_author_based_message_predicate(["jeremy"]): self._autoreact_to_jeremy,
create_author_based_message_predicate(["kardashian", "jenner"]): self._autoreact_to_kardashian,
}
# automatic actions on voice state changes
# 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
# if the predicate is true, the action is performed
self.on_voice_state_triggers = {
lambda m, b, a: b.channel != a.channel
and a.channel is not None
and isinstance(a.channel, VoiceChannel): self._play_entrance_sound,
self.on_voice_state_triggers: dict[
Callable[[Member, VoiceState, VoiceState], bool],
Callable[[Member, VoiceState, VoiceState], CoroutineType[Any, Any, None]],
] = {
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
async def setup_hook(self):
@override
async def setup_hook(self) -> None:
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)
await self.tree.sync(guild=TEST_GUILD)
_ = await self.tree.sync(guild=TEST_GUILD)
def update_to_default_user_config(self) -> None:
"""
Adds config keys to the config, if they don't exist yet.
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:
print(f"Adding section {section} to {CONFIGPATH}/{USERCONFIGNAME}")
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()
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.
"""
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
if (
member is not None
disable_join_sound_if_alone
and member.voice is not None
and member.voice.channel is not None
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")
return
soundpath: Union[str, None] = self.user_config["ENTRANCE.SOUND"].get(
member.name, None
)
soundpath: str | None = self.user_config["ENTRANCE.SOUND"].get(member.name, None)
if soundpath is None:
print(f"User {member.name} has not set an entrance sound")