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,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,104 +1,110 @@
import config from "config";
import {
type CacheType,
Client,
EmbedBuilder,
type GuildMember,
type Interaction,
type Message,
type OmitPartialGroupDMChannel,
} from "discord.js";
import client from "lib/client";
import { getRandomInt } from "lib/utils";
import { dmWelcomeContent, dmAcceptedContent } from "./dm.components.ts";
import {
Client,
EmbedBuilder,
type Message,
type CacheType,
type GuildMember,
type Interaction,
type OmitPartialGroupDMChannel,
} from "discord.js";
import { dmAcceptedContent, dmWelcomeContent } from "./dm.components.ts";
export class DmService {
async handleInteraction(interaction: Interaction<CacheType>) {
// todo
}
async handleInteraction(interaction: Interaction<CacheType>) {
// todo
}
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`;
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`,
);
}
console.error("error while sending a welcome msg:", error);
}
}
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`
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`);
}
console.error("error while sending a welcome msg:", error);
}
}
async welcomePrivate(member: GuildMember) {
console.log("welcome private");
try {
await client.users.send(member, dmWelcomeContent);
} 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`,
);
}
console.error("error while sending a welcome msg:", error);
}
}
async welcomePrivate(member: GuildMember) {
console.log("welcome private");
try {
await client.users.send(member, dmWelcomeContent);
} 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`);
}
console.error("error while sending a welcome msg:", error);
}
}
async forward(message: OmitPartialGroupDMChannel<Message<boolean>>) {
if (message.channel.isDMBased()) {
const author = message.author.id;
const recipient = message.channel.recipient?.id;
console.log("forward message");
let context = "";
async forward(message: OmitPartialGroupDMChannel<Message<boolean>>) {
if (message.channel.isDMBased()) {
const author = message.author.id;
const recipient = message.channel.recipient?.id;
console.log("forward message");
let context = "";
if (message.author.bot) {
context = `<@${author}> hat an <@${recipient}> geschrieben:\n"${message.content}"`;
} else {
context = `<@${author}> hat geschrieben:\n"${message.content}"`;
}
if (message.author.bot) {
context = `<@${author}> hat an <@${recipient}> geschrieben:\n"${message.content}"`;
}
else {
context = `<@${author}> hat geschrieben:\n"${message.content}"`;
}
try {
const channels = client.channels;
const channel = channels.cache.get(config.discord.channelIdLog);
if (channel?.isTextBased() && channel?.isSendable()) {
await channel.send(context);
}
} catch (error) {
console.error("error while forwarding a msg:", error);
}
}
}
try {
const channels = client.channels;
const channel = channels.cache.get(config.discord.channelIdLog);
if (channel?.isTextBased() && channel?.isSendable()) {
await channel.send(context);
}
} catch (error) {
console.error("error while forwarding a msg:", error);
}
}
}
async acceptDm(member: GuildMember) {
console.log("accept dm");
try {
await client.users.send(member, dmAcceptedContent);
} 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`,
);
}
console.error("error while sending a accept msg:", error);
}
}
async acceptDm(member: GuildMember) {
console.log("accept dm");
try {
await client.users.send(member, dmAcceptedContent);
} 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`);
}
console.error("error while sending a accept msg:", error);
}
}
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>`;
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`);
}
console.error("error while sending a accept msg:", error);
}
}
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>`;
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`,
);
}
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,301 +0,0 @@
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";
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;
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 () => {
const channels = client.channels;
const logChannel = channels.cache.get(config.discord.channelIdLog);
if (logChannel?.isTextBased() && logChannel?.isSendable()) {
try {
console.log("bot is online");
await logChannel.send("wieder online!!!");
} catch (error) {
console.error("failed to send online message:", error);
}
} else {
console.error("log channel is not valid or sendable.");
}
await this.setActivity(100);
console.log("ready");
});
process.on("exit", async () => {
const channels = client.channels;
const logChannel = channels.cache.get(config.discord.channelIdLog);
await this.handleShutdown(logChannel);
process.exit(0);
});
process.on("SIGINT", async () => {
const channels = client.channels;
const logChannel = channels.cache.get(config.discord.channelIdLog);
await this.handleShutdown(logChannel);
process.exit(0);
});
process.on("SIGTERM", async () => {
const channels = client.channels;
const logChannel = channels.cache.get(config.discord.channelIdLog);
await this.handleShutdown(logChannel);
process.exit(0);
});
// listen for interactions
client.on("interactionCreate", this.handleInteraction.bind(this));
client.on("messageCreate", async (message) => {
console.log(message.id);
if (message.channel.type === ChannelType.DM) {
console.log("got msg");
await this.dmService.forward(message);
}
});
client.on("guildMemberAdd", async (member) => {
await this.greetingService.welcome(member);
});
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 handleShutdown(logChannel: Channel | undefined) {
if (logChannel?.isTextBased() && logChannel?.isSendable()) {
try {
await logChannel.send("bot is going offline...");
} catch (error) {
console.error("failed to send offline message:", error);
}
} else {
console.error("log channel is not valid or sendable.");
}
await this.setActivity(0);
console.log("bot is offline.");
}
async init() {
await this.discordService.init();
}
async handleInteraction(interaction: Interaction<CacheType>) {
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<CacheType>) {
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<CacheType>,
) {
const commandName = interaction.commandName as CommandsType;
// add commands
switch (commandName) {
case Commands.Enum.giessen:
await this.waterMeService.handleInteraction(interaction); // zu chatinputcommand wechseln
return;
case Commands.Enum.medikamente:
await this.medicationService.handleChatInputCommand(interaction);
return;
case Commands.Enum.hilfe:
await this.helpService.handleInteraction(interaction); // zu chatinputcommand wechseln
return;
case Commands.Enum.support:
case Commands.Enum.kofi:
case Commands.Enum.disboard:
case Commands.Enum.discadia:
await this.supportService.handleInteraction(interaction);
return;
case Commands.Enum.accept:
await this.greetingService.handleChatInputCommand(interaction);
return;
case Commands.Enum.welcome:
await this.greetingService.handleChatInputCommand(interaction);
return;
case Commands.Enum.embed:
case Commands.Enum.message:
await this.customMessageService.handleChatInputCommand(interaction);
return;
case Commands.Enum.reminder:
await this.greetingService.handleChatInputCommand(interaction);
return;
case Commands.Enum.version:
await this.debugService.handleChatInputCommand(interaction);
return;
default:
break;
}
}
// wenn neues fenster durch buttonclick or so
async handleModalSubmit(interaction: ModalSubmitInteraction<CacheType>) {
const { customId } = interaction;
switch (customId) {
default:
break;
}
}
}

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",