begin rewriting first services to be domain agnostic

more rewrites
This commit is contained in:
2026-02-17 22:42:36 +01:00
parent b3766b9584
commit 071fe2f891
45 changed files with 915 additions and 521 deletions

View File

@@ -0,0 +1,4 @@
import { GreetingService } from "@avocadi/bot-core/features/greeting/greeting.service";
import { messagesService } from "entitites/messages/messages.service";
export const greetingsService = new GreetingService(messagesService);

View File

@@ -0,0 +1,128 @@
import type { Roles } from "@avocadi/bot-core/entities/roles/roles.schema";
import { createLogger } from "@avocadi/bot-core/lib/logger";
import { config } from "config";
import type {
GuildMember,
MessageReaction,
PartialMessageReaction,
PartialUser,
User,
} from "discord.js";
import type z from "zod";
export class ReactionRolesService {
private logger = createLogger("ReactionRolesService");
/**
* This method validates if the reaction is on an allowed message for reaction roles.
* @param reaction
* @returns
*/
async validateReaction(reaction: MessageReaction | PartialMessageReaction) {
this.logger.info(
`Validating reaction ${reaction.emoji.name} on message ${reaction.message.id}`,
);
const message = await reaction.message.fetch();
if (!message) {
this.logger.error(`Message with ID ${reaction.message.id} not found.`);
throw new Error("Message not found");
}
if (!config.reactionRoles.allowedMessageIds.includes(message.id)) {
this.logger.error(
`Message with ID ${message.id} is not allowed for reaction roles.`,
);
throw new Error("Message not allowed for reaction roles");
}
return;
}
/**
* Takes a reaction, validates it, and assigns or removes the given role.
* @param reaction
* @param guild
* @param user
* @param role
*/
async handleReaction(
reaction: MessageReaction | PartialMessageReaction,
user: User | PartialUser,
targetRole: z.output<typeof Roles>,
action: "add" | "remove",
) {
const guild = reaction.message.guild;
if (!guild) {
this.logger.error(`Guild not found for message ${reaction.message.id}.`);
throw new Error("Guild not found");
}
await this.validateReaction(reaction);
const role = guild.roles.cache.get(config.roleMapping[targetRole]);
if (!role) {
this.logger.error(`Role ${targetRole} not found in guild ${guild.name}.`);
throw new Error("Role not found");
}
const member = await guild.members.fetch(user.id);
if (!member) {
this.logger.error(
`User with ID ${user.id} not found in guild ${guild.name}.`,
);
throw new Error("User not found");
}
if (member.roles.cache.has(role.id)) {
if (action === "remove") {
this.removeRoleByReaction(reaction, member, targetRole);
} else {
this.logger.info(
`User ${member.user.tag} already has role ${targetRole}. No action taken.`,
);
}
return;
}
if (action === "remove") {
this.logger.info(
`User ${member.user.tag} does not have role ${targetRole}. No action taken.`,
);
return;
}
this.assignRoleByReaction(reaction, member, targetRole);
return;
}
async assignRoleByReaction(
reaction: MessageReaction | PartialMessageReaction,
member: GuildMember,
role: z.output<typeof Roles>,
) {
this.logger.info(
`Assigning role ${role} based on reaction ${reaction.emoji.name} by user ${member.user.tag}`,
);
await member.roles.add(config.roleMapping[role]);
}
async removeRoleByReaction(
reaction: MessageReaction | PartialMessageReaction,
member: GuildMember,
role: z.output<typeof Roles>,
) {
this.logger.info(
`Removing role ${role} based on reaction ${reaction.emoji.name} by user ${member.user.tag}`,
);
await member.roles.remove(config.roleMapping[role]);
}
}

View File

@@ -0,0 +1,41 @@
import type { WaterMeService } from "@avocadi/bot-core/features/water-me/water-me.service";
import {
ActionRowBuilder,
ButtonBuilder,
ButtonStyle,
type CacheType,
type Interaction,
} from "discord.js";
import { waterMeService } from "./water-me.service";
class WaterMeController {
waterMeService: WaterMeService;
constructor() {
this.waterMeService = waterMeService;
}
async handleInteraction(interaction: Interaction<CacheType>) {
const result = this.waterMeService.waterMe();
const moreButton = new ButtonBuilder()
.setCustomId("moreWater")
.setLabel("mehr")
.setStyle(ButtonStyle.Secondary);
const row = new ActionRowBuilder<ButtonBuilder>().addComponents(moreButton);
if (interaction.isChatInputCommand()) {
await interaction.reply({
content: result.reply,
components: [row],
});
} else if (interaction.isButton()) {
await interaction.reply({
content: result.reply,
});
}
}
}
export const waterMeController = new WaterMeController();

View File

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

View File

@@ -0,0 +1,12 @@
import { CronJob } from "cron";
import { waterMeService } from "./water-me.service";
new CronJob(
"0 0 20 * * *", // cronTime
async () => {
await waterMeService.notifyIfThirsty();
}, // onTick
null, // onComplete
true, // start
"Europe/Berlin", // timeZone
);