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

3
core/.dockerignore Normal file
View File

@@ -0,0 +1,3 @@
node_modules
prod.sqlite
mydb.sqlite

32
core/Dockerfile Normal file
View File

@@ -0,0 +1,32 @@
# use the official Bun image
# see all versions at https://hub.docker.com/r/oven/bun/tags
FROM oven/bun:1 AS base
WORKDIR /usr/src/app
# install dependencies into temp directory
# this will cache them and speed up future builds
FROM base AS install
# install with --production (exclude devDependencies)
RUN mkdir -p /temp/prod
COPY package.json bun.lockb /temp/prod/
RUN cd /temp/prod && bun install --frozen-lockfile --production
# copy node_modules from temp directory
# then copy all (non-ignored) project files into the image
FROM base AS prerelease
COPY --from=install /temp/prod/node_modules node_modules
COPY . .
# [optional] tests & build
ENV NODE_ENV=production
RUN bun test
#RUN bun run build
# copy production dependencies and source code into final image
FROM base AS release
COPY --from=install /temp/prod/node_modules node_modules
COPY --from=prerelease /usr/src/app/ .
# run the app
USER bun
ENTRYPOINT [ "bun", "run", "./src/index.ts" ]

9
core/LICENSE Normal file
View File

@@ -0,0 +1,9 @@
MIT License
Copyright (c) 2024 mo (mo@unom.io)
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

3
core/README.md Normal file
View File

@@ -0,0 +1,3 @@
# avocadi-bot
i am trying to write a cute but still somehow minimalistic bot, that could be used as a study technique, especially for mentally ill people or people that struggle in life (me) + trying to stay focused on one task (difficulty: impossible).

8
core/docker-compose.yml Normal file
View File

@@ -0,0 +1,8 @@
services:
avocadi-bot:
container_name: avocadi-bot
image: git.unom.io/mo/avocadi-bot:latest
restart: unless-stopped
volumes:
- ./.env:/usr/src/app/.env
- ./prod.sqlite:/usr/src/app/prod.sqlite:rw

17
core/drizzle.config.ts Normal file
View File

@@ -0,0 +1,17 @@
import "dotenv/config";
import { defineConfig } from "drizzle-kit";
const dbFileName = process.env.DB_FILE_NAME;
if (!dbFileName) {
throw new Error("Die Umgebungsvariable DB_FILE_NAME ist nicht gesetzt.");
}
export default defineConfig({
out: "./drizzle",
schema: "./src/db/schema.ts",
dialect: "sqlite",
dbCredentials: {
url: dbFileName,
},
});

View File

@@ -0,0 +1,8 @@
CREATE TABLE `users_table` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`name` text NOT NULL,
`discord_id` integer NOT NULL,
`took_medication_today` integer DEFAULT 0 NOT NULL
);
--> statement-breakpoint
CREATE UNIQUE INDEX `users_table_discord_id_unique` ON `users_table` (`discord_id`);

View File

@@ -0,0 +1,2 @@
ALTER TABLE `users_table` ADD `join_streak` integer;--> statement-breakpoint
ALTER TABLE `users_table` ADD `last_joined_at` integer;

View File

@@ -0,0 +1,65 @@
{
"version": "6",
"dialect": "sqlite",
"id": "ddb7170b-7f66-4e3c-ab64-9069e760e09a",
"prevId": "00000000-0000-0000-0000-000000000000",
"tables": {
"users_table": {
"name": "users_table",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"discord_id": {
"name": "discord_id",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"took_medication_today": {
"name": "took_medication_today",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": 0
}
},
"indexes": {
"users_table_discord_id_unique": {
"name": "users_table_discord_id_unique",
"columns": [
"discord_id"
],
"isUnique": true
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
}
},
"views": {},
"enums": {},
"_meta": {
"schemas": {},
"tables": {},
"columns": {}
},
"internal": {
"indexes": {}
}
}

View File

@@ -0,0 +1,79 @@
{
"version": "6",
"dialect": "sqlite",
"id": "e9592066-1da0-41af-9e25-e2672ebe256f",
"prevId": "ddb7170b-7f66-4e3c-ab64-9069e760e09a",
"tables": {
"users_table": {
"name": "users_table",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"discord_id": {
"name": "discord_id",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"join_streak": {
"name": "join_streak",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"last_joined_at": {
"name": "last_joined_at",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"took_medication_today": {
"name": "took_medication_today",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": 0
}
},
"indexes": {
"users_table_discord_id_unique": {
"name": "users_table_discord_id_unique",
"columns": [
"discord_id"
],
"isUnique": true
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
}
},
"views": {},
"enums": {},
"_meta": {
"schemas": {},
"tables": {},
"columns": {}
},
"internal": {
"indexes": {}
}
}

View File

@@ -0,0 +1,20 @@
{
"version": "7",
"dialect": "sqlite",
"entries": [
{
"idx": 0,
"version": "6",
"when": 1735431085265,
"tag": "0000_needy_nightshade",
"breakpoints": true
},
{
"idx": 1,
"version": "6",
"when": 1736688734822,
"tag": "0001_dusty_wolverine",
"breakpoints": true
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

41
core/package.json Normal file
View File

@@ -0,0 +1,41 @@
{
"name": "@avocadi/bot-core",
"type": "module",
"scripts": {
"dev": "tsdown --watch",
"test": "bun test",
"build": "tsdown"
},
"devDependencies": {
"@types/bun": "^1.1.14",
"better-sqlite3": "^11.7.0",
"drizzle-kit": "^0.30.1",
"tsdown": "catalog:"
},
"peerDependencies": {
"typescript": "^5.7.2"
},
"dependencies": {
"@discordjs/rest": "^2.4.0",
"cron": "^3.3.1",
"discord.js": "^14.16.3",
"dotenv": "^16.4.7",
"drizzle-orm": "^0.38.3",
"zod": "catalog:"
},
"trustedDependencies": [
"better-sqlite3",
"esbuild"
],
"exports": {
".": "./dist/index.js",
"./db": "./dist/db/index.js",
"./db/schema": "./dist/db/schema.js",
"./entities/channels/channels.schema": "./dist/entities/channels/channels.schema.js",
"./entities/roles/roles.schema": "./dist/entities/roles/roles.schema.js",
"./lib/client": "./dist/lib/client.js",
"./lib/utils": "./dist/lib/utils.js",
"./lib/utils.test": "./dist/lib/utils.test.js",
"./package.json": "./package.json"
}
}

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

27
core/tsconfig.json Normal file
View File

@@ -0,0 +1,27 @@
{
"compilerOptions": {
// Enable latest features
"lib": ["ESNext", "DOM"],
"target": "ESNext",
"module": "ESNext",
"moduleDetection": "force",
"jsx": "react-jsx",
"allowJs": true,
"baseUrl": "src",
// Bundler mode
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"noEmit": true,
// Best practices
"strict": true,
"skipLibCheck": true,
"noFallthroughCasesInSwitch": true,
// Some stricter flags (disabled by default)
"noUnusedLocals": false,
"noUnusedParameters": false,
"noPropertyAccessFromIndexSignature": false
}
}

15
core/tsdown.config.ts Normal file
View File

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