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

@@ -21,6 +21,8 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
.env.test.local
.env.production.local
.env.local
.env.dev
.env.prod
# caches
.eslintcache

View File

@@ -1 +0,0 @@
console.log("Hello via Bun!");

View File

@@ -1,16 +1,30 @@
{
"name": "@avocadi/bot-adapter-discord",
"module": "index.ts",
"type": "module",
"private": true,
"scripts": {
"build": "tsdown",
"dev:prod": "NODE_ENV=production tsdown --watch & node --watch ./dist/index.js",
"dev": "NODE_ENV=development tsdown --watch & node --watch ./dist/index.js"
},
"devDependencies": {
"@types/bun": "latest"
"@types/bun": "latest",
"tsdown": "catalog:"
},
"peerDependencies": {
"typescript": "^5"
},
"dependencies": {
"zod": "catalog:",
"@avocadi/bot-core": "workspace:*"
"@avocadi/bot-core": "workspace:*",
"@discordjs/rest": "^2.6.0",
"cron": "^4.4.0",
"discord.js": "^14.25.1",
"dotenv": "^17.3.1",
"dotenv-expand": "^12.0.3",
"zod": "catalog:"
},
"exports": {
".": "./dist/index.js",
"./package.json": "./package.json"
}
}

View File

@@ -0,0 +1,18 @@
import { config } from "config";
import { Routes } from "discord.js";
import getCommands from "entitites/commands";
import { discordRestClient } from "lib/rest-client";
export const publishCommands = async () => {
try {
await discordRestClient.put(
Routes.applicationCommands(config.discord.applicationId),
{
body: getCommands(),
},
);
console.log("Successfully added commands");
} catch (e) {
console.error(e);
}
};

View File

@@ -11,6 +11,9 @@ export const ConfigSchema = z.object({
voice: z.record(VoiceChannels, z.string()),
}),
roleMapping: z.record(Roles, z.string()),
reactionRoles: z.object({
allowedMessageIds: z.array(z.string()),
}),
serverId: z.string(),
version: z.number(),
discord: z.object({

View File

@@ -0,0 +1,6 @@
import z from "zod";
export const EnvSchema = z.object({
DISCORD_APPLICATION_ID: z.string(),
DISCORD_TOKEN: z.string(),
});

View File

@@ -0,0 +1,29 @@
import { readFileSync } from "node:fs";
import { join } from "node:path";
import dotenv from "dotenv";
import dotenvExpand from "dotenv-expand";
import { EnvSchema } from "./env.schema";
const envFile =
process.env.NODE_ENV === "production" ? ".env.prod" : ".env.dev";
const envPath = join(process.cwd(), envFile);
const rawEnv = Buffer.from(
readFileSync(envPath, {
encoding: "utf8",
}),
);
const envJson = { processEnv: dotenv.parse(rawEnv) };
const targetObj = {};
const expandedEnv = dotenvExpand.expand({
processEnv: targetObj,
parsed: envJson.processEnv,
}) as { processEnv: Record<string, string> };
const env = EnvSchema.parse(expandedEnv.processEnv);
export default env;

View File

@@ -1,5 +1,6 @@
import type z from "zod";
import type { ConfigSchema } from "./config.schema";
import env from "./env";
export const config: z.output<typeof ConfigSchema> = {
channelMapping: {
@@ -34,10 +35,15 @@ export const config: z.output<typeof ConfigSchema> = {
people: "1321470720424939662",
bot: "1321491461111283722",
},
reactionRoles: {
allowedMessageIds: [
"1321491461111283722", // Example message ID for reaction roles
],
},
serverId: "1316153371899592774",
version: 1,
discord: {
token: process.env.DISCORD_TOKEN || "",
applicationId: process.env.DISCORD_APPLICATION_ID || "",
token: env.DISCORD_TOKEN,
applicationId: env.DISCORD_APPLICATION_ID,
},
};

View File

@@ -1,74 +1,16 @@
import { Commands, type CommandsType } from "commands";
import {
ChannelType,
Client,
Events,
IntentsBitField,
type VoiceState,
type ButtonInteraction,
type CacheType,
type ChatInputCommandInteraction,
type Interaction,
type ModalSubmitInteraction,
ActivityType,
type Channel,
} from "discord.js";
import client from "lib/client";
import EventEmitter from "node:events";
import DiscordService from "discord.service";
import { WaterMeService } from "actions/waterMe/waterMe.service";
import { MedicationService } from "actions/medication/medication.service";
import { HelpService } from "actions/help/help.service";
import { SupportService } from "actions/support/support.service";
import { GreetingService } from "actions/greeting/greeting.service";
import { ActivityService } from "actions/activity/activity.service";
import { DmService } from "actions/dm/dm.service";
import { CustomMessageService } from "actions/customMessage/customMessage.service";
import { DynamicVChannelService } from "actions/dynamicVChannel/dynamicVChannel.service";
import { DebugService } from "actions/debug/debug.service";
import { ReactRolesService } from "actions/reactRole/reactRoles.service";
import config from "config";
import {
type WaterMeController,
waterMeController,
} from "features/water-me/water-me.controller";
import client from "lib/client";
export default class DiscordController extends EventEmitter {
private discordService: DiscordService;
private waterMeService: WaterMeService;
private greetingService: GreetingService;
private medicationService: MedicationService;
private helpService: HelpService;
private supportService: SupportService;
private activityService: ActivityService;
private dmService: DmService;
private customMessageService: CustomMessageService;
private dynamicVChannelService: DynamicVChannelService;
private debugService: DebugService;
private channelListeners = new Map();
private reactRolesService: ReactRolesService;
private waterMeController: WaterMeController = waterMeController;
constructor() {
super();
let channelListeners = new Map();
this.discordService = new DiscordService();
this.waterMeService = new WaterMeService();
this.greetingService = new GreetingService();
this.medicationService = new MedicationService();
this.helpService = new HelpService();
this.supportService = new SupportService();
this.activityService = new ActivityService();
this.dmService = new DmService();
this.customMessageService = new CustomMessageService();
this.dynamicVChannelService = new DynamicVChannelService();
this.debugService = new DebugService();
this.reactRolesService = new ReactRolesService();
client.on("messageReactionAdd", async (reaction, user) => {
await this.reactRolesService.roleMention(reaction, user, true);
});
client.on("messageReactionRemove", async (reaction, user) => {
await this.reactRolesService.roleMention(reaction, user, false);
});
// log when running
client.once("ready", async () => {
@@ -146,7 +88,8 @@ export default class DiscordController extends EventEmitter {
return;
}
try {
const newChannel = await this.dynamicVChannelService.createVChannel(
const newChannel =
await this.dynamicVChannelService.createVChannel(
newState,
channel,
);
@@ -177,7 +120,9 @@ export default class DiscordController extends EventEmitter {
}
},
);
console.log(`----------------\nversion ${config.discord.version}\n----------------`);
console.log(
`----------------\nversion ${config.discord.version}\n----------------`,
);
}
async setActivity(state: number) {
@@ -213,7 +158,6 @@ export default class DiscordController extends EventEmitter {
console.log("bot is offline.");
}
async init() {
await this.discordService.init();
}

View File

@@ -0,0 +1,32 @@
import { CommandsCollection } from "@avocadi/bot-core/entities/commands/commands.entity";
import type { Command } from "@avocadi/bot-core/entities/commands/commands.schema";
import { SlashCommandBuilder } from "discord.js";
import type { z } from "zod";
const convertCommandToDiscordFormat = (
command: z.output<typeof Command>,
key: string,
) => {
const slashCommand = new SlashCommandBuilder()
.setName(command.name || key)
.setDescription(command.description);
if (command.options) {
command.options.forEach((option) => {
slashCommand.addStringOption((opt) =>
opt
.setName(option.name)
.setDescription(option.description)
.setRequired(option.required),
);
});
}
return slashCommand;
};
export default function getCommands() {
return Object.entries(CommandsCollection).map(([key, command]) =>
convertCommandToDiscordFormat(command, key),
);
}

View File

@@ -0,0 +1,20 @@
import type { MessagesServiceInterface } from "@avocadi/bot-core/entities/messages/messages.service";
import { createLogger } from "@avocadi/bot-core/lib/logger";
import type { User } from "discord.js";
import client from "lib/client";
export class MessagesService implements MessagesServiceInterface<User> {
private logger = createLogger("MessagesService");
async sendToUser(userInput: User, message: string): Promise<void> {
const user = await client.users.fetch(userInput.id);
if (user) {
await user.send(message);
} else {
this.logger.error(`User with ID ${userInput.id} not found.`);
}
}
}
export const messagesService = new MessagesService();

View File

@@ -0,0 +1,33 @@
import type { RolesServiceInterface } from "@avocadi/bot-core/entities/roles/roles.service";
import { createLogger } from "@avocadi/bot-core/lib/logger";
import type { GuildMember } from "discord.js";
export class RolesService implements RolesServiceInterface<GuildMember> {
private logger = createLogger("RolesService");
async assignRole(user: GuildMember, role: string) {
const roleToAssign = user.guild.roles.cache.find((r) => r.name === role);
if (!roleToAssign) {
this.logger.error(`Role ${role} not found in guild ${user.guild.name}.`);
return;
}
await user.roles.add(roleToAssign);
}
async removeRole(user: GuildMember, role: string) {
const roleToRemove = user.guild.roles.cache.find((r) => r.name === role);
if (!roleToRemove) {
this.logger.error(`Role ${role} not found in guild ${user.guild.name}.`);
return;
}
await user.roles.remove(roleToRemove);
}
async getRoles(user: GuildMember) {
return user.roles.cache.map((role) => role.name);
}
async hasRole(user: GuildMember, role: string) {
return user.roles.cache.some((r) => r.name === role);
}
}

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
);

View File

@@ -0,0 +1,4 @@
import { publishCommands } from "actions/publish-commands";
// Publish commands when the application starts
await publishCommands();

View File

@@ -0,0 +1,24 @@
import { config } from "config";
import { Client, IntentsBitField, Partials } from "discord.js";
const client = new Client({
intents: [
IntentsBitField.Flags.Guilds,
IntentsBitField.Flags.GuildMembers,
IntentsBitField.Flags.GuildModeration,
IntentsBitField.Flags.GuildMessages,
IntentsBitField.Flags.GuildMessageReactions,
IntentsBitField.Flags.GuildMessagePolls,
IntentsBitField.Flags.GuildVoiceStates,
IntentsBitField.Flags.MessageContent,
IntentsBitField.Flags.DirectMessages,
IntentsBitField.Flags.DirectMessageReactions,
IntentsBitField.Flags.DirectMessageTyping,
IntentsBitField.Flags.DirectMessagePolls,
],
partials: [Partials.Channel, Partials.Message, Partials.Reaction],
});
await client.login(config.discord.token);
export default client;

View File

@@ -0,0 +1,6 @@
import { REST } from "@discordjs/rest";
import { config } from "config";
export const discordRestClient = new REST({ version: "10" }).setToken(
config.discord.token,
);

View File

@@ -0,0 +1,12 @@
import { greetingsService } from "features/greeting/greetings.service";
import client from "lib/client";
client.on("guildMemberAdd", async (member) => {
if (member.user.bot) {
// Don't send a welcome message for bots (sorry tom)
return;
}
greetingsService.sendGreeting(member.user, member.user.username);
});

View File

@@ -0,0 +1,20 @@
import { Roles } from "@avocadi/bot-core/entities/roles/roles.schema";
import { ReactionRolesService } from "features/reaction-roles/reaction-roles.service";
import client from "lib/client";
const reactionRolesService = new ReactionRolesService();
// Currently only used for the "people" role, but can be extended to handle multiple roles based on the reaction emoji
client.on("messageReactionAdd", async (reaction, user) => {
reactionRolesService.handleReaction(reaction, user, Roles.enum.people, "add");
});
client.on("messageReactionRemove", async (reaction, user) => {
reactionRolesService.handleReaction(
reaction,
user,
Roles.enum.people,
"remove",
);
});

View File

@@ -0,0 +1,6 @@
import { Events } from "discord.js";
import client from "lib/client";
client.on(Events.VoiceStateUpdate, async (oldState, newState) => {
// TODO: handle updates
});

View File

@@ -21,6 +21,8 @@
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
"baseUrl": "src",
// Some stricter flags (disabled by default)
"noUnusedLocals": false,
"noUnusedParameters": false,

View File

@@ -0,0 +1,9 @@
import { defineConfig } from "tsdown";
export default defineConfig({
entry: ["./src/index.ts"],
format: "esm",
dts: true,
exports: true,
fixedExtension: false,
});

View File

@@ -11,10 +11,16 @@
"name": "@avocadi/bot-adapter-discord",
"dependencies": {
"@avocadi/bot-core": "workspace:*",
"@discordjs/rest": "^2.6.0",
"cron": "^4.4.0",
"discord.js": "^14.25.1",
"dotenv": "^17.3.1",
"dotenv-expand": "^12.0.3",
"zod": "catalog:",
},
"devDependencies": {
"@types/bun": "latest",
"tsdown": "catalog:",
},
"peerDependencies": {
"typescript": "^5",
@@ -37,6 +43,7 @@
"discord.js": "^14.16.3",
"dotenv": "^16.4.7",
"drizzle-orm": "^0.38.3",
"tslog": "^4.10.2",
"zod": "catalog:",
},
"devDependencies": {
@@ -221,7 +228,7 @@
"@types/jsesc": ["@types/jsesc@2.5.1", "", {}, "sha512-9VN+6yxLOPLOav+7PwjZbxiID2bVaeq0ED4qSQmdQTdjnXJSaCVKTR58t15oqH1H5t8Ng2ZX1SabJVoN9Q34bw=="],
"@types/luxon": ["@types/luxon@3.4.2", "", {}, "sha512-TifLZlFudklWlMBfhubvgqTXRzLDI5pCbGa4P8a3wPyUQSW+1xQ5eDsreP9DWHX3tjq1ke96uYG/nwundroWcA=="],
"@types/luxon": ["@types/luxon@3.7.1", "", {}, "sha512-H3iskjFIAn5SlJU7OuxUmTEpebK6TKB8rxZShDslBMZJ5u9S//KM1sbdAisiSrqwLQncVjnpi2OK2J51h+4lsg=="],
"@types/node": ["@types/node@25.2.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-m0jEgYlYz+mDJZ2+F4v8D1AyQb+QzsNqRuI7xg1VQX/KlKS0qT9r1Mo16yo5F/MtifXFgaofIFsdFMox2SxIbQ=="],
@@ -253,7 +260,7 @@
"chownr": ["chownr@1.1.4", "", {}, "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="],
"cron": ["cron@3.5.0", "", { "dependencies": { "@types/luxon": "~3.4.0", "luxon": "~3.5.0" } }, "sha512-0eYZqCnapmxYcV06uktql93wNWdlTmmBFP2iYz+JPVcQqlyFYcn1lFuIk4R54pkOmE7mcldTAPZv6X5XA4Q46A=="],
"cron": ["cron@4.4.0", "", { "dependencies": { "@types/luxon": "~3.7.0", "luxon": "~3.7.0" } }, "sha512-fkdfq+b+AHI4cKdhZlppHveI/mgz2qpiYxcm+t5E5TsxX7QrLS1VE0+7GENEk9z0EeGPcpSciGv6ez24duWhwQ=="],
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
@@ -269,7 +276,9 @@
"discord.js": ["discord.js@14.25.1", "", { "dependencies": { "@discordjs/builders": "^1.13.0", "@discordjs/collection": "1.5.3", "@discordjs/formatters": "^0.6.2", "@discordjs/rest": "^2.6.0", "@discordjs/util": "^1.2.0", "@discordjs/ws": "^1.2.3", "@sapphire/snowflake": "3.5.3", "discord-api-types": "^0.38.33", "fast-deep-equal": "3.1.3", "lodash.snakecase": "4.1.1", "magic-bytes.js": "^1.10.0", "tslib": "^2.6.3", "undici": "6.21.3" } }, "sha512-2l0gsPOLPs5t6GFZfQZKnL1OJNYFcuC/ETWsW4VtKVD/tg4ICa9x+jb9bkPffkMdRpRpuUaO/fKkHCBeiCKh8g=="],
"dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="],
"dotenv": ["dotenv@17.3.1", "", {}, "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA=="],
"dotenv-expand": ["dotenv-expand@12.0.3", "", { "dependencies": { "dotenv": "^16.4.5" } }, "sha512-uc47g4b+4k/M/SeaW1y4OApx+mtLWl92l5LMPP0GNXctZqELk+YGgOPIIC5elYmUH4OuoK3JLhuRUYegeySiFA=="],
"drizzle-kit": ["drizzle-kit@0.30.6", "", { "dependencies": { "@drizzle-team/brocli": "^0.10.2", "@esbuild-kit/esm-loader": "^2.5.5", "esbuild": "^0.19.7", "esbuild-register": "^3.5.0", "gel": "^2.0.0" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-U4wWit0fyZuGuP7iNmRleQyK2V8wCuv57vf5l3MnG4z4fzNTjY/U13M8owyQ5RavqvqxBifWORaR3wIUzlN64g=="],
@@ -323,7 +332,7 @@
"lodash.snakecase": ["lodash.snakecase@4.1.1", "", {}, "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw=="],
"luxon": ["luxon@3.5.0", "", {}, "sha512-rh+Zjr6DNfUYR3bPwJEnuwDdqMbxZW7LOQfUN4B54+Cl+0o5zaU9RJ6bcidfDtC1cWCZXQ+nvX8bf6bAji37QQ=="],
"luxon": ["luxon@3.7.2", "", {}, "sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew=="],
"magic-bytes.js": ["magic-bytes.js@1.13.0", "", {}, "sha512-afO2mnxW7GDTXMm5/AoN1WuOcdoKhtgXjIvHmobqTD1grNplhGdv3PFOyjCVmrnOZBIT/gD/koDKpYG+0mvHcg=="],
@@ -397,6 +406,8 @@
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"tslog": ["tslog@4.10.2", "", {}, "sha512-XuELoRpMR+sq8fuWwX7P0bcj+PRNiicOKDEb3fGNURhxWVyykCi9BNq7c4uVz7h7P0sj8qgBsr5SWS6yBClq3g=="],
"tunnel-agent": ["tunnel-agent@0.6.0", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w=="],
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
@@ -419,12 +430,22 @@
"zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="],
"@avocadi/bot-core/cron": ["cron@3.5.0", "", { "dependencies": { "@types/luxon": "~3.4.0", "luxon": "~3.5.0" } }, "sha512-0eYZqCnapmxYcV06uktql93wNWdlTmmBFP2iYz+JPVcQqlyFYcn1lFuIk4R54pkOmE7mcldTAPZv6X5XA4Q46A=="],
"@avocadi/bot-core/dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="],
"@esbuild-kit/core-utils/esbuild": ["esbuild@0.18.20", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="],
"discord.js/@discordjs/collection": ["@discordjs/collection@1.5.3", "", {}, "sha512-SVb428OMd3WO1paV3rm6tSjM4wC+Kecaa1EUGX7vc6/fddvw/6lg90z4QtCqm21zvVe92vMMDt9+DkIvjXImQQ=="],
"discord.js/@sapphire/snowflake": ["@sapphire/snowflake@3.5.3", "", {}, "sha512-jjmJywLAFoWeBi1W7994zZyiNWPIiqRRNAmSERxyg93xRGzNYvGjlZ0gR6x0F4gPRi2+0O6S71kOZYyr3cxaIQ=="],
"dotenv-expand/dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="],
"@avocadi/bot-core/cron/@types/luxon": ["@types/luxon@3.4.2", "", {}, "sha512-TifLZlFudklWlMBfhubvgqTXRzLDI5pCbGa4P8a3wPyUQSW+1xQ5eDsreP9DWHX3tjq1ke96uYG/nwundroWcA=="],
"@avocadi/bot-core/cron/luxon": ["luxon@3.5.0", "", {}, "sha512-rh+Zjr6DNfUYR3bPwJEnuwDdqMbxZW7LOQfUN4B54+Cl+0o5zaU9RJ6bcidfDtC1cWCZXQ+nvX8bf6bAji37QQ=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.18.20", "", { "os": "android", "cpu": "arm" }, "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.18.20", "", { "os": "android", "cpu": "arm64" }, "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ=="],

View File

@@ -21,6 +21,7 @@
"discord.js": "^14.16.3",
"dotenv": "^16.4.7",
"drizzle-orm": "^0.38.3",
"tslog": "^4.10.2",
"zod": "catalog:"
},
"trustedDependencies": [
@@ -32,8 +33,17 @@
"./db": "./dist/db/index.js",
"./db/schema": "./dist/db/schema.js",
"./entities/channels/channels.schema": "./dist/entities/channels/channels.schema.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",
"./lib/client": "./dist/lib/client.js",
"./entities/roles/roles.service": "./dist/entities/roles/roles.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",
"./lib/logger": "./dist/lib/logger.js",
"./lib/utils": "./dist/lib/utils.js",
"./lib/utils.test": "./dist/lib/utils.test.js",
"./package.json": "./package.json"

View File

@@ -1,19 +1,18 @@
import config from "config";
import client from "lib/client";
import { getRandomInt } from "lib/utils";
import { dmWelcomeContent, dmAcceptedContent } from "./dm.components.ts";
import {
type CacheType,
Client,
EmbedBuilder,
type Message,
type CacheType,
type GuildMember,
type Interaction,
type Message,
type OmitPartialGroupDMChannel,
} from "discord.js";
import client from "lib/client";
import { getRandomInt } from "lib/utils";
import { dmAcceptedContent, dmWelcomeContent } from "./dm.components.ts";
export class DmService {
async handleInteraction(interaction: Interaction<CacheType>) {
// todo
}
@@ -21,13 +20,15 @@ export class DmService {
async reminderPrivate(member: GuildMember) {
console.log("reminder");
try {
const content = `hey, kleine erinnerung :)\nbitte stelle dich auf <#${config.discord.channelIdIntroduction}> vor, damit du alle kanaele ansehen und nutzen kannst! <3`
const content = `hey, kleine erinnerung :)\nbitte stelle dich auf <#${config.discord.channelIdIntroduction}> vor, damit du alle kanaele ansehen und nutzen kannst! <3`;
await client.users.send(member, content);
} catch (error) {
const channels = client.channels;
const channel = channels.cache.get(config.discord.channelIdLog);
if (channel?.isTextBased() && channel?.isSendable()) {
await channel.send(`konnte keine private nachricht an <@${member.user.id}> senden`);
await channel.send(
`konnte keine private nachricht an <@${member.user.id}> senden`,
);
}
console.error("error while sending a welcome msg:", error);
}
@@ -41,7 +42,9 @@ export class DmService {
const channels = client.channels;
const channel = channels.cache.get(config.discord.channelIdLog);
if (channel?.isTextBased() && channel?.isSendable()) {
await channel.send(`konnte keine private nachricht an <@${member.user.id}> senden`);
await channel.send(
`konnte keine private nachricht an <@${member.user.id}> senden`,
);
}
console.error("error while sending a welcome msg:", error);
}
@@ -56,8 +59,7 @@ export class DmService {
if (message.author.bot) {
context = `<@${author}> hat an <@${recipient}> geschrieben:\n"${message.content}"`;
}
else {
} else {
context = `<@${author}> hat geschrieben:\n"${message.content}"`;
}
@@ -81,7 +83,9 @@ export class DmService {
const channels = client.channels;
const channel = channels.cache.get(config.discord.channelIdLog);
if (channel?.isTextBased() && channel?.isSendable()) {
await channel.send(`konnte keine private nachricht an <@${member.user.id}> senden`);
await channel.send(
`konnte keine private nachricht an <@${member.user.id}> senden`,
);
}
console.error("error while sending a accept msg:", error);
}
@@ -90,13 +94,15 @@ export class DmService {
async roleMentionDm(member: GuildMember, add: boolean) {
console.log("rolementionadd dm");
try {
const contentRoleMentionDm = `du hast die rolle **streber:in** erfolgreich ** *${(add ? "zugeteilt" : "entfernt")}* ** bekommen :3 <#${config.discord.channelIdOffTopic}> <:avocadi_cute:1321893797138923602>`;
const contentRoleMentionDm = `du hast die rolle **streber:in** erfolgreich ** *${add ? "zugeteilt" : "entfernt"}* ** bekommen :3 <#${config.discord.channelIdOffTopic}> <:avocadi_cute:1321893797138923602>`;
client.users.send(member, contentRoleMentionDm);
} catch (error) {
const channels = client.channels;
const channel = channels.cache.get(config.discord.channelIdLog);
if (channel?.isTextBased() && channel?.isSendable()) {
await channel.send(`konnte keine private nachricht an <@${member.user.id}> senden`);
await channel.send(
`konnte keine private nachricht an <@${member.user.id}> senden`,
);
}
console.error("error while sending a accept msg:", error);
}

View File

@@ -1,24 +1,21 @@
import { DmService } from "actions/dm/dm.service.ts";
import { Commands, type CommandsType } from "commands/index.ts";
import config from "config";
import {
type CacheType,
type ChatInputCommandInteraction,
type GuildMember,
GuildMemberRoleManager,
type Interaction,
} from "discord.js";
import client from "lib/client";
import { getRandomInt } from "lib/utils";
import { checkPermission } from "permissions/index.ts";
import {
getWelcomeContent,
greetContent,
sleepContent,
} from "./greeting.components.ts";
import {
type ChatInputCommandInteraction,
Client,
EmbedBuilder,
type CacheType,
type GuildMember,
type Interaction,
GuildMemberRoleManager,
type APIInteractionGuildMember,
} from "discord.js";
import { DmService } from "actions/dm/dm.service.ts";
import { Commands, type CommandsType } from "commands/index.ts";
import { checkPermission } from "permissions/index.ts";
export class GreetingService {
dmService: DmService;
@@ -27,17 +24,15 @@ export class GreetingService {
this.dmService = new DmService();
}
async handleInteraction(
interaction: Interaction<CacheType>
) {
async handleInteraction(interaction: Interaction<CacheType>) {
if (interaction.isChatInputCommand()) {
await this.handleChatInputCommand(interaction);
return;
}
}
async handleChatInputCommand(interaction: ChatInputCommandInteraction<CacheType>) {
async handleChatInputCommand(
interaction: ChatInputCommandInteraction<CacheType>,
) {
const commandName = interaction.commandName as CommandsType;
switch (commandName) {
case Commands.Enum.accept:
@@ -72,9 +67,7 @@ export class GreetingService {
}
}
async acceptUser(
interaction: ChatInputCommandInteraction<CacheType>
) {
async acceptUser(interaction: ChatInputCommandInteraction<CacheType>) {
console.log("accept user");
// get the string option
@@ -83,7 +76,7 @@ export class GreetingService {
//console.log(input);
// permission check
if (await checkPermission(interaction.member) !== true) {
if ((await checkPermission(interaction.member)) !== true) {
await interaction.reply({
content: "du hast keine rechte fuer diesen befehl",
ephemeral: true,
@@ -108,7 +101,7 @@ export class GreetingService {
const username = member.user.username;
console.log(username);
if (await this.checkRole(member) === true) {
if ((await this.checkRole(member)) === true) {
await interaction.reply({
content: `${member.user.username} hat die rolle *lernende:r* schon!`,
ephemeral: true,
@@ -192,8 +185,7 @@ export class GreetingService {
return greetContent[getRandomInt(0, greetContent.length - 1)];
}
async reminderCommand(
interaction: ChatInputCommandInteraction<CacheType>) {
async reminderCommand(interaction: ChatInputCommandInteraction<CacheType>) {
console.log("remind user");
// get the string option
@@ -203,7 +195,7 @@ export class GreetingService {
try {
// permission check
if (await checkPermission(interaction.member) !== true) {
if ((await checkPermission(interaction.member)) !== true) {
await interaction.reply({
content: "du hast keine rechte fuer diesen befehl",
ephemeral: true,
@@ -227,7 +219,7 @@ export class GreetingService {
const username = member.user.username;
console.log(username);
if (await this.checkRole(member) === true) {
if ((await this.checkRole(member)) === true) {
await interaction.reply({
content: `${member.user.username} hat die rolle *lernende:r* schon!`,
ephemeral: true,
@@ -252,9 +244,7 @@ export class GreetingService {
}
}
async welcomeCommand(
interaction: ChatInputCommandInteraction<CacheType>
) {
async welcomeCommand(interaction: ChatInputCommandInteraction<CacheType>) {
console.log("welcome user");
// get the string option
@@ -275,7 +265,7 @@ export class GreetingService {
return;
}
if (await checkPermission(interaction.member) !== true) {
if ((await checkPermission(interaction.member)) !== true) {
await interaction.reply({
content: "du hast keine rechte fuer diesen befehl",
ephemeral: true,
@@ -290,7 +280,7 @@ export class GreetingService {
const welcomeContent = getWelcomeContent(member);
if (await this.checkRole(member) === true) {
if ((await this.checkRole(member)) === true) {
await interaction.reply({
content: `${member.user.username} wurde schon begruesst!`,
ephemeral: true,
@@ -306,7 +296,6 @@ export class GreetingService {
}
await this.dmService.welcomePrivate(member);
} catch (error) {
console.error("error while sending a welcome command msg:", error);
}
@@ -315,12 +304,10 @@ export class GreetingService {
content: `erfolgreich welcome command: ${member.user.username}`,
ephemeral: true,
});
} catch (error) {
console.error("fehler bei welcome command", error);
await interaction.reply({
content:
"fehler bei welcome command",
content: "fehler bei welcome command",
ephemeral: true,
});
}
@@ -329,7 +316,10 @@ export class GreetingService {
async checkRole(member: GuildMember) {
let hasRole = false;
if (member?.roles instanceof GuildMemberRoleManager) {
if (member.roles.cache.has(config.discord.roleAdmin) || member.roles.cache.has(config.discord.roleMod)) {
if (
member.roles.cache.has(config.discord.roleAdmin) ||
member.roles.cache.has(config.discord.roleMod)
) {
console.log("user has permission");
hasRole = true;
}

View File

@@ -1,99 +0,0 @@
import { CronJob } from "cron";
import { getRandomInt } from "lib/utils";
import config from "config";
import client from "lib/client";
import {
ActionRowBuilder,
ButtonBuilder,
ButtonStyle,
type CacheType,
type Interaction,
} from "discord.js";
export class WaterMeService {
waterLevel: number;
private thirsty = 3 as const;
private enough = 10 as const;
constructor() {
this.waterLevel = 0;
}
getReply() {
const thirstyReplies = [
"... wow das wars schon??? ich brauche noch mehr wasser :(",
"dankeeeee!!!! ich waer fast verdurstet :(((",
"*roelpssssss*",
];
const fullReplies = [
"langsam reicht es :o",
"poah, das hat gut getan",
"das ist krass :3",
];
const tooMuchReplies = [
"ES REICHT!!!!",
"bitte hoer auf, ich platze gleich :(",
];
if (this.waterLevel <= this.thirsty) {
return thirstyReplies[getRandomInt(0, thirstyReplies.length - 1)];
}
if (this.waterLevel > this.thirsty && this.waterLevel <= this.enough) {
return fullReplies[getRandomInt(0, fullReplies.length - 1)];
}
if (this.waterLevel > this.enough) {
return tooMuchReplies[getRandomInt(0, tooMuchReplies.length - 1)];
}
}
async isThirsty() {
const channels = client.channels;
const channel = channels.cache.get(config.discord.channelIdBot);
if (
channel?.isTextBased &&
channel?.isSendable() &&
this.waterLevel <= this.thirsty
) {
await channel.send({ content: "ich brauche wasser :(" });
}
}
waterMe() {
const reply = this.getReply();
this.waterLevel++;
console.log(this.waterLevel);
return {
reply,
};
}
async handleInteraction(interaction: Interaction<CacheType>) {
const result = this.waterMe();
const moreButton = new ButtonBuilder()
.setCustomId("moreWater")
.setLabel("mehr")
.setStyle(ButtonStyle.Secondary);
const row = new ActionRowBuilder().addComponents(moreButton);
if (interaction.isChatInputCommand()) {
await interaction.reply({
content: result.reply,
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
components: [row as any],
});
} else if (interaction.isButton()) {
await interaction.reply({
content: result.reply,
});
}
}
}

View File

@@ -1,16 +0,0 @@
import { CronJob } from "cron";
import { WaterMeService } from "actions/waterMe/waterMe.service";
const waterMeService = new WaterMeService();
/*
new CronJob(
"0 0 20 * * *", // cronTime
async () => {
console.log("isThirsty()");
await waterMeService.isThirsty();
}, // onTick
null, // onComplete
true, // start
"Europe/Berlin", // timeZone
);*/
// job.start() is optional here because of the fourth parameter set to true.

View File

@@ -1,123 +0,0 @@
import { SlashCommandBuilder, userMention } from "discord.js";
import { z } from "zod";
export const Commands = z.enum(["giessen", "medikamente", "hilfe", "support", "kofi", "disboard", "discadia", "accept", "welcome", "embed", "message", "reminder", "version"]);
export const CommandsMeta: Record<z.output<typeof Commands>, { description: string }> = {
giessen: {
description: "giess mich mit etwas wasser :3"
},
medikamente: {
description: "ich erinnere dich gerne daran, deine medikamente zu nehmen! :)"
},
hilfe: {
description: "ich schreibe dir auf, was du alles mit mir machen kannst :)"
},
support: {
description: "unterstuetze uns! link zu unserem ko-fi, disboard und discardia c:"
},
kofi: {
description: "link zu unserem ko-fi (spendenportal):"
},
disboard: {
description: "link zu disboard, hier kannst du uns bewerten!"
},
discadia: {
description: "link zu discadia, hier kannst du fuer uns voten!"
},
accept: {
description: "admin use only"
},
welcome: {
description: "admin use only"
},
embed: {
description: "admin use only"
},
message: {
description: "admin use only"
},
reminder: {
description: "admin use only"
},
version: {
description: "admin use only"
}
}
export type CommandsType = z.output<typeof Commands>;
export default function getCommands() {
const commands = [
new SlashCommandBuilder()
.setName(Commands.Enum.giessen)
.setDescription(CommandsMeta.giessen.description),
new SlashCommandBuilder()
.setName(Commands.Enum.medikamente)
.setDescription(CommandsMeta.medikamente.description),
new SlashCommandBuilder()
.setName(Commands.Enum.hilfe)
.setDescription(CommandsMeta.hilfe.description),
new SlashCommandBuilder()
.setName(Commands.Enum.support)
.setDescription(CommandsMeta.support.description),
new SlashCommandBuilder()
.setName(Commands.Enum.kofi)
.setDescription(CommandsMeta.kofi.description),
new SlashCommandBuilder()
.setName(Commands.Enum.disboard)
.setDescription(CommandsMeta.disboard.description),
new SlashCommandBuilder()
.setName(Commands.Enum.discadia)
.setDescription(CommandsMeta.discadia.description),
new SlashCommandBuilder()
.setName(Commands.Enum.accept)
.setDescription(CommandsMeta.accept.description)
.addStringOption(option =>
option.setName('input')
.setDescription('input for bot')
.setRequired(true)),
new SlashCommandBuilder()
.setName(Commands.Enum.welcome)
.setDescription(CommandsMeta.welcome.description)
.addStringOption(option =>
option.setName('input')
.setDescription('input for bot')
.setRequired(true)),
new SlashCommandBuilder()
.setName(Commands.Enum.embed)
.setDescription(CommandsMeta.embed.description)
.addStringOption(option =>
option.setName('title')
.setDescription('title')
.setRequired(true))
.addStringOption(option =>
option.setName('description')
.setDescription('description')
.setRequired(true))
.addBooleanOption(option =>
option.setName('timestamp')
.setDescription('timestamp bool')
.setRequired(false)),
new SlashCommandBuilder()
.setName(Commands.Enum.message)
.setDescription(CommandsMeta.message.description)
.addStringOption(option =>
option.setName('input')
.setDescription('input for bot')
.setRequired(true)),
new SlashCommandBuilder()
.setName(Commands.Enum.reminder)
.setDescription(CommandsMeta.reminder.description)
.addStringOption(option =>
option.setName('input')
.setDescription('input for bot')
.setRequired(true)),
new SlashCommandBuilder()
.setName(Commands.Enum.version)
.setDescription(CommandsMeta.version.description),
].map((command) => command.toJSON());
return commands;
}

View File

@@ -1,26 +0,0 @@
import { Routes } from "discord.js";
import { REST } from "@discordjs/rest";
import config from "config";
import getCommands from "commands";
export default class DiscordService {
rest: REST;
constructor() {
this.rest = new REST({ version: "10" }).setToken(config.discord.token);
}
async init() {
try {
await this.rest.put(
Routes.applicationCommands(config.discord.applicationId),
{
body: getCommands(),
},
);
console.log("Successfully added commands");
} catch (e) {
console.error(e);
}
}
}

View File

@@ -0,0 +1,84 @@
import type z from "zod";
import type { Commands } from "./commands.schema";
// TODO: add missing options
export const CommandsCollection: z.output<typeof Commands> = {
giessen: {
description: "giess mich mit etwas wasser :3",
},
medikamente: {
description:
"ich erinnere dich gerne daran, deine medikamente zu nehmen! :)",
},
hilfe: {
description: "ich schreibe dir auf, was du alles mit mir machen kannst :)",
},
support: {
description:
"unterstuetze uns! link zu unserem ko-fi, disboard und discardia c:",
},
kofi: {
description: "link zu unserem ko-fi (spendenportal):",
},
disboard: {
description: "link zu disboard, hier kannst du uns bewerten!",
},
discadia: {
description: "link zu discadia, hier kannst du fuer uns voten!",
},
accept: {
description: "admin use only",
options: [
{
name: "input",
description: "input for bot",
required: true,
type: "string",
},
],
},
welcome: {
description: "admin use only",
options: [
{
name: "input",
description: "input for bot",
required: true,
type: "string",
},
],
},
embed: {
description: "admin use only",
options: [
{
name: "title",
description: "title",
required: true,
type: "string",
},
{
name: "description",
description: "description",
required: true,
type: "string",
},
{
name: "timestamp",
description: "timestamp bool",
required: false,
type: "boolean",
},
],
},
message: {
description: "admin use only",
},
reminder: {
description: "admin use only",
},
version: {
description: "admin use only",
},
};

View File

@@ -0,0 +1,59 @@
import z from "zod";
export const CommandKeyOptions = [
"giessen",
"medikamente",
"hilfe",
"support",
"kofi",
"disboard",
"discadia",
"accept",
"welcome",
"embed",
"message",
"reminder",
"version",
] as const;
export const CommandOptionTypeOptions = [
"string",
"integer",
"boolean",
"mentionable",
"channel",
"role",
"user",
] as const;
export const CommandOptionTypes = z.enum(CommandOptionTypeOptions);
export const CommandOptionCommon = z.object({
name: z.string(),
description: z.string(),
required: z.boolean(),
});
export const CommandOptionString = CommandOptionCommon.extend({
type: z.literal(CommandOptionTypes.enum.string),
});
export const CommandOptionBoolean = CommandOptionCommon.extend({
type: z.literal(CommandOptionTypes.enum.boolean),
});
// TODO: add other option types
export const CommandOption = z.discriminatedUnion("type", [
CommandOptionString,
CommandOptionBoolean,
]);
export const CommandKeys = z.enum(CommandKeyOptions);
export const Command = z.object({
name: z.string().optional(),
description: z.string(),
options: z.array(CommandOption).optional(),
});
export const Commands = z.record(CommandKeys, Command);

View File

@@ -0,0 +1,5 @@
import z from "zod";
export const InteractionOptions = ["command", "button"] as const;
export const Interactions = z.enum(InteractionOptions);

View File

@@ -0,0 +1,3 @@
export interface MessagesServiceInterface<U = unknown> {
sendToUser(user: U, message: string): Promise<void>;
}

View File

@@ -0,0 +1,6 @@
export interface RolesServiceInterface<U = unknown> {
assignRole(user: U, role: string): void | Promise<void>;
removeRole(user: U, role: string): void | Promise<void>;
getRoles(user: U): string[] | Promise<string[]>;
hasRole(user: U, role: string): boolean | Promise<boolean>;
}

View File

@@ -0,0 +1,14 @@
import type { MessagesServiceInterface } from "entities/messages/messages.service";
export class GreetingService<U = unknown> {
messagesService: MessagesServiceInterface<U>;
constructor(messagesService: MessagesServiceInterface<U>) {
this.messagesService = messagesService;
}
async sendGreeting(user: U, userName: string) {
const greetingMessage = `Hello, ${userName}! Welcome to the server!`;
await this.messagesService.sendToUser(user, greetingMessage);
}
}

View File

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

View File

@@ -0,0 +1,18 @@
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

@@ -0,0 +1,64 @@
import { TextBasedFeature } from "features/text-based-feature/text-based-feature";
import type { TextBasedFeatureInput } from "features/text-based-feature/text-based-feature.schema";
import { createLogger } from "lib/logger";
import { getRandomInt } from "lib/utils";
export class WaterMeService extends TextBasedFeature {
waterLevel: number;
private logger = createLogger("WaterMeService");
private thirsty = 3 as const;
private enough = 10 as const;
constructor(input: TextBasedFeatureInput) {
super(input);
this.waterLevel = 0;
}
getReply() {
const thirstyReplies = [
"... wow das wars schon??? ich brauche noch mehr wasser :(",
"dankeeeee!!!! ich waer fast verdurstet :(((",
"*roelpssssss*",
];
const fullReplies = [
"langsam reicht es :o",
"poah, das hat gut getan",
"das ist krass :3",
];
const tooMuchReplies = [
"ES REICHT!!!!",
"bitte hoer auf, ich platze gleich :(",
];
if (this.waterLevel <= this.thirsty) {
return thirstyReplies[getRandomInt(0, thirstyReplies.length - 1)];
}
if (this.waterLevel > this.thirsty && this.waterLevel <= this.enough) {
return fullReplies[getRandomInt(0, fullReplies.length - 1)];
}
if (this.waterLevel > this.enough) {
return tooMuchReplies[getRandomInt(0, tooMuchReplies.length - 1)];
}
}
async notifyIfThirsty() {
if (this.waterLevel <= this.thirsty) {
await this.sendMessage({ content: "ich brauche wasser :(" });
}
}
waterMe() {
const reply = this.getReply();
this.waterLevel++;
this.logger.info(`Water level increased to ${this.waterLevel}`);
return {
reply,
};
}
}

View File

@@ -1,22 +0,0 @@
import config from "config";
import { Client, GatewayIntentBits, Partials, ChannelType, Events, IntentsBitField } from "discord.js";
const client = new Client({
intents: [IntentsBitField.Flags.Guilds,
IntentsBitField.Flags.GuildMembers,
IntentsBitField.Flags.GuildModeration,
IntentsBitField.Flags.GuildMessages,
IntentsBitField.Flags.GuildMessageReactions,
IntentsBitField.Flags.GuildMessagePolls,
IntentsBitField.Flags.GuildVoiceStates,
IntentsBitField.Flags.MessageContent,
IntentsBitField.Flags.DirectMessages,
IntentsBitField.Flags.DirectMessageReactions,
IntentsBitField.Flags.DirectMessageTyping,
IntentsBitField.Flags.DirectMessagePolls,],
partials: [Partials.Channel, Partials.Message, Partials.Reaction]
});
await client.login(config.discord.token);
export default client;

8
core/src/lib/logger.ts Normal file
View File

@@ -0,0 +1,8 @@
import { Logger } from "tslog";
export const createLogger = (name: string) =>
new Logger({
name,
prettyLogTemplate:
"{{yyyy}}.{{mm}}.{{dd}} {{hh}}:{{MM}}:{{ss}}:{{ms}}\t{{logLevelName}}\t{{name}}\t",
});

View File

@@ -4,6 +4,7 @@ export default defineConfig({
entry: [
"./src/index.ts",
"./src/entities/**/*.ts",
"./src/features/**/*.ts",
"./src/lib/**/*.ts",
"./src/api/**/*.ts",
"./src/db/**/*.ts",