From 6053692bc191e4a18276363e1c080f8ddbe7aa2d Mon Sep 17 00:00:00 2001 From: enricobuehler Date: Wed, 18 Feb 2026 00:56:54 +0100 Subject: [PATCH] implement first interaction handling --- .../discord/src/actions/publish-commands.ts | 5 +- adapters/discord/src/actions/shutdown.ts | 4 +- adapters/discord/src/config/config.schema.ts | 2 +- adapters/discord/src/config/index.ts | 4 +- adapters/discord/src/discord.controller.ts | 111 -------- .../channels/voice/voice-channels.service.ts | 50 ++++ .../dynamic-voice-channel.service.ts | 30 +++ .../log-channel/log-channel.service.ts | 2 +- .../features/water-me/water-me.controller.ts | 6 +- adapters/discord/src/index.ts | 1 + adapters/discord/src/listeners/index.ts | 8 +- .../listeners/interactions/handle-button.ts | 5 +- .../interactions/handle-chat-input-command.ts | 5 +- .../listeners/messages/messages.listener.ts | 8 +- .../discord/src/listeners/ready.listener.ts | 22 +- .../handle-dynamic-voice-channel-creation.ts | 28 ++ .../handle-dynamic-voice-channel-deletion.ts | 21 ++ .../voice-state/voice-state.listener.ts | 7 +- core/package.json | 3 + core/src/actions/drink/drink.service.ts | 14 - core/src/actions/drink/drink.task.ts | 0 .../dynamicVChannel.components.ts | 0 .../dynamicVChannel.service.ts | 60 ----- .../medication/medication.components.ts | 10 - .../actions/medication/medication.service.ts | 250 ------------------ .../src/actions/medication/medication.task.ts | 15 -- core/src/entities/channels/channels.schema.ts | 2 +- .../channels/voice/voice-channels.service.ts | 14 + .../dynamic-voice-channels.schema.ts | 20 ++ .../dynamic-voice-channels.service.ts | 62 +++++ .../text-based-feature.schema.ts | 2 +- 31 files changed, 278 insertions(+), 493 deletions(-) create mode 100644 adapters/discord/src/entitites/channels/voice/voice-channels.service.ts create mode 100644 adapters/discord/src/features/dynamic-voice-channel/dynamic-voice-channel.service.ts create mode 100644 adapters/discord/src/listeners/voice-state/handle-dynamic-voice-channel-creation.ts create mode 100644 adapters/discord/src/listeners/voice-state/handle-dynamic-voice-channel-deletion.ts delete mode 100644 core/src/actions/drink/drink.service.ts delete mode 100644 core/src/actions/drink/drink.task.ts delete mode 100644 core/src/actions/dynamicVChannel/dynamicVChannel.components.ts delete mode 100644 core/src/actions/dynamicVChannel/dynamicVChannel.service.ts delete mode 100644 core/src/actions/medication/medication.components.ts delete mode 100644 core/src/actions/medication/medication.service.ts delete mode 100644 core/src/actions/medication/medication.task.ts create mode 100644 core/src/entities/channels/voice/voice-channels.service.ts create mode 100644 core/src/features/dynamic-voice-channels/dynamic-voice-channels.schema.ts create mode 100644 core/src/features/dynamic-voice-channels/dynamic-voice-channels.service.ts diff --git a/adapters/discord/src/actions/publish-commands.ts b/adapters/discord/src/actions/publish-commands.ts index 372e034..b9461ca 100644 --- a/adapters/discord/src/actions/publish-commands.ts +++ b/adapters/discord/src/actions/publish-commands.ts @@ -1,6 +1,7 @@ import { config } from "config"; import { Routes } from "discord.js"; import getCommands from "entitites/commands"; +import { logger } from "lib/common-logger"; import { discordRestClient } from "lib/rest-client"; export const publishCommands = async () => { @@ -11,8 +12,8 @@ export const publishCommands = async () => { body: getCommands(), }, ); - console.log("Successfully added commands"); + logger.info("successfully added commands"); } catch (e) { - console.error(e); + logger.error("failed to add commands:", e); } }; diff --git a/adapters/discord/src/actions/shutdown.ts b/adapters/discord/src/actions/shutdown.ts index 27c4225..5995b19 100644 --- a/adapters/discord/src/actions/shutdown.ts +++ b/adapters/discord/src/actions/shutdown.ts @@ -2,6 +2,6 @@ import { logChannelService } from "features/log-channel/log-channel.service"; import { logger } from "lib/common-logger"; export const handleShutdown = async () => { - logger.info("Bot is shutting down..."); - await logChannelService.sendLogMessage("Bot is shutting down..."); + logger.info("bot is shutting down..."); + await logChannelService.sendLogMessage("bot is shutting down..."); }; diff --git a/adapters/discord/src/config/config.schema.ts b/adapters/discord/src/config/config.schema.ts index 3d5beaa..4cb0409 100644 --- a/adapters/discord/src/config/config.schema.ts +++ b/adapters/discord/src/config/config.schema.ts @@ -14,7 +14,7 @@ export const ConfigSchema = z.object({ reactionRoles: z.object({ allowedMessageIds: z.array(z.string()), }), - serverId: z.string(), + guildId: z.string(), version: z.number(), discord: z.object({ token: z.string(), diff --git a/adapters/discord/src/config/index.ts b/adapters/discord/src/config/index.ts index fc8bb5a..325a3f8 100644 --- a/adapters/discord/src/config/index.ts +++ b/adapters/discord/src/config/index.ts @@ -6,12 +6,12 @@ export const config: z.output = { channelMapping: { text: { bot: "1317253291633279026", + log: "1321972449365065728", bump: "1330310075759329412", feedback: "1321626600185266176", help: "1321860125127872605", introduction: "1321473655523246171", news: "1321953521435934741", - notification: "1321972449365065728", "off-topic": "1316153372507639855", rules: "1316153372507639849", testing: "1451310086864507112", @@ -40,7 +40,7 @@ export const config: z.output = { "1321491461111283722", // Example message ID for reaction roles ], }, - serverId: "1316153371899592774", + guildId: "1316153371899592774", version: 1, discord: { token: env.DISCORD_TOKEN, diff --git a/adapters/discord/src/discord.controller.ts b/adapters/discord/src/discord.controller.ts index 9f634da..aea2933 100644 --- a/adapters/discord/src/discord.controller.ts +++ b/adapters/discord/src/discord.controller.ts @@ -6,117 +6,6 @@ import { import client from "lib/client"; export default class DiscordController extends EventEmitter { - private waterMeController: WaterMeController = waterMeController; - - constructor() { - super(); - let channelListeners = new Map(); - - client.on( - Events.VoiceStateUpdate, - async (oldState: VoiceState, newState: VoiceState) => { - // check if user joined a vc - if ( - (!oldState.channelId && newState.channelId) || - oldState.channelId !== newState.channelId - ) { - // check if right vc - if ( - newState.channelId === config.discord.vchannelIdForTwo || - newState.channelId === config.discord.vchannelIdForThree || - newState.channelId === config.discord.vchannelIdForFour || - newState.channelId === config.discord.vchannelIdForGroup - ) { - const channel = newState.channel; - if (!channel) { - console.error("channel not found"); - return; - } - try { - const newChannel = - await this.dynamicVChannelService.createVChannel( - newState, - channel, - ); - // move user in new channel - await newState.setChannel(newChannel); - // create specific listener for channel - const channelListener = async ( - oldState: VoiceState, - newState: VoiceState, - ) => { - channelListeners = - await this.dynamicVChannelService.deleteVChannel( - oldState, - newState, - newChannel, - channelListeners, - channelListener, - ); - }; - // save listener in map - channelListeners.set(newChannel.id, channelListener); - // add listener - client.on(Events.VoiceStateUpdate, channelListener); - } catch (error) { - console.error("error while duplicating channel", error); - } - } - } - }, - ); - console.log( - `----------------\nversion ${config.discord.version}\n----------------`, - ); - } - - async setActivity(state: number) { - switch (state) { - case 0: - client.user?.setActivity(" ", { type: 0 }); - console.log("set activity"); - client.user?.setPresence({ - status: "invisible", - }); - break; - default: - client.user?.setActivity("spielt sudoku", { type: 0 }); - console.log("set activity"); - client.user?.setPresence({ - status: "online", - }); - break; - } - } - - async handleInteraction(interaction: Interaction) { - if (interaction.isModalSubmit()) { - await this.handleModalSubmit(interaction); - return; - } - - if (interaction.isChatInputCommand()) { - await this.handleChatInputCommand(interaction); - return; - } - if (interaction.isButton()) { - await this.handleButton(interaction); - return; - } - } - - async handleButton(interaction: ButtonInteraction) { - const { customId } = interaction; - console.log(interaction.customId); - - if (customId.toLowerCase().includes("moreWater")) { - await this.waterMeService.handleInteraction(interaction); - } - if (customId.toLowerCase().includes("medication")) { - await this.medicationService.handleInteraction(interaction); - } - } - async handleChatInputCommand( interaction: ChatInputCommandInteraction, ) { diff --git a/adapters/discord/src/entitites/channels/voice/voice-channels.service.ts b/adapters/discord/src/entitites/channels/voice/voice-channels.service.ts new file mode 100644 index 0000000..5b560b2 --- /dev/null +++ b/adapters/discord/src/entitites/channels/voice/voice-channels.service.ts @@ -0,0 +1,50 @@ +import type { VoiceChannelsServiceInterface } from "@avocadi/bot-core/entities/channels/voice/voice-channels.service"; +import { config } from "config"; +import { ChannelType, type VoiceChannel } from "discord.js"; +import client from "lib/client"; + +export class VoiceChannelsService + implements VoiceChannelsServiceInterface +{ + async getVoiceChannelById(channelId: string) { + const channel = await client.channels.fetch(channelId); + + if (!channel) { + throw new Error(`Channel with id ${channelId} not found`); + } + + if (channel.type !== ChannelType.GuildVoice) { + throw new Error(`Channel with id ${channelId} is not a voice channel`); + } + + return channel; + } + + async createVoiceChannel(name: string) { + const guild = await client.guilds.fetch(config.guildId); + const channel = await guild.channels.create({ + name, + type: ChannelType.GuildVoice, + }); + return channel; + } + + async cloneVoiceChannel( + channel: VoiceChannel, + options?: { name?: string; position?: number }, + ) { + const clonedChannel = await channel.clone({ + name: options?.name, + position: options?.position, + }); + return clonedChannel; + } + + async deleteVoiceChannel(channelId: string) { + const channel = await this.getVoiceChannelById(channelId); + + await channel.delete(); + } +} + +export const voiceChannelsService = new VoiceChannelsService(); diff --git a/adapters/discord/src/features/dynamic-voice-channel/dynamic-voice-channel.service.ts b/adapters/discord/src/features/dynamic-voice-channel/dynamic-voice-channel.service.ts new file mode 100644 index 0000000..49df4d9 --- /dev/null +++ b/adapters/discord/src/features/dynamic-voice-channel/dynamic-voice-channel.service.ts @@ -0,0 +1,30 @@ +import { VoiceChannels } from "@avocadi/bot-core/entities/channels/channels.schema"; +import { DynamicVoiceChannelsService } from "@avocadi/bot-core/features/dynamic-voice-channels/dynamic-voice-channels.service"; +import { config } from "config"; +import { voiceChannelsService } from "entitites/channels/voice/voice-channels.service"; + +export const dynamicVoiceChannelService = new DynamicVoiceChannelsService( + voiceChannelsService, + [ + { + channelId: config.channelMapping.voice["for-two"], + key: VoiceChannels.enum["for-two"], + size: 2, + }, + { + channelId: config.channelMapping.voice["for-three"], + key: VoiceChannels.enum["for-three"], + size: 3, + }, + { + channelId: config.channelMapping.voice["for-four"], + key: VoiceChannels.enum["for-four"], + size: 4, + }, + { + channelId: config.channelMapping.voice["for-group"], + key: VoiceChannels.enum["for-group"], + size: 99, + }, + ], +); diff --git a/adapters/discord/src/features/log-channel/log-channel.service.ts b/adapters/discord/src/features/log-channel/log-channel.service.ts index f241146..b5b4661 100644 --- a/adapters/discord/src/features/log-channel/log-channel.service.ts +++ b/adapters/discord/src/features/log-channel/log-channel.service.ts @@ -3,7 +3,7 @@ import client from "lib/client"; import { logger } from "lib/common-logger"; export class LogChannelService { - private logChannelId = config.channelMapping.text.bot; + private logChannelId = config.channelMapping.text.log; async getLogChannel() { const logChannel = await client.channels.fetch(this.logChannelId); diff --git a/adapters/discord/src/features/water-me/water-me.controller.ts b/adapters/discord/src/features/water-me/water-me.controller.ts index 91be47c..65b378d 100644 --- a/adapters/discord/src/features/water-me/water-me.controller.ts +++ b/adapters/discord/src/features/water-me/water-me.controller.ts @@ -2,8 +2,10 @@ import type { WaterMeService } from "@avocadi/bot-core/features/water-me/water-m import { ActionRowBuilder, ButtonBuilder, + type ButtonInteraction, ButtonStyle, type CacheType, + type ChatInputCommandInteraction, type Interaction, } from "discord.js"; import { waterMeService } from "./water-me.service"; @@ -15,7 +17,9 @@ class WaterMeController { this.waterMeService = waterMeService; } - async handleInteraction(interaction: Interaction) { + async handleInteraction( + interaction: ButtonInteraction | ChatInputCommandInteraction, + ) { const result = this.waterMeService.waterMe(); const moreButton = new ButtonBuilder() diff --git a/adapters/discord/src/index.ts b/adapters/discord/src/index.ts index e7612fb..bf463ec 100644 --- a/adapters/discord/src/index.ts +++ b/adapters/discord/src/index.ts @@ -1,4 +1,5 @@ import { publishCommands } from "actions/publish-commands"; +import "./listeners"; // Publish commands when the application starts await publishCommands(); diff --git a/adapters/discord/src/listeners/index.ts b/adapters/discord/src/listeners/index.ts index 6ba5aef..7226ff1 100644 --- a/adapters/discord/src/listeners/index.ts +++ b/adapters/discord/src/listeners/index.ts @@ -1,5 +1,7 @@ import "./ready.listener"; -import "./guild-member-add.listener"; +import "./stop.listener"; +import "./guild-members/guild-members.listener"; import "./voice-state/voice-state.listener"; -import "./message-reaction-add.listener"; -import "./message-reaction-remove.listener"; +import "./interactions/interactions.listener"; +import "./reactions/reactions.listener"; +import "./messages/messages.listener"; diff --git a/adapters/discord/src/listeners/interactions/handle-button.ts b/adapters/discord/src/listeners/interactions/handle-button.ts index 71a037b..702a58e 100644 --- a/adapters/discord/src/listeners/interactions/handle-button.ts +++ b/adapters/discord/src/listeners/interactions/handle-button.ts @@ -1,5 +1,8 @@ import type { ButtonInteraction } from "discord.js"; +import { waterMeController } from "features/water-me/water-me.controller"; export const handleButtonInteraction = async ( interaction: ButtonInteraction, -) => {}; +) => { + waterMeController.handleInteraction(interaction); +}; diff --git a/adapters/discord/src/listeners/interactions/handle-chat-input-command.ts b/adapters/discord/src/listeners/interactions/handle-chat-input-command.ts index 38b666a..9a10b30 100644 --- a/adapters/discord/src/listeners/interactions/handle-chat-input-command.ts +++ b/adapters/discord/src/listeners/interactions/handle-chat-input-command.ts @@ -1,5 +1,8 @@ import type { ChatInputCommandInteraction } from "discord.js"; +import { waterMeController } from "features/water-me/water-me.controller"; export const handleChatInputCommandInteraction = async ( interaction: ChatInputCommandInteraction, -) => {}; +) => { + waterMeController.handleInteraction(interaction); +}; diff --git a/adapters/discord/src/listeners/messages/messages.listener.ts b/adapters/discord/src/listeners/messages/messages.listener.ts index b54eb4f..f518faa 100644 --- a/adapters/discord/src/listeners/messages/messages.listener.ts +++ b/adapters/discord/src/listeners/messages/messages.listener.ts @@ -1,6 +1,12 @@ +import { config } from "config"; import { messagesService } from "entitites/messages/messages.service"; import client from "lib/client"; client.on("messageCreate", async (message) => { - messagesService.logMessage(message); + if ( + message.channel.id !== config.channelMapping.text.log && + message.channel.id !== config.channelMapping.text.bot + ) { + messagesService.logMessage(message); + } }); diff --git a/adapters/discord/src/listeners/ready.listener.ts b/adapters/discord/src/listeners/ready.listener.ts index 629c99e..19a08d5 100644 --- a/adapters/discord/src/listeners/ready.listener.ts +++ b/adapters/discord/src/listeners/ready.listener.ts @@ -1,22 +1,14 @@ import { config } from "config"; import { activityService } from "features/activity/activity.service"; +import { logChannelService } from "features/log-channel/log-channel.service"; import client from "lib/client"; import { logger } from "lib/common-logger"; -client.once("ready", async () => { - const channels = client.channels; - const logChannel = channels.cache.get(config.channelMapping.text.bot); +client.once("clientReady", async () => { + await logChannelService.sendLogMessage("wieder online!!!"); - if (logChannel?.isTextBased() && logChannel?.isSendable()) { - try { - logger.info("bot is online"); - await logChannel.send("wieder online!!!"); - } catch (error) { - logger.error("failed to send online message:", error); - } - } else { - logger.error("log channel is not valid or sendable."); - } - await activityService.set("playing"); - logger.info("ready"); + logger.info("bot is online"); + + await activityService.set("competing"); + logger.info("finished ready procedure"); }); diff --git a/adapters/discord/src/listeners/voice-state/handle-dynamic-voice-channel-creation.ts b/adapters/discord/src/listeners/voice-state/handle-dynamic-voice-channel-creation.ts new file mode 100644 index 0000000..417d523 --- /dev/null +++ b/adapters/discord/src/listeners/voice-state/handle-dynamic-voice-channel-creation.ts @@ -0,0 +1,28 @@ +import type { VoiceState } from "discord.js"; +import { dynamicVoiceChannelService } from "features/dynamic-voice-channel/dynamic-voice-channel.service"; +import { logger } from "lib/common-logger"; + +export const handleDynamicVoiceChannelCreation = async ( + oldState: VoiceState, + newState: VoiceState, +) => { + const userJoinedChannel = !oldState.channelId && newState.channelId; + const userSwitchedChannel = oldState.channelId !== newState.channelId; + + if (userJoinedChannel || userSwitchedChannel) { + const channel = newState.channel; + + if (!channel) { + logger.error("Channel not found for VoiceStateUpdate event"); + return; + } + + // New channel is created if user joined one of the specific channels (e.g. "Join to Create") or switched to one of them + const newChannel = + await dynamicVoiceChannelService.createDynamicVoiceChannel(channel.id); + + if (newChannel) { + await newState.setChannel(newChannel); + } + } +}; diff --git a/adapters/discord/src/listeners/voice-state/handle-dynamic-voice-channel-deletion.ts b/adapters/discord/src/listeners/voice-state/handle-dynamic-voice-channel-deletion.ts new file mode 100644 index 0000000..5ebb6f3 --- /dev/null +++ b/adapters/discord/src/listeners/voice-state/handle-dynamic-voice-channel-deletion.ts @@ -0,0 +1,21 @@ +import type { VoiceState } from "discord.js"; +import { voiceChannelsService } from "entitites/channels/voice/voice-channels.service"; +import { dynamicVoiceChannelService } from "features/dynamic-voice-channel/dynamic-voice-channel.service"; + +export const handleDynamicVoiceChannelDeletion = async ( + oldState: VoiceState, + newState: VoiceState, +) => { + // Check if user left a channel that was created by the bot and delete it if it's empty + if (oldState.channelId && !newState.channelId) { + const channelId = oldState.channelId; + + if (dynamicVoiceChannelService.createdChannelIdsSet.has(channelId)) { + const channel = await voiceChannelsService.getVoiceChannelById(channelId); + + if (channel.members.size === 0) { + await dynamicVoiceChannelService.deleteDynamicVoiceChannel(channelId); + } + } + } +}; diff --git a/adapters/discord/src/listeners/voice-state/voice-state.listener.ts b/adapters/discord/src/listeners/voice-state/voice-state.listener.ts index 07db4d8..1c34360 100644 --- a/adapters/discord/src/listeners/voice-state/voice-state.listener.ts +++ b/adapters/discord/src/listeners/voice-state/voice-state.listener.ts @@ -1,6 +1,11 @@ import { Events } from "discord.js"; import client from "lib/client"; +import { handleDynamicVoiceChannelCreation } from "./handle-dynamic-voice-channel-creation"; +import { handleDynamicVoiceChannelDeletion } from "./handle-dynamic-voice-channel-deletion"; client.on(Events.VoiceStateUpdate, async (oldState, newState) => { - // TODO: handle updates + await Promise.allSettled([ + handleDynamicVoiceChannelCreation(oldState, newState), + handleDynamicVoiceChannelDeletion(oldState, newState), + ]); }); diff --git a/core/package.json b/core/package.json index f5e705c..031bee6 100644 --- a/core/package.json +++ b/core/package.json @@ -33,12 +33,15 @@ "./db": "./dist/db/index.js", "./db/schema": "./dist/db/schema.js", "./entities/channels/channels.schema": "./dist/entities/channels/channels.schema.js", + "./entities/channels/voice/voice-channels.service": "./dist/entities/channels/voice/voice-channels.service.js", "./entities/commands/commands.entity": "./dist/entities/commands/commands.entity.js", "./entities/commands/commands.schema": "./dist/entities/commands/commands.schema.js", "./entities/interactions/interactions.schema": "./dist/entities/interactions/interactions.schema.js", "./entities/messages/messages.service": "./dist/entities/messages/messages.service.js", "./entities/roles/roles.schema": "./dist/entities/roles/roles.schema.js", "./entities/roles/roles.service": "./dist/entities/roles/roles.service.js", + "./features/dynamic-voice-channels/dynamic-voice-channels.schema": "./dist/features/dynamic-voice-channels/dynamic-voice-channels.schema.js", + "./features/dynamic-voice-channels/dynamic-voice-channels.service": "./dist/features/dynamic-voice-channels/dynamic-voice-channels.service.js", "./features/greeting/greeting.service": "./dist/features/greeting/greeting.service.js", "./features/text-based-feature/text-based-feature": "./dist/features/text-based-feature/text-based-feature.js", "./features/text-based-feature/text-based-feature.schema": "./dist/features/text-based-feature/text-based-feature.schema.js", diff --git a/core/src/actions/drink/drink.service.ts b/core/src/actions/drink/drink.service.ts deleted file mode 100644 index 00e1b55..0000000 --- a/core/src/actions/drink/drink.service.ts +++ /dev/null @@ -1,14 +0,0 @@ -import config from "config"; -import client from "lib/client"; - -export class GreetingService { - async greet() { - const channels = client.channels; - - const channel = channels.cache.get(config.discord.channelIdBot); - - if (channel?.isTextBased && channel?.isSendable()) { - await channel.send({ content: "HALLOOOO" }); - } - } -} diff --git a/core/src/actions/drink/drink.task.ts b/core/src/actions/drink/drink.task.ts deleted file mode 100644 index e69de29..0000000 diff --git a/core/src/actions/dynamicVChannel/dynamicVChannel.components.ts b/core/src/actions/dynamicVChannel/dynamicVChannel.components.ts deleted file mode 100644 index e69de29..0000000 diff --git a/core/src/actions/dynamicVChannel/dynamicVChannel.service.ts b/core/src/actions/dynamicVChannel/dynamicVChannel.service.ts deleted file mode 100644 index 559442d..0000000 --- a/core/src/actions/dynamicVChannel/dynamicVChannel.service.ts +++ /dev/null @@ -1,60 +0,0 @@ -import config from "config"; -import client from "lib/client"; -import { getRandomInt } from "lib/utils"; -import { } from "./dynamicVChannel.components.ts"; -import { - Client, - EmbedBuilder, - type Message, - type CacheType, - type GuildMember, - type Interaction, - type OmitPartialGroupDMChannel, - type VoiceState, - type VoiceChannel, - type StageChannel, - Events, - type VoiceBasedChannel, -} from "discord.js"; - -export class DynamicVChannelService { - async handleInteraction(interaction: Interaction) { - // todo - } - - async createVChannel( - newState: VoiceState, - channel: VoiceBasedChannel - ): Promise { - //console.log("createChannel()"); - const newVChannel = await channel.clone({ - name: `${channel.name.substring(2)}; ${newState.member?.displayName}`, - position: 100, - }); - - return newVChannel; - } - - async deleteVChannel( - oldState: VoiceState, - newState: VoiceState, - newChannel: StageChannel | VoiceChannel, - // biome-ignore lint/suspicious/noExplicitAny: - channelListeners: Map, - channelListener: (oldState: VoiceState, newState: VoiceState) => void - ) { - //console.log("deleteChannel()"); - if ( - oldState.channelId === newChannel.id || - newState.channelId === newChannel.id - ) { - if (newChannel.members.size === 0) { - newChannel.delete().catch(console.error); - - client.removeListener(Events.VoiceStateUpdate, channelListener); - channelListeners.delete(newChannel.id); - } - } - return channelListeners; - } -} diff --git a/core/src/actions/medication/medication.components.ts b/core/src/actions/medication/medication.components.ts deleted file mode 100644 index ef4c52d..0000000 --- a/core/src/actions/medication/medication.components.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { ButtonBuilder, ButtonStyle } from "discord.js"; - -export const yesButton = new ButtonBuilder() - .setCustomId("yesMedication") - .setLabel("ja") - .setStyle(ButtonStyle.Primary); -export const noButton = new ButtonBuilder() - .setCustomId("noMedication") - .setLabel("noch nicht") - .setStyle(ButtonStyle.Secondary); diff --git a/core/src/actions/medication/medication.service.ts b/core/src/actions/medication/medication.service.ts deleted file mode 100644 index a01abc8..0000000 --- a/core/src/actions/medication/medication.service.ts +++ /dev/null @@ -1,250 +0,0 @@ -import { CronJob } from "cron"; -import { getRandomInt } from "lib/utils"; -import config from "config"; -import client from "lib/client"; -import { - ActionRowBuilder, - ButtonBuilder, - type ButtonInteraction, - ButtonStyle, - type ChatInputCommandInteraction, - type ModalSubmitInteraction, - type CacheType, - type Interaction, -} from "discord.js"; -import { yesButton, noButton } from "./medication.components"; -import { db } from "db"; -import { usersTable } from "db/schema"; -import { eq } from "drizzle-orm"; - -export class MedicationService { - medication: string; - tookMedication: boolean; - - constructor() { - this.medication = ""; - this.tookMedication = false; - } - - async askMedication() { - const channels = client.channels; - - const channel = channels.cache.get(config.discord.channelIdBot); - - // funkt noch nicht, beides - - const row = new ActionRowBuilder().addComponents(yesButton); - row.addComponents(noButton); - - if ( - channel?.isTextBased && - channel?.isSendable() && - this.tookMedication === false - ) { - await channel.send({ - content: "hast du schon deine medis genommen? :)", - // biome-ignore lint/suspicious/noExplicitAny: - components: [row as any], - }); - } - } - - getMedication() { - // todo - } - - setMedication() { - const reply = this.getReply(); - console.log("medication"); - - // this.medication = input von user:in hier rein; - return { - reply, - }; - } - - getReply() { - return "medication reminder"; - } - - async handleInteraction(interaction: Interaction) { - if (interaction.isModalSubmit()) { - await this.handleModalSubmit(interaction); - return; - } - - if (interaction.isChatInputCommand()) { - await this.handleChatInputCommand(interaction); - return; - } - if (interaction.isButton()) { - await this.handleButton(interaction); - return; - } - } - - async handleModalSubmit(interaction: ModalSubmitInteraction) { - switch (interaction.customId) { - default: - break; - } - } - - async handleButton(interaction: ButtonInteraction) { - console.log("button interaction"); - - const discordId = Number.parseInt(interaction.user.id); - const name = interaction.user.displayName; - - console.log(`userid: ${discordId}`); - - try { - const userId = await this.ensureUserExists(discordId, name); - console.log(`userid: ${userId}`); - - const tookMedication = interaction.customId === "yesMedication"; - - await interaction.reply({ - content: tookMedication - ? "das hast du toll gemacht <3 mach weiter so :3" - : "das passiert mal... aber versuch sie heute noch zu nehmen, oki? :)", - }); - await this.logMedication(userId, tookMedication); - } catch (error) { - console.error("error ensuring user exists:", error); - await interaction.reply({ - content: - "es gab einen fehler beim verarbeiten deiner anfrage :( versuch es bitte spaeter nochmal, oki? c:", - ephemeral: true, - }); - return; - } - } - - async handleChatInputCommand( - interaction: ChatInputCommandInteraction, - ) { - const result = this.setMedication(); - - const row = new ActionRowBuilder().addComponents(yesButton); - row.addComponents(noButton); - await interaction.reply({ - content: result.reply, - // biome-ignore lint/suspicious/noExplicitAny: - components: [row as any], - }); - } - - /**, um die Benutzerdaten in die Datenbank zu schreiben. - * @param discordId unique user id - * @param name name how the user wants to get called by avocadi - * @param tookMedication if user took medication - */ - async logMedication(id: number, tookMedication: boolean) { - try { - await db - .update(usersTable) - .set({ - took_medication_today: Number(tookMedication), - }) - .where(eq(usersTable.id, id)); - - /* await db.insert(usersTable).values({ - name: name, - discord_id: discordId, - took_medication_today: Number(tookMedication), - }); */ - - console.log(`user with id ${id} saved`); - } catch (error) { - console.error("error while saving in db:", error); - } - } - - async getNameByDiscordId(discordId: number): Promise { - const result = await db - .select({ - name: usersTable.name, - }) - .from(usersTable) - .where(eq(usersTable.discord_id, discordId)) - .limit(1); - - if (result.length > 0) { - console.log("user found"); - return result[0].name; - } - console.log("name not found"); - return ""; - } - - async findUserIdByDiscordId(discordId: number): Promise { - try { - const result = await db - .select({ - id: usersTable.id, - }) - .from(usersTable) - .where(eq(usersTable.discord_id, discordId)) - .limit(1); - - if (result.length > 0) { - console.log(`ID für Discord-ID ${discordId} gefunden: ${result[0].id}`); - return result[0].id; - } - console.log(`no id for discordId ${discordId} found`); - return null; - } catch (error) { - console.error( - `error while calling id for discordId ${discordId}:`, - error, - ); - return null; - } - } - - async ensureUserExists(discordId: number, name: string): Promise { - try { - const userId = await this.findUserIdByDiscordId(discordId); - - if (userId !== null) { - console.log(`entry for discordID ${discordId} already exists`); - return userId; - } - - console.log( - `found no entry for discordID ${discordId}. creating new entry`, - ); - - const result = await db - .insert(usersTable) - .values({ - name: name, - discord_id: discordId, - }) - .onConflictDoNothing() - .returning({ - id: usersTable.id, - }); - - if (result.length > 0) { - const newUserId = result[0].id; - console.log( - `new user with discordId ${discordId} and name ${name} created. id: ${newUserId}`, - ); - return newUserId; - } - - // check again if user is now existing - const newUserId = await this.findUserIdByDiscordId(discordId); - if (newUserId !== null) { - console.log(`user created in parallel. fetched id: ${newUserId}`); - return newUserId; - } - throw new Error(`creating a new user for discordId ${discordId} failed`); - } catch (error) { - console.error("error while creating or calling the user:", error); - throw error; - } - } -} diff --git a/core/src/actions/medication/medication.task.ts b/core/src/actions/medication/medication.task.ts deleted file mode 100644 index 51844df..0000000 --- a/core/src/actions/medication/medication.task.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { CronJob } from "cron"; -import { MedicationService } from "actions/medication/medication.service"; - -const medicationService = new MedicationService(); - -/*new CronJob( - "0 0 19 * * *", // cronTime - async () => { - console.log("askMedication()"); - await medicationService.askMedication(); - }, // onTick - null, // onComplete - true, // start - "Europe/Berlin", // timeZone -);*/ \ No newline at end of file diff --git a/core/src/entities/channels/channels.schema.ts b/core/src/entities/channels/channels.schema.ts index c541f0c..7734430 100644 --- a/core/src/entities/channels/channels.schema.ts +++ b/core/src/entities/channels/channels.schema.ts @@ -3,7 +3,7 @@ import z from "zod"; export const TextChannelOptions = [ "bump", "bot", - "notification", + "log", "testing", "news", "rules", diff --git a/core/src/entities/channels/voice/voice-channels.service.ts b/core/src/entities/channels/voice/voice-channels.service.ts new file mode 100644 index 0000000..bea1635 --- /dev/null +++ b/core/src/entities/channels/voice/voice-channels.service.ts @@ -0,0 +1,14 @@ +export interface VoiceChannelsServiceInterface< + C extends { name: string; id: string }, +> { + getVoiceChannelById(channelId: string): Promise; + + createVoiceChannel(name: string): Promise; + + cloneVoiceChannel( + channel: C, + options?: { name?: string; position?: number }, + ): Promise; + + deleteVoiceChannel(channelId: string): Promise; +} diff --git a/core/src/features/dynamic-voice-channels/dynamic-voice-channels.schema.ts b/core/src/features/dynamic-voice-channels/dynamic-voice-channels.schema.ts new file mode 100644 index 0000000..e9edf22 --- /dev/null +++ b/core/src/features/dynamic-voice-channels/dynamic-voice-channels.schema.ts @@ -0,0 +1,20 @@ +import { VoiceChannels } from "entities/channels/channels.schema"; +import z from "zod"; + +export const DynamicVoiceChannelKeyOptions = [ + VoiceChannels.enum["for-two"], + VoiceChannels.enum["for-three"], + VoiceChannels.enum["for-four"], + VoiceChannels.enum["for-group"], +] as const; + +export const DynamicVoiceChannelKeys = z.enum(DynamicVoiceChannelKeyOptions); + +export const DynamicVoiceChannel = z.object({ + channelId: z.string(), + size: z.number(), + key: DynamicVoiceChannelKeys, +}); + +export const DynamicVoiceChannels = z.array(DynamicVoiceChannel); +export const DynamicVoiceChannelsMap = z.map(z.string(), DynamicVoiceChannel); diff --git a/core/src/features/dynamic-voice-channels/dynamic-voice-channels.service.ts b/core/src/features/dynamic-voice-channels/dynamic-voice-channels.service.ts new file mode 100644 index 0000000..ab3fc31 --- /dev/null +++ b/core/src/features/dynamic-voice-channels/dynamic-voice-channels.service.ts @@ -0,0 +1,62 @@ +import type { VoiceChannelsServiceInterface } from "entities/channels/voice/voice-channels.service"; +import type z from "zod"; +import type { + DynamicVoiceChannels, + DynamicVoiceChannelsMap, +} from "./dynamic-voice-channels.schema"; + +export class DynamicVoiceChannelsService< + C extends { name: string; id: string }, +> { + private voiceChannelsService: VoiceChannelsServiceInterface; + dynamicVoiceChannels: z.output; + validChannelIds = new Set(); + dynamicVoiceChannelsMap: z.output; + createdChannelIdsSet = new Set(); + + constructor( + voiceChannelsService: VoiceChannelsServiceInterface, + dynamicVoiceChannels: z.output, + ) { + this.voiceChannelsService = voiceChannelsService; + this.dynamicVoiceChannels = dynamicVoiceChannels; + + this.dynamicVoiceChannelsMap = new Map( + dynamicVoiceChannels.map((channel) => [channel.channelId, channel]), + ); + + this.validChannelIds = new Set( + dynamicVoiceChannels.map((channel) => channel.channelId), + ); + } + + async createDynamicVoiceChannel(id: string): Promise { + if (this.validChannelIds.has(id)) { + // Channel is one of the dynamic voice channels, create a new one based on it + + const channel = await this.voiceChannelsService.getVoiceChannelById(id); + + if (!channel) { + throw new Error(`Channel with id ${id} not found`); + } + + const newChannel = await this.voiceChannelsService.cloneVoiceChannel( + channel, + { + name: channel.name, + }, + ); + + this.createdChannelIdsSet.add(newChannel.id); + + return newChannel; + } else { + // Channel is not a dynamic voice channel, do nothing + } + } + + deleteDynamicVoiceChannel(channelId: string): Promise { + this.createdChannelIdsSet.delete(channelId); + return this.voiceChannelsService.deleteVoiceChannel(channelId); + } +} diff --git a/core/src/features/text-based-feature/text-based-feature.schema.ts b/core/src/features/text-based-feature/text-based-feature.schema.ts index 93f1add..526754f 100644 --- a/core/src/features/text-based-feature/text-based-feature.schema.ts +++ b/core/src/features/text-based-feature/text-based-feature.schema.ts @@ -5,5 +5,5 @@ export type TextBasedFeatureHandleMessageSend = ( export type TextBasedFeatureInput = { channelId: string; - handleMessageSend: TextBasedFeatureHandleMessageSend; + messagesService: TextBasedFeatureHandleMessageSend; };