restructure

move to bun workspaces
bump zod
begin adding fluxer
This commit is contained in:
2026-02-17 18:04:32 +01:00
parent 6608bd6ee7
commit 6ce1fdf67d
79 changed files with 1106 additions and 79 deletions

View File

@@ -0,0 +1,13 @@
import type { CacheType, Client, Interaction } from "discord.js";
import client from "lib/client";
export class ActivityService {
async setActivity(client: Client<boolean>, description: string, activity: string) {
client.user?.setActivity(":3", { type: 4 });
console.log("set activity");
client.user?.setPresence({
status: "online",
});
}
}

View File

@@ -0,0 +1,30 @@
import config from "config";
import { EmbedBuilder } from "discord.js";
export const customContent = `hey <@&${config.discord.roleStudy}>! meine [eigene website](https://avocadi.unom.io) ist endlich on :3\ngebe mir gerne rueckmeldung unter <#${config.discord.channelIdFeedback}>! <3`;
export function createEmbed(title: string, description: string, timestamp?: boolean) {
// ({ embeds: [exampleEmbed] })
console.log("createEmbed()");
const customEmbed = (timestamp === true) ? new EmbedBuilder()
.setColor(0x004400)
.setAuthor({
name: title,
iconURL:
"https://media.discordapp.net/attachments/1321933410188656693/1323447010380222474/mo_Avocadi_Avatar_Closeup_2.png?ex=67748b93&is=67733a13&hm=f48efb3523bca5f50e79144c7b41a127c94670e693e3da3dc2e6ffe62ad8a769&=&format=webp&quality=lossless&width=1524&height=1524",
url: "https://avocadi.unom.io",
})
.setDescription(description)
.setTimestamp() :
new EmbedBuilder()
.setColor(0x004400)
.setAuthor({
name: title,
iconURL:
"https://media.discordapp.net/attachments/1321933410188656693/1323447010380222474/mo_Avocadi_Avatar_Closeup_2.png?ex=67748b93&is=67733a13&hm=f48efb3523bca5f50e79144c7b41a127c94670e693e3da3dc2e6ffe62ad8a769&=&format=webp&quality=lossless&width=1524&height=1524",
url: "https://avocadi.unom.io",
})
.setDescription(description);
//.setFooter({ text: 'Some footer text here', iconURL: 'https://i.imgur.com/AfFp7pu.png' });
return customEmbed;
}

View File

@@ -0,0 +1,133 @@
import config from "config";
import client from "lib/client";
import { getRandomInt } from "lib/utils";
import { customContent, createEmbed } from "./customMessage.components.ts";
import {
Client,
EmbedBuilder,
type Message,
type CacheType,
type GuildMember,
type Interaction,
type OmitPartialGroupDMChannel,
type ChatInputCommandInteraction,
ChannelType,
} from "discord.js";
import { type CommandsType, Commands } from "commands/index.ts";
import { time } from "drizzle-orm/mysql-core";
export class CustomMessageService {
async handleInteraction(interaction: Interaction<CacheType>) {
if (interaction.isChatInputCommand()) {
await this.handleChatInputCommand(interaction);
return;
}
}
async handleChatInputCommand(
interaction: ChatInputCommandInteraction<CacheType>,
) {
console.log("accept");
const commandName = interaction.commandName as CommandsType;
switch (commandName) {
case Commands.Enum.embed:
await this.customEmbed(interaction);
return;
case Commands.Enum.message:
await this.customMessage(interaction);
return;
default:
break;
}
}
async checkPermission(interaction: ChatInputCommandInteraction<CacheType>) {
const userIdCommand = interaction.user.id;
if (userIdCommand !== config.discord.myId) {
await interaction.reply({
content: "you have no permission for that command",
ephemeral: true,
});
return false;
}
return true;
}
// check if command done in server
async checkIfServer(interaction: ChatInputCommandInteraction<CacheType>) {
const guild = interaction.guild;
if (!guild) {
await interaction.reply({
content: "command can only be used on a server",
ephemeral: true,
});
return false;
}
return true;
}
async customEmbed(interaction: ChatInputCommandInteraction<CacheType>) {
const title = interaction.options.getString("title") || " ";
const description = interaction.options.getString("description") || " ";
const timestamp = interaction.options.getBoolean("timestamp") || false;
// return the value
console.log(title, description, timestamp);
// permission check
// permission check
if (await this.checkPermission(interaction) && await this.checkIfServer(interaction))
try {
const channels = client.channels;
const channel = channels.cache.get(interaction.channelId);
if (channel?.isTextBased() && channel?.isSendable()) {
await channel.send({
embeds: [createEmbed(title, description, timestamp)],
});
}
await interaction.reply({
content: "successfully created embed",
ephemeral: true,
});
} catch (error) {
console.error("error while creating embed:", error);
await interaction.reply({
content:
"error while creating embed",
ephemeral: true,
});
}
}
async customMessage(interaction: ChatInputCommandInteraction<CacheType>) {
const input: string = interaction.options.getString("input") || "";
const result = input.replaceAll(";", "\n");
// return the value
console.log(input);
// permission check && server check
if (await this.checkPermission(interaction) && await this.checkIfServer(interaction))
try {
const channels = client.channels;
const channel = channels.cache.get(interaction.channelId);
if (channel?.isTextBased() && channel?.isSendable()) {
await channel.send({
content: result,
});
}
await interaction.reply({
content: "successfully created message",
ephemeral: true,
});
} catch (error) {
console.error("error while creating message:", error);
await interaction.reply({
content:
"error while creating message",
ephemeral: true,
});
}
}
}

View File

@@ -0,0 +1,48 @@
import { type CommandsType, Commands } from "commands";
import config from "config";
import type { CacheType, ChatInputCommandInteraction, Interaction } from "discord.js";
import { checkPermission } from "permissions";
export class DebugService {
async handleInteraction(
interaction: Interaction<CacheType>
) {
if (interaction.isChatInputCommand()) {
await this.handleChatInputCommand(interaction);
return;
}
}
async handleChatInputCommand(interaction: ChatInputCommandInteraction<CacheType>) {
const commandName = interaction.commandName as CommandsType;
switch (commandName) {
case Commands.Enum.version:
await this.version(interaction);
return;
default:
break;
}
}
async version(interaction: ChatInputCommandInteraction<CacheType>) {
try {
console.log("version command");
if (await checkPermission(interaction.member) !== true) {
await interaction.reply({
content: "du hast keine rechte fuer diesen befehl",
ephemeral: true,
});
return;
}
await interaction.reply({
content: "version: " + config.discord.version,
});
}
catch (error) {
console.error("error while sending version msg:", error);
}
}
}

View File

@@ -0,0 +1,5 @@
import config from "config";
import { EmbedBuilder } from "discord.js";
export const dmWelcomeContent = `hey! ich bin avocadi von [avocadi-study](<https://discord.gg/kkryyeXu3S>)!\n\num auf den rest des servers zugreifen zu koennen, musst du dich noch vorstellen (unter <#${config.discord.channelIdIntroduction}>)!\n\n---\nname und alter:\npronomen:\nklasse/studiengang/beruf:\nhobby:\nueber mich:\n---\n\nsobald wir deine nachricht ueberprueft haben, bekommst du die rolle **lernende:r** :)`;
export const dmAcceptedContent = `huhu! du wurdest als lernende:r akzeptiert :3\nsag gerne hallo: <#${config.discord.channelIdOffTopic}> <:avocadi_cute:1321893797138923602>`;

View File

@@ -0,0 +1,104 @@
import config from "config";
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";
export class DmService {
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 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 = "";
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);
}
}
}
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);
}
}
}

View File

@@ -0,0 +1,14 @@
import config from "config";
import client from "lib/client";
export class GreetingService {
async greet() {
const channels = client.channels;
const channel = channels.cache.get(config.discord.channelIdBot);
if (channel?.isTextBased && channel?.isSendable()) {
await channel.send({ content: "HALLOOOO" });
}
}
}

View File

View File

@@ -0,0 +1,60 @@
import config from "config";
import client from "lib/client";
import { getRandomInt } from "lib/utils";
import { } from "./dynamicVChannel.components.ts";
import {
Client,
EmbedBuilder,
type Message,
type CacheType,
type GuildMember,
type Interaction,
type OmitPartialGroupDMChannel,
type VoiceState,
type VoiceChannel,
type StageChannel,
Events,
type VoiceBasedChannel,
} from "discord.js";
export class DynamicVChannelService {
async handleInteraction(interaction: Interaction<CacheType>) {
// todo
}
async createVChannel(
newState: VoiceState,
channel: VoiceBasedChannel
): Promise<StageChannel | VoiceChannel> {
//console.log("createChannel()");
const newVChannel = await channel.clone({
name: `${channel.name.substring(2)}; ${newState.member?.displayName}`,
position: 100,
});
return newVChannel;
}
async deleteVChannel(
oldState: VoiceState,
newState: VoiceState,
newChannel: StageChannel | VoiceChannel,
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
channelListeners: Map<any, any>,
channelListener: (oldState: VoiceState, newState: VoiceState) => void
) {
//console.log("deleteChannel()");
if (
oldState.channelId === newChannel.id ||
newState.channelId === newChannel.id
) {
if (newChannel.members.size === 0) {
newChannel.delete().catch(console.error);
client.removeListener(Events.VoiceStateUpdate, channelListener);
channelListeners.delete(newChannel.id);
}
}
return channelListeners;
}
}

View File

@@ -0,0 +1,38 @@
import config from "config";
import type { GuildMember } from "discord.js";
export const greetContent = ["HALLOOOO", "guten morgen! ich hoffe es geht euch gut <3"];
export const sleepContent = ["gute nacht! ich muss jetzt schlafen gehen :c", "zzzzZZ..", "*schnarch*"];
export const dmWelcomeContent = `hey! ich bin avocadi von [avocadi-study](<https://discord.gg/kkryyeXu3S>)!\n\num auf den rest des servers zugreifen zu koennen, musst du dich noch vorstellen (unter <#${config.discord.channelIdIntroduction}>)!\n\n---\nname und alter:\npronomen:\nklasse/studiengang/beruf:\nhobby:\nueber mich:\n---\n\nsobald wir deine nachricht ueberprueft haben, bekommst du die rolle **lernende:r** :)`;
export const dmAcceptedContent = `huhu! du wurdest als lernende:r akzeptiert :3\nsag gerne hallo: <#${config.discord.channelIdOffTopic}> <:avocadi_cute:1321893797138923602>`;
export function getWelcomeContent(member: GuildMember) {
const welcomeContents = [
`willkommen auf dem server, ${member}! fuehl dich wie zuhause💕`,
`hey ${member}! schoen, dass du hier bist! 😊`,
`hi ${member}, willkommen! viel spass hier! 💖`,
`willkommen, ${member}! schoen, dass du da bist! :3`,
`moin ${member}! viel spass im server! c:`,
`hey ${member}, herzlich willkommen! fuehl dich wie zu hause! <3`,
`hi ${member}! cool, dass du da bist! <3`,
`willkommen, ${member}! wir freuen uns, dass du hier bist! 💕`,
`hey ${member}! schoen, dass du bei uns bist! :3`,
`willkommen auf dem server, ${member}! viel spass hier! ✨`,
`hi ${member}, super, dass du dabei bist! :3`,
`hey ${member}, willkommen bei uns! 💖`,
`moin ${member}! schoen, dass du dabei bist! :)`,
`hi ${member}, willkommen in unserer kleinen community! ✨`,
`willkommen, ${member}! fuehl dich wie zu hause! 💕`,
`hey ${member}, schoen, dass du uns gefunden hast! <333`,
`hi ${member}, willkommen in unserer runde! c:`,
`willkommen, ${member}! schoen, dass du hier bist! 💖`,
`moin ${member}! lass uns zusammen spass haben! ✨`,
`hey ${member}, herzlich willkommen bei uns! 😊`,
`hi ${member}! schoen, dass du dabei bist! 💕`,
`willkommen auf dem server, ${member}! wir freuen uns auf dich! <3`,
`hey ${member}, schoen, dass du da bist! ✨`,
`hi ${member}, willkommen! fuehl dich wie zu hause! 💖`,
`willkommen, ${member}! lass uns gemeinsam eine tolle zeit haben! :3`,
];
return welcomeContents[Math.floor(Math.random() * welcomeContents.length)];
}

View File

@@ -0,0 +1,339 @@
import config from "config";
import client from "lib/client";
import { getRandomInt } from "lib/utils";
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;
constructor() {
this.dmService = new DmService();
}
async handleInteraction(
interaction: Interaction<CacheType>
) {
if (interaction.isChatInputCommand()) {
await this.handleChatInputCommand(interaction);
return;
}
}
async handleChatInputCommand(interaction: ChatInputCommandInteraction<CacheType>) {
const commandName = interaction.commandName as CommandsType;
switch (commandName) {
case Commands.Enum.accept:
await this.acceptUser(interaction);
return;
case Commands.Enum.welcome:
await this.welcomeCommand(interaction);
return;
case Commands.Enum.reminder:
await this.reminderCommand(interaction);
return;
default:
break;
}
}
async welcome(member: GuildMember) {
console.log("welcome msg");
const welcomeContent = getWelcomeContent(member);
try {
const channels = client.channels;
const channel = channels.cache.get(config.discord.channelIdWelcome);
if (channel?.isTextBased() && channel?.isSendable()) {
await channel.send(welcomeContent);
}
await this.dmService.welcomePrivate(member);
} catch (error) {
console.error("error while sending a welcome msg:", error);
}
}
async acceptUser(
interaction: ChatInputCommandInteraction<CacheType>
) {
console.log("accept user");
// get the string option
const input = interaction.options.getString("input") || "";
// return the value
//console.log(input);
// permission check
if (await checkPermission(interaction.member) !== true) {
await interaction.reply({
content: "du hast keine rechte fuer diesen befehl",
ephemeral: true,
});
return;
}
try {
// get user id from mentioning
const userId = input.replace(/[<@!>]/g, "");
console.log(userId.toString());
const guild = interaction.guild;
if (!guild) {
await interaction.reply({
content: "command can only be used on a server",
ephemeral: true,
});
return;
}
const member = await guild.members.fetch(userId);
const username = member.user.username;
console.log(username);
if (await this.checkRole(member) === true) {
await interaction.reply({
content: `${member.user.username} hat die rolle *lernende:r* schon!`,
ephemeral: true,
});
return;
}
await member.roles.add(config.discord.roleStudy);
await interaction.reply({
content: `die rolle *lernende:r* wurde erfolgreich an ${member.user.username} vergeben`,
ephemeral: true,
});
this.dmService.acceptDm(member);
} catch (error) {
console.error("Fehler beim Hinzufügen der Rolle:", error);
await interaction.reply({
content:
"Es gab einen Fehler beim Hinzufügen der Rolle. Stelle sicher, dass du einen gültigen User erwähnt hast.",
ephemeral: true,
});
}
}
// unused
async greet() {
client.user?.setActivity("guten morgen! :3", { type: 4 });
console.log("set activity: awake");
client.user?.setPresence({
status: "online",
});
const channels = client.channels;
const channel = channels.cache.get(config.discord.channelIdOffTopic);
if (channel?.isTextBased && channel?.isSendable()) {
await channel.send({ content: this.getContent(false) });
}
}
// unused
async sleep() {
client.user?.setActivity("zzzzZZ..", { type: 4 });
console.log("set activity: asleep");
client.user?.setPresence({
status: "dnd",
});
const channels = client.channels;
const channel = channels.cache.get(config.discord.channelIdOffTopic);
if (channel?.isTextBased && channel?.isSendable()) {
await channel.send({ content: this.getContent(true) });
}
}
async newYear() {
client.user?.setActivity("frohes neues! :)", { type: 4 });
console.log("set activity: happy new year");
client.user?.setPresence({
status: "online",
});
// unused
/*const channels = client.channels;
const channel = channels.cache.get(config.discord.channelIdOffTopic);
if (channel?.isTextBased && channel?.isSendable()) {
await channel.send({ content: "frohes neues! @everyone" });
}*/
}
getContent(asleep: boolean) {
if (asleep) {
return sleepContent[getRandomInt(0, sleepContent.length - 1)];
}
return greetContent[getRandomInt(0, greetContent.length - 1)];
}
async reminderCommand(
interaction: ChatInputCommandInteraction<CacheType>) {
console.log("remind user");
// get the string option
const input = interaction.options.getString("input") || "";
// return the value
//console.log(input);
try {
// permission check
if (await checkPermission(interaction.member) !== true) {
await interaction.reply({
content: "du hast keine rechte fuer diesen befehl",
ephemeral: true,
});
return;
}
// get user id from mentioning
const userId = input.replace(/[<@!>]/g, "");
console.log(userId.toString());
const guild = interaction.guild;
if (!guild) {
await interaction.reply({
content: "command can only be used on a server",
ephemeral: true,
});
return;
}
const member = await guild.members.fetch(userId);
const username = member.user.username;
console.log(username);
if (await this.checkRole(member) === true) {
await interaction.reply({
content: `${member.user.username} hat die rolle *lernende:r* schon!`,
ephemeral: true,
});
return;
}
await this.dmService.reminderPrivate(member);
//await member.roles.add(config.discord.roleStudy);
await interaction.reply({
content: `${member.user.username} wurde erfolgrich remindet`,
ephemeral: true,
});
} catch (error) {
console.error("error while reminding:", error);
await interaction.reply({
content:
"Es gab einen Fehler beim reminden. Stelle sicher, dass du einen gültigen User erwähnt hast.",
ephemeral: true,
});
}
}
async welcomeCommand(
interaction: ChatInputCommandInteraction<CacheType>
) {
console.log("welcome user");
// get the string option
const input = interaction.options.getString("input") || "";
// return the value
//console.log(input);
try {
// get user id from mentioning
const userId = input.replace(/[<@!>]/g, "");
console.log(userId.toString());
const guild = interaction.guild;
if (!guild) {
await interaction.reply({
content: "command can only be used on a server",
ephemeral: true,
});
return;
}
if (await checkPermission(interaction.member) !== true) {
await interaction.reply({
content: "du hast keine rechte fuer diesen befehl",
ephemeral: true,
});
return;
}
// get member from id
const member = await guild.members.fetch(userId);
const username = member.user.username;
console.log(username);
const welcomeContent = getWelcomeContent(member);
if (await this.checkRole(member) === true) {
await interaction.reply({
content: `${member.user.username} wurde schon begruesst!`,
ephemeral: true,
});
}
try {
const channels = client.channels;
const channel = channels.cache.get(config.discord.channelIdWelcome);
if (channel?.isTextBased() && channel?.isSendable()) {
await channel.send(welcomeContent);
}
await this.dmService.welcomePrivate(member);
} catch (error) {
console.error("error while sending a welcome command msg:", error);
}
await interaction.reply({
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",
ephemeral: true,
});
}
}
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)) {
console.log("user has permission");
hasRole = true;
}
}
return hasRole;
}
}

View File

@@ -0,0 +1,38 @@
import { CronJob } from "cron";
import { GreetingService } from "actions/greeting/greeting.service";
const greetingService = new GreetingService();
/*
new CronJob(
"0 30 6 * * *", // cronTime
async () => {
console.log("good morning");
await greetingService.greet();
}, // onTick
null, // onComplete
true, // start
"Europe/Berlin", // timeZone
);
// job.start() is optional here because of the fourth parameter set to true.
new CronJob(
"0 30 22 * * *",
async () => {
console.log("good night");
await greetingService.sleep();
},
null,
true,
"Europe/Berlin",
);*/
new CronJob(
"0 0 0 1 1 *",
async () => {
console.log("happy new year");
await greetingService.newYear();
},
null,
true,
"Europe/Berlin",
);

View File

@@ -0,0 +1,26 @@
import { Commands, CommandsMeta } from 'commands';
import { AttachmentBuilder, EmbedBuilder } from 'discord.js';
export default function createEmbed() { // ({ embeds: [exampleEmbed] })
console.log("createHelpEmbed()");
const exampleEmbed = new EmbedBuilder()
.setColor(0x004400)
//.setTitle("/hilfe")
//.setURL("")
.setAuthor({ name: "avocadi - befehle", iconURL: "https://media.discordapp.net/attachments/1321933410188656693/1323447010380222474/mo_Avocadi_Avatar_Closeup_2.png?ex=67748b93&is=67733a13&hm=f48efb3523bca5f50e79144c7b41a127c94670e693e3da3dc2e6ffe62ad8a769&=&format=webp&quality=lossless&width=1524&height=1524", url: 'https://git.unom.io/mo/avocadi-bot' })
.setDescription(" ")
.addFields(
{ name: `/${Commands.Enum.giessen}`, value: CommandsMeta.giessen.description },
{ name: `/${Commands.Enum.medikamente}`, value: CommandsMeta.medikamente.description },
{ name: `/${Commands.Enum.hilfe}`, value: CommandsMeta.hilfe.description },
{ name: `/${Commands.Enum.support}`, value: CommandsMeta.support.description },
{ name: `/${Commands.Enum.kofi}`, value: CommandsMeta.kofi.description },
{ name: `/${Commands.Enum.disboard}`, value: CommandsMeta.disboard.description },
{ name: `/${Commands.Enum.discadia}`, value: CommandsMeta.discadia.description },
)
.setTimestamp()
//.setFooter({ text: 'Some footer text here', iconURL: 'https://i.imgur.com/AfFp7pu.png' });
;
return exampleEmbed;
}

View File

@@ -0,0 +1,22 @@
import type { CacheType, Interaction } from "discord.js";
import createEmbed from "./help.components";
export class HelpService {
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
exampleEmbed: any;
constructor() {
this.exampleEmbed = createEmbed();
}
async handleInteraction(interaction: Interaction<CacheType>) {
console.log("help");
if (interaction.isChatInputCommand()) {
await interaction.reply({
embeds: [this.exampleEmbed],
ephemeral: true,
});
}
}
}

View File

@@ -0,0 +1,10 @@
import { ButtonBuilder, ButtonStyle } from "discord.js";
export const yesButton = new ButtonBuilder()
.setCustomId("yesMedication")
.setLabel("ja")
.setStyle(ButtonStyle.Primary);
export const noButton = new ButtonBuilder()
.setCustomId("noMedication")
.setLabel("noch nicht")
.setStyle(ButtonStyle.Secondary);

View File

@@ -0,0 +1,250 @@
import { CronJob } from "cron";
import { getRandomInt } from "lib/utils";
import config from "config";
import client from "lib/client";
import {
ActionRowBuilder,
ButtonBuilder,
type ButtonInteraction,
ButtonStyle,
type ChatInputCommandInteraction,
type ModalSubmitInteraction,
type CacheType,
type Interaction,
} from "discord.js";
import { yesButton, noButton } from "./medication.components";
import { db } from "db";
import { usersTable } from "db/schema";
import { eq } from "drizzle-orm";
export class MedicationService {
medication: string;
tookMedication: boolean;
constructor() {
this.medication = "";
this.tookMedication = false;
}
async askMedication() {
const channels = client.channels;
const channel = channels.cache.get(config.discord.channelIdBot);
// funkt noch nicht, beides
const row = new ActionRowBuilder().addComponents(yesButton);
row.addComponents(noButton);
if (
channel?.isTextBased &&
channel?.isSendable() &&
this.tookMedication === false
) {
await channel.send({
content: "hast du schon deine medis genommen? :)",
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
components: [row as any],
});
}
}
getMedication() {
// todo
}
setMedication() {
const reply = this.getReply();
console.log("medication");
// this.medication = input von user:in hier rein;
return {
reply,
};
}
getReply() {
return "medication reminder";
}
async handleInteraction(interaction: Interaction<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 handleModalSubmit(interaction: ModalSubmitInteraction<CacheType>) {
switch (interaction.customId) {
default:
break;
}
}
async handleButton(interaction: ButtonInteraction<CacheType>) {
console.log("button interaction");
const discordId = Number.parseInt(interaction.user.id);
const name = interaction.user.displayName;
console.log(`userid: ${discordId}`);
try {
const userId = await this.ensureUserExists(discordId, name);
console.log(`userid: ${userId}`);
const tookMedication = interaction.customId === "yesMedication";
await interaction.reply({
content: tookMedication
? "das hast du toll gemacht <3 mach weiter so :3"
: "das passiert mal... aber versuch sie heute noch zu nehmen, oki? :)",
});
await this.logMedication(userId, tookMedication);
} catch (error) {
console.error("error ensuring user exists:", error);
await interaction.reply({
content:
"es gab einen fehler beim verarbeiten deiner anfrage :( versuch es bitte spaeter nochmal, oki? c:",
ephemeral: true,
});
return;
}
}
async handleChatInputCommand(
interaction: ChatInputCommandInteraction<CacheType>,
) {
const result = this.setMedication();
const row = new ActionRowBuilder().addComponents(yesButton);
row.addComponents(noButton);
await interaction.reply({
content: result.reply,
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
components: [row as any],
});
}
/**, um die Benutzerdaten in die Datenbank zu schreiben.
* @param discordId unique user id
* @param name name how the user wants to get called by avocadi
* @param tookMedication if user took medication
*/
async logMedication(id: number, tookMedication: boolean) {
try {
await db
.update(usersTable)
.set({
took_medication_today: Number(tookMedication),
})
.where(eq(usersTable.id, id));
/* await db.insert(usersTable).values({
name: name,
discord_id: discordId,
took_medication_today: Number(tookMedication),
}); */
console.log(`user with id ${id} saved`);
} catch (error) {
console.error("error while saving in db:", error);
}
}
async getNameByDiscordId(discordId: number): Promise<string | null> {
const result = await db
.select({
name: usersTable.name,
})
.from(usersTable)
.where(eq(usersTable.discord_id, discordId))
.limit(1);
if (result.length > 0) {
console.log("user found");
return result[0].name;
}
console.log("name not found");
return "";
}
async findUserIdByDiscordId(discordId: number): Promise<number | null> {
try {
const result = await db
.select({
id: usersTable.id,
})
.from(usersTable)
.where(eq(usersTable.discord_id, discordId))
.limit(1);
if (result.length > 0) {
console.log(`ID für Discord-ID ${discordId} gefunden: ${result[0].id}`);
return result[0].id;
}
console.log(`no id for discordId ${discordId} found`);
return null;
} catch (error) {
console.error(
`error while calling id for discordId ${discordId}:`,
error,
);
return null;
}
}
async ensureUserExists(discordId: number, name: string): Promise<number> {
try {
const userId = await this.findUserIdByDiscordId(discordId);
if (userId !== null) {
console.log(`entry for discordID ${discordId} already exists`);
return userId;
}
console.log(
`found no entry for discordID ${discordId}. creating new entry`,
);
const result = await db
.insert(usersTable)
.values({
name: name,
discord_id: discordId,
})
.onConflictDoNothing()
.returning({
id: usersTable.id,
});
if (result.length > 0) {
const newUserId = result[0].id;
console.log(
`new user with discordId ${discordId} and name ${name} created. id: ${newUserId}`,
);
return newUserId;
}
// check again if user is now existing
const newUserId = await this.findUserIdByDiscordId(discordId);
if (newUserId !== null) {
console.log(`user created in parallel. fetched id: ${newUserId}`);
return newUserId;
}
throw new Error(`creating a new user for discordId ${discordId} failed`);
} catch (error) {
console.error("error while creating or calling the user:", error);
throw error;
}
}
}

View File

@@ -0,0 +1,15 @@
import { CronJob } from "cron";
import { MedicationService } from "actions/medication/medication.service";
const medicationService = new MedicationService();
/*new CronJob(
"0 0 19 * * *", // cronTime
async () => {
console.log("askMedication()");
await medicationService.askMedication();
}, // onTick
null, // onComplete
true, // start
"Europe/Berlin", // timeZone
);*/

View File

@@ -0,0 +1,42 @@
import config from "config";
import { Events, NewsChannel, type VoiceState } from "discord.js";
import client from "lib/client";
import EventEmitter from "node:events";
import { PomodoroService } from "actions/pomodoro/pomodoro.service";
export default class PomodoroController extends EventEmitter {
private pomodoroService: PomodoroService;
private activePomodoros = new Set<string>();
private pomodoroChannels = [config.discord.vchannelIdPomodoro25, config.discord.vchannelIdPomodoro50];
constructor() {
super();
this.pomodoroService = new PomodoroService();
client.on(Events.VoiceStateUpdate, async (oldState, newState) => {
const userId = newState.id;
const joinedPomodoroVC = newState.channelId != null && this.pomodoroChannels.includes(newState.channelId) &&
oldState.channelId !== newState.channelId;
const leftPomodoroVC = oldState.channelId != null && this.pomodoroChannels.includes(oldState.channelId) &&
newState.channelId !== oldState.channelId;
if (leftPomodoroVC && this.activePomodoros.has(userId)) {
console.log("pomodoro left");
this.pomodoroService.stopPomodoro(userId);
this.activePomodoros.delete(userId);
}
if (joinedPomodoroVC && !this.activePomodoros.has(userId)) {
console.log("pomodoro join");
const member = newState.member;
const vchannel = newState.channel;
if (!member || !vchannel) return;
this.activePomodoros.add(userId);
this.pomodoroService.startPomodoroLoop(member, vchannel);
}
});
}
}

View File

@@ -0,0 +1,67 @@
import type { GuildMember, VoiceBasedChannel } from "discord.js";
import client from "lib/client";
import config from "config";
import { CustomMessageService } from "actions/customMessage/customMessage.service";
export class PomodoroService {
customMessageService: CustomMessageService;
private activeControllers = new Map<string, AbortController>();
constructor() {
this.customMessageService = new CustomMessageService();
}
public async startPomodoroLoop(member: GuildMember, vchannel: VoiceBasedChannel) {
const userId = member.id;
const controller = new AbortController();
this.activeControllers.set(userId, controller);
const minutesWork = vchannel.id === config.discord.vchannelIdPomodoro25 ? 25 : 50;
const minutesBreak = minutesWork / 5;//vchannel.id === config.discord.vchannelIdPomodoro25 ? 5 : 10;
const signal = controller.signal;
try {
while (!signal.aborted) {
await this.sendMessage(`<@${userId}> 🍅 **pomodoro gestartet!** ${minutesWork} minuten produktivitaet`);
const finishedWork = await this.sleep(minutesWork * 60 * 1000, signal);
if (!finishedWork) break;
await this.sendMessage(`<@${userId}> ☕ **pause!** ${minutesBreak} minuten chillen`);
const finishedBreak = await this.sleep(minutesBreak * 60 * 1000, signal);
if (!finishedBreak) break;
}
} catch (err) {
if ((err as Error).name !== "AbortError") {
console.error("pomodoro fehler:", err);
}
} finally {
this.activeControllers.delete(userId);
}
}
public stopPomodoro(userId: string) {
const controller = this.activeControllers.get(userId);
if (controller) {
controller.abort();
this.activeControllers.delete(userId);
}
}
private async sendMessage(text: string) {
const channel = client.channels.cache.get(config.discord.channelIdPomodoro);
if (channel?.isTextBased() && channel?.isSendable()) {
await channel.send(text);
}
}
private sleep(ms: number, signal: AbortSignal): Promise<boolean> {
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => resolve(true), ms);
signal.addEventListener("abort", () => {
clearTimeout(timeout);
resolve(false);
});
});
}
}

View File

@@ -0,0 +1,69 @@
import { DmService } from "actions/dm/dm.service";
import config from "config";
import type { CacheType, ChatInputCommandInteraction, Guild, GuildMember, MessageReaction, PartialMessageReaction, PartialUser, User } from "discord.js";
export class ReactRolesService {
dmService: DmService;
constructor() {
this.dmService = new DmService();
}
async roleMention(reaction: MessageReaction | PartialMessageReaction, user: User | PartialUser, add: boolean) {
if (!await this.validMsg(reaction.message.id)) return;
try {
await reaction.fetch();
const guild = reaction.message.guild;
if (!guild) return;
const member = await this.getUser(guild, user);
if (!member) return;
add ? await this.giveRoleMention(member, guild, user) : await this.removeRoleMention(member, guild, user);
await this.dmService.roleMentionDm(member, add);
} catch (error) {
console.error('smt went wring while roleMention():', error);
return;
}
}
async giveRoleMention(member: GuildMember, guild: Guild, user: User | PartialUser) {
this.updateRoleMention(member, guild, user, true);
}
async removeRoleMention(member: GuildMember, guild: Guild, user: User | PartialUser) {
this.updateRoleMention(member, guild, user, false);
}
async updateRoleMention(member: GuildMember, guild: Guild, user: User | PartialUser, add: boolean) {
try {
const role = guild.roles.cache.get(config.discord.roleMention);
if (!role) {
console.error("role ot found.");
return;
}
if (add === member.roles.cache.has(role.id)) {
console.log(`${member.user.tag} hat die Rolle *streber* bereits.`);
return;
}
await (add ? member.roles.add(role) : member.roles.remove(role));
console.log(`role *streber* successfully ${add ? "added to" : "removed from"} ${member.user.tag}.`);
} catch (error) {
console.error(`error while ${add ? "added to" : "removed from"} RoleMention:`, error);
}
}
async validMsg(id: string) {
return id === config.discord.rolesMsg;
}
async getUser(guild: Guild, user: User | PartialUser) {
try {
return await guild.members.fetch(user.id);
} catch (error) {
console.error("error fetching user:", error);
return null;
}
}
}

View File

@@ -0,0 +1,18 @@
import { createEmbed } from "actions/customMessage/customMessage.components";
const kofiTitle = "ko-fi";
const kofiLink = "[ko-fi.com/avocadi](https://ko-fi.com/avocadi)";
const kofiDesc = "gerne kannst du uns eine kleine spende ueber ko-fi zukommen lassen!";
const disboardTitle = "disboard";
const disboardLink = "[disboard.org/de/server/1316153371899592774](https://disboard.org/de/server/1316153371899592774)";
const disboardDesc = "hier kannst du eine bewertung schreiben <3";
const discadiaTitle = "discadia";
const discadiaLink = "[discadia.com/server/avocadi](https://discadia.com/server/avocadi/)";
const discadiaDesc = "du moechtest fuer uns voten? dann klicke auf den link :)";
export const supportContent = createEmbed("support", (`${kofiTitle}: ${kofiLink}\n${disboardTitle}: ${disboardLink}\n${discadiaTitle}: ${discadiaLink}`));
export const kofiContent = createEmbed(kofiTitle, (`${kofiDesc}\n${kofiLink}`));
export const disboardContent = createEmbed(disboardTitle, (`${disboardDesc}\n${disboardLink}`));
export const discadiaContent = createEmbed(discadiaTitle, (`${discadiaDesc}\n${discadiaLink}`));

View File

@@ -0,0 +1,93 @@
import config from "config";
import client from "lib/client";
import { getRandomInt } from "lib/utils";
import {
kofiContent,
supportContent,
discadiaContent,
disboardContent,
} from "./support.components.ts";
import type {
CacheType,
Interaction,
ChatInputCommandInteraction,
} from "discord.js";
import { Commands, type CommandsType } from "commands/index.ts";
export class SupportService {
async handleInteraction(interaction: Interaction<CacheType>) {
if (interaction.isChatInputCommand()) {
await this.handleChatInputCommand(interaction);
return;
}
}
async handleChatInputCommand(
interaction: ChatInputCommandInteraction<CacheType>,
) {
const commandName = interaction.commandName as CommandsType;
switch (commandName) {
case Commands.Enum.support:
await this.supportCommand(interaction);
return;
case Commands.Enum.kofi:
await this.kofiCommand(interaction);
return;
case Commands.Enum.disboard:
await this.disboardCommand(interaction);
return;
case Commands.Enum.discadia:
await this.discadiaCommand(interaction);
return;
default:
break;
}
}
async supportCommand(interaction: ChatInputCommandInteraction<CacheType>) {
console.log("supportCommand");
try {
await interaction.reply({
embeds: [supportContent],
ephemeral: true,
});
} catch (error) {
console.error("error while sending supportCommand msg:", error);
}
}
async kofiCommand(interaction: ChatInputCommandInteraction<CacheType>) {
console.log("kofiCommand");
try {
await interaction.reply({
embeds: [kofiContent],
ephemeral: true,
});
} catch (error) {
console.error("error while sending kofiCommand msg:", error);
}
}
async disboardCommand(interaction: ChatInputCommandInteraction<CacheType>) {
console.log("disboardCommand");
try {
await interaction.reply({
embeds: [disboardContent],
ephemeral: true,
});
} catch (error) {
console.error("error while sending disboardCommand msg:", error);
}
}
async discadiaCommand(interaction: ChatInputCommandInteraction<CacheType>) {
console.log("discadiaCommand");
try {
await interaction.reply({
embeds: [discadiaContent],
ephemeral: true,
});
} catch (error) {
console.error("error while sending discadiaCommand msg:", error);
}
}
}

View File

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

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

123
core/src/commands/index.ts Normal file
View File

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

@@ -0,0 +1,24 @@
import { EmbedBuilder } from "discord.js";
import { Commands, CommandsMeta } from "commands";
export default function getEmbed() { // channel.send({ embeds: [exampleEmbed] });
const exampleEmbed = new EmbedBuilder()
.setColor(0x0099FF)
.setTitle('Some title')
.setURL('https://discord.js.org/')
.setAuthor({ name: 'Some name', iconURL: 'https://i.imgur.com/AfFp7pu.png', url: 'https://discord.js.org' })
.setDescription('Some description here')
.setThumbnail('https://i.imgur.com/AfFp7pu.png')
.addFields(
{ name: Commands.Enum.giessen, value: CommandsMeta.giessen.description },
{ name: Commands.Enum.medikamente, value: CommandsMeta.medikamente.description },
{ name: Commands.Enum.hilfe, value: CommandsMeta.hilfe.description },
{ name: '\u200B', value: '\u200B' },
{ name: 'Inline field title', value: 'Some value here', inline: true },
)
.addFields({ name: 'Inline field title', value: 'Some value here', inline: true })
.setImage('https://i.imgur.com/AfFp7pu.png')
.setTimestamp()
.setFooter({ text: 'Some footer text here', iconURL: 'https://i.imgur.com/AfFp7pu.png' });
return exampleEmbed;
}

42
core/src/config.ts Normal file
View File

@@ -0,0 +1,42 @@
export default {
discord: {
version: 251218,
// avocadi
serverID: process.env.DISCORD_SERVER_ID || "",
// text channel
channelIdBump: process.env.DISCORD_CHANNEL_ID_BUMP || "",
channelIdBot: process.env.DISCORD_CHANNEL_ID_BOT || "",
channelIdFeedback: process.env.DISCORD_CHANNEL_ID_FEEDBACK || "",
channelIdLog: process.env.DISCORD_CHANNEL_ID_NOTIFICATION || "",
channelIdWelcome: process.env.DISCORD_CHANNEL_ID_WELCOME || "",
channelIdRules: process.env.DISCORD_CHANNEL_ID_RULE || "",
channelIdNews: process.env.DISCORD_CHANNEL_ID_NEWS || "",
channelIdIntroduction: process.env.DISCORD_CHANNEL_ID_INTRODUCTION || "",
channelIdOffTopic: process.env.DISCORD_CHANNEL_ID_OFF_TOPIC || "",
channelIdHelp: process.env.DISCORD_CHANNEL_ID_HELP || "",
channelIdPomodoro: process.env.DISCORD_CHANNEL_ID_POMODORO || "",
// voice channel
vchannelIdForTwo: process.env.DISCORD_VCHANNEL_ID_FOR_TWO || "",
vchannelIdForThree: process.env.DISCORD_VCHANNEL_ID_FOR_THREE || "",
vchannelIdForFour: process.env.DISCORD_VCHANNEL_ID_FOR_FOUR || "",
vchannelIdForGroup: process.env.DISCORD_VCHANNEL_ID_FOR_GROUP || "",
vchannelIdPomodoro25: process.env.DISCORD_VCHANNEL_ID_POMODORO_25_5 || "",
vchannelIdPomodoro50: process.env.DISCORD_VCHANNEL_ID_POMODORO_50_10 || "",
// roles
roleBot: process.env.BOT || "",
roleStudy: process.env.PEOPLE || "",
roleMod: process.env.MOD || "",
roleAdmin: process.env.ADMIN || "",
roleMention: process.env.MENTION || "",
rolesMsg: process.env.ROLES_NOTIFICATION_MSG || "",
rolesMsgTest: process.env.ROLES_MSG_TEST || "",
// other
applicationId: process.env.DISCORD_APPLICATION_ID || "",
token: process.env.DISCORD_TOKEN || "",
botId: process.env.BOT_ID || "",
myId: process.env.MY_ID || "",
enricoId: process.env.ENRICO_ID || "",
},
};

36
core/src/db/index.ts Normal file
View File

@@ -0,0 +1,36 @@
import fs from 'node:fs';
import path from 'node:path';
import { drizzle } from "drizzle-orm/bun-sqlite";
import { Database } from 'bun:sqlite';
import { usersTable } from './schema'; // Importiere die Tabelle hier
// biome-ignore lint/style/noNonNullAssertion: <explanation>
const dbPath = process.env.DB_FILE_NAME!;
const dbDir = path.dirname(dbPath);
// Erstelle das Verzeichnis, wenn es noch nicht existiert
if (!fs.existsSync(dbDir)) {
fs.mkdirSync(dbDir, { recursive: true });
}
// Wenn die Datenbankdatei nicht existiert, dann erstelle sie
if (!fs.existsSync(dbPath)) {
fs.writeFileSync(dbPath, ''); // Leere Datei erstellen
}
// Datenbankverbindung herstellen
const client = new Database(dbPath);
// Stelle sicher, dass die Tabelle existiert
client.run(`
CREATE TABLE IF NOT EXISTS users_table (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
discord_id INTEGER NOT NULL UNIQUE,
join_streak INTEGER,
last_joined_at INTEGER,
took_medication_today INTEGER NOT NULL DEFAULT 0
);
`);
export const db = drizzle(client);

10
core/src/db/schema.ts Normal file
View File

@@ -0,0 +1,10 @@
import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
export const usersTable = sqliteTable("users_table", {
id: integer().primaryKey({ autoIncrement: true }),
name: text().notNull(),
discord_id: integer().notNull().unique(),
join_streak: integer(),
last_joined_at: integer({ mode: "timestamp" }),
took_medication_today: integer().notNull().default(0),
});

View File

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

@@ -0,0 +1,26 @@
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,29 @@
import z from "zod";
export const TextChannelOptions = [
"bump",
"bot",
"notification",
"testing",
"news",
"rules",
"feedback",
"welcome",
"introduction",
"off-topic",
"help",
"pomodoro",
] as const;
export const VoiceChannelOptions = [
"for-two",
"for-three",
"for-four",
"for-group",
"custom",
"pomodoro-25-5",
"pomodoro-50-10",
] as const;
export const TextChannels = z.enum(TextChannelOptions);
export const VoiceChannels = z.enum(VoiceChannelOptions);

View File

@@ -0,0 +1,11 @@
import z from "zod";
export const RoleOptions = [
"admin",
"mention",
"mod",
"people",
"bot",
] as const;
export const Roles = z.enum(RoleOptions);

14
core/src/index.ts Normal file
View File

@@ -0,0 +1,14 @@
import "actions/waterMe/waterMe.task";
import "actions/greeting/greeting.task";
import "actions/medication/medication.task";
import "actions/drink/drink.task";
import DiscordController from "discord.controller";
import PomodoroController from "actions/pomodoro/pomodoro.controller";
import "dotenv/config";
// bootstrap application
const discordController = new DiscordController();
const pomodoroController = new PomodoroController();
discordController.init();

22
core/src/lib/client.ts Normal file
View File

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

View File

@@ -0,0 +1,13 @@
import { describe, expect, it } from "bun:test";
import { getRandomInt } from "./utils.ts";
describe("utils", () => {
it("gets a random int", () => {
const randomInt = getRandomInt(0, 10);
expect(randomInt).toBeDefined();
expect(randomInt).toBeNumber();
expect(randomInt).toBeLessThanOrEqual(10);
expect(randomInt).toBeGreaterThanOrEqual(0);
});
});

5
core/src/lib/utils.ts Normal file
View File

@@ -0,0 +1,5 @@
export function getRandomInt(min: number, max: number) {
const nextMin = Math.ceil(min);
const nextMax = Math.floor(max);
return Math.floor(Math.random() * (nextMax - nextMin + 1)) + nextMin;
}

View File

@@ -0,0 +1,13 @@
import config from "config";
import { GuildMember, GuildMemberRoleManager, type APIInteractionGuildMember } from "discord.js";
export async function checkPermission(member: GuildMember | APIInteractionGuildMember | null) {
let permission = false;
if (member?.roles instanceof GuildMemberRoleManager) {
if (member.roles.cache.has(config.discord.roleAdmin) || member.roles.cache.has(config.discord.roleMod)) {
permission = true;
}
}
console.log("user permission == " + permission);
return permission;
}