refactor water me service

remove "text-based-feature"
improve messages service interface
implement more features for fluxer
This commit is contained in:
2026-02-18 18:38:09 +01:00
parent 84b851f60f
commit 0a460800b6
20 changed files with 202 additions and 56 deletions

View File

@@ -3,5 +3,5 @@ import { logger } from "lib/common-logger";
export const handleShutdown = async () => { export const handleShutdown = async () => {
logger.info("bot is shutting down..."); logger.info("bot is shutting down...");
await logChannelService.sendLogMessage("bot is shutting down..."); await logChannelService.sendLogMessage("ich geh schlafen...");
}; };

View File

@@ -1,9 +1,11 @@
import type { MessagesServiceInterface } from "@avocadi/bot-core/entities/messages/messages.service"; import type { MessagesServiceInterface } from "@avocadi/bot-core/entities/messages/messages.service";
import { createLogger } from "@avocadi/bot-core/lib/logger"; import { createLogger } from "@avocadi/bot-core/lib/logger";
import { import {
type Channel,
ChannelType, ChannelType,
type DMChannel, type DMChannel,
type Message, type Message,
type MessagePayload,
type PartialDMChannel, type PartialDMChannel,
type User, type User,
} from "discord.js"; } from "discord.js";
@@ -11,11 +13,11 @@ import { logChannelService } from "features/log-channel/log-channel.service";
import client from "lib/client"; import client from "lib/client";
export class MessagesService export class MessagesService
implements MessagesServiceInterface<User, Message> implements MessagesServiceInterface<User, Message, Channel, MessagePayload>
{ {
private logger = createLogger("MessagesService"); private logger = createLogger("MessagesService");
async sendToUser(userInput: User, message: string): Promise<void> { async sendToUser(userInput: User, message: MessagePayload): Promise<void> {
const user = await client.users.fetch(userInput.id); const user = await client.users.fetch(userInput.id);
if (user) { if (user) {
@@ -47,6 +49,23 @@ export class MessagesService
await logChannelService.sendLogMessage(logMessage); await logChannelService.sendLogMessage(logMessage);
} }
async sendToChannel(
channel: Channel,
createMessageInput: MessagePayload,
): Promise<void> {
const fetchedChannel = await client.channels.fetch(channel.id);
if (fetchedChannel?.isTextBased() && fetchedChannel.isSendable()) {
await fetchedChannel.send(createMessageInput);
} else {
this.logger.error(
`Channel with ID ${channel.id} not found or is not text-based.`,
);
throw new Error("Channel not found or is not text-based.");
}
}
} }
export const messagesService = new MessagesService(); export const messagesService = new MessagesService();

View File

@@ -4,9 +4,7 @@ import {
ButtonBuilder, ButtonBuilder,
type ButtonInteraction, type ButtonInteraction,
ButtonStyle, ButtonStyle,
type CacheType,
type ChatInputCommandInteraction, type ChatInputCommandInteraction,
type Interaction,
} from "discord.js"; } from "discord.js";
import { waterMeService } from "./water-me.service"; import { waterMeService } from "./water-me.service";

View File

@@ -1,8 +1,4 @@
import { WaterMeService } from "@avocadi/bot-core/features/water-me/water-me.service"; import { WaterMeService } from "@avocadi/bot-core/features/water-me/water-me.service";
import { messagesService } from "entitites/messages/messages.service";
export const waterMeService = new WaterMeService({ export const waterMeService = new WaterMeService(messagesService);
channelId: "123",
handleMessageSend: async (message, channelId) => {
console.log(`Sending message to channel ${channelId}: ${message}`);
},
});

View File

@@ -0,0 +1,7 @@
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("ich geh schlafen...");
};

View File

@@ -13,6 +13,7 @@ export const ConfigSchema = z.object({
roleMapping: z.record(Roles, z.string()), roleMapping: z.record(Roles, z.string()),
serverId: z.string(), serverId: z.string(),
version: z.number(), version: z.number(),
commandPrefix: z.string().default("!"),
fluxer: z.object({ fluxer: z.object({
token: z.string(), token: z.string(),
applicationId: z.string(), applicationId: z.string(),

View File

@@ -1,8 +1,8 @@
import type z from "zod"; import type z from "zod";
import type { ConfigSchema } from "./config.schema"; import { ConfigSchema } from "./config.schema";
import env from "./env"; import env from "./env";
export const config: z.output<typeof ConfigSchema> = { const configInput: z.input<typeof ConfigSchema> = {
channelMapping: { channelMapping: {
text: { text: {
bot: "1473270893617315899", bot: "1473270893617315899",
@@ -42,3 +42,5 @@ export const config: z.output<typeof ConfigSchema> = {
applicationId: env.FLUXER_APPLICATION_ID, applicationId: env.FLUXER_APPLICATION_ID,
}, },
}; };
export const config = ConfigSchema.parse(configInput);

View File

@@ -0,0 +1,56 @@
import type { MessagesServiceInterface } from "@avocadi/bot-core/entities/messages/messages.service";
import { createLogger } from "@avocadi/bot-core/lib/logger";
import type {
Channel,
Message,
MessageSendOptions,
User,
} from "@fluxerjs/core";
import { logChannelService } from "features/log-channel/log-channel.service";
import client from "lib/client";
export class MessagesService
implements
MessagesServiceInterface<User, Message, Channel, MessageSendOptions>
{
private logger = createLogger("MessagesService");
async sendToUser(
userInput: User,
createMessageInput: MessageSendOptions,
): Promise<void> {
const user = await client.users.fetch(userInput.id);
if (user) {
await user.send(createMessageInput);
} else {
this.logger.error(`User with ID ${userInput.id} not found.`);
}
}
async sendToChannel(
channel: Channel,
createMessageInput: MessageSendOptions,
): Promise<void> {
const fetchedChannel = await client.channels.fetch(channel.id);
if (fetchedChannel?.isSendable()) {
await fetchedChannel.send(createMessageInput);
} else {
this.logger.error(
`Channel with ID ${channel.id} not found or is not text-based.`,
);
throw new Error("Channel not found or is not text-based.");
}
}
async logMessage(message: Message): Promise<void> {
const logMessage = `<@${message.author.id}> sent a message:\n"${message.content}"`;
await logChannelService.sendLogMessage(logMessage);
}
}
export const messagesService = new MessagesService();

View File

@@ -1,18 +1,22 @@
import { createLogger } from "@avocadi/bot-core/lib/logger";
import { config } from "config"; import { config } from "config";
import client from "lib/client"; import client from "lib/client";
import { logger } from "lib/common-logger";
export class LogChannelService { export class LogChannelService {
private logger = createLogger("LogChannelService");
private logChannelId = config.channelMapping.text.log; private logChannelId = config.channelMapping.text.log;
async getLogChannel() { async getLogChannel() {
this.logger.debug(`fetching log channel with ID: ${this.logChannelId}`);
const logChannel = await client.channels.fetch(this.logChannelId); const logChannel = await client.channels.fetch(this.logChannelId);
if (logChannel.isSendable()) { if (logChannel.isSendable()) {
return logChannel; return logChannel;
} else { } else {
logger.fatal("Log channel not found or is not text-based."); this.logger.fatal("log channel not found or is not text-based.");
throw new Error("Log channel not found or is not text-based"); throw new Error("log channel not found or is not text-based");
} }
} }

View File

@@ -0,0 +1,4 @@
import { WaterMeService } from "@avocadi/bot-core/features/water-me/water-me.service";
import { messagesService } from "entities/messages/messages.service";
export const waterMeService = new WaterMeService(messagesService);

View File

View File

@@ -1,2 +1,3 @@
import "./ready.listener"; import "./ready.listener";
import "./stop.listener"; import "./stop.listener";
import "./messages/messages.listener";

View File

@@ -0,0 +1,37 @@
import { CommandKeys } from "@avocadi/bot-core/entities/commands/commands.schema";
import { Events, type Message } from "@fluxerjs/core";
import { config } from "config";
import { messagesService } from "entities/messages/messages.service";
import client from "lib/client";
import { logger } from "lib/common-logger";
client.on(Events.MessageCreate, async (message: Message) => {
if (
message.channel?.id !== config.channelMapping.text.log &&
message.channel?.id !== config.channelMapping.text.bot
) {
await messagesService.logMessage(message);
}
if (message.content?.startsWith(config.commandPrefix)) {
await messagesService.logMessage(message);
logger.info(
`Command received: ${message.content} from user ${message.author.id}`,
);
const command = message.content
.slice(config.commandPrefix.length)
.trim()
.split(" ")[0];
logger.info(`Parsed command: ${command}`);
const result = CommandKeys.safeParse(command);
if (result.success) {
logger.info(`Command ${command} is valid.`);
} else {
logger.warn(`Command ${command} is not recognized.`);
}
}
});

View File

@@ -1,3 +1,15 @@
import { Events } from "@fluxerjs/core"; import { handleShutdown } from "actions/shutdown";
import client from "lib/client";
import { logger } from "lib/common-logger"; process.on("exit", async () => {
await handleShutdown();
});
process.on("SIGINT", async () => {
await handleShutdown();
process.exit(0);
});
process.on("SIGTERM", async () => {
await handleShutdown();
process.exit(0);
});

View File

@@ -43,9 +43,8 @@
"./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.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/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/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",
"./features/water-me/water-me.service": "./dist/features/water-me/water-me.service.js", "./features/water-me/water-me.service": "./dist/features/water-me/water-me.service.js",
"./lib/common": "./dist/lib/common.js",
"./lib/logger": "./dist/lib/logger.js", "./lib/logger": "./dist/lib/logger.js",
"./lib/utils": "./dist/lib/utils.js", "./lib/utils": "./dist/lib/utils.js",
"./lib/utils.test": "./dist/lib/utils.test.js", "./lib/utils.test": "./dist/lib/utils.test.js",

View File

@@ -1,5 +1,19 @@
export interface MessagesServiceInterface<U = unknown, M = unknown> { import type {
sendToUser(user: U, message: string): Promise<void>; BaseChannel,
BaseCreateMessage,
BaseMessage,
BaseUser,
} from "lib/common";
export interface MessagesServiceInterface<
U extends BaseUser = BaseUser,
M extends BaseMessage = BaseMessage,
C extends BaseChannel = BaseChannel,
CM extends BaseCreateMessage = BaseCreateMessage,
> {
sendToUser(user: U, createMessageInput: CM): Promise<void>;
sendToChannel(channel: C, createMessageInput: CM): Promise<void>;
logMessage(message: M): Promise<void>; logMessage(message: M): Promise<void>;
} }

View File

@@ -1,9 +0,0 @@
export type TextBasedFeatureHandleMessageSend = (
message: string,
channelId: string,
) => void | Promise<void>;
export type TextBasedFeatureInput = {
channelId: string;
messagesService: TextBasedFeatureHandleMessageSend;
};

View File

@@ -1,18 +0,0 @@
import type {
TextBasedFeatureHandleMessageSend,
TextBasedFeatureInput,
} from "./text-based-feature.schema";
export class TextBasedFeature {
channelId: string;
handleMessageSend: TextBasedFeatureHandleMessageSend;
constructor(input: TextBasedFeatureInput) {
this.channelId = input.channelId;
this.handleMessageSend = input.handleMessageSend;
}
async sendMessage(input: { content: string }) {
this.handleMessageSend(input.content, this.channelId);
}
}

View File

@@ -1,19 +1,19 @@
import { TextBasedFeature } from "features/text-based-feature/text-based-feature"; import type { MessagesServiceInterface } from "entities/messages/messages.service";
import type { TextBasedFeatureInput } from "features/text-based-feature/text-based-feature.schema";
import { createLogger } from "lib/logger"; import { createLogger } from "lib/logger";
import { getRandomInt } from "lib/utils"; import { getRandomInt } from "lib/utils";
export class WaterMeService extends TextBasedFeature { export class WaterMeService {
waterLevel: number; waterLevel: number;
private logger = createLogger("WaterMeService"); private logger = createLogger("WaterMeService");
private thirsty = 3 as const; private thirsty = 3 as const;
private enough = 10 as const; private enough = 10 as const;
messagesService: MessagesServiceInterface;
constructor(input: TextBasedFeatureInput) { constructor(messagesService: MessagesServiceInterface) {
super(input);
this.waterLevel = 0; this.waterLevel = 0;
this.messagesService = messagesService;
} }
getReply() { getReply() {
@@ -47,7 +47,10 @@ export class WaterMeService extends TextBasedFeature {
async notifyIfThirsty() { async notifyIfThirsty() {
if (this.waterLevel <= this.thirsty) { if (this.waterLevel <= this.thirsty) {
await this.sendMessage({ content: "ich brauche wasser :(" }); await this.messagesService.sendToChannel(
{ id: "channelId" },
{ content: "ich brauche wasser :(" },
);
} }
} }

20
core/src/lib/common.ts Normal file
View File

@@ -0,0 +1,20 @@
export type BaseMessage = {
content: string;
};
export type BaseChannel = {
id: string;
};
export type BaseUser = {
id: string;
};
export type BaseCreateMessage =
| {
content?: string;
embeds?: unknown[] | null;
attachments?: unknown[] | null;
files?: unknown[] | null;
}
| string;