Compare commits

...

58 Commits

Author SHA1 Message Date
7a1f02ebd0 name fix docker-compose
All checks were successful
release-tag / release-image (push) Successful in 22s
2025-06-02 11:29:56 +02:00
f6bceb215c pomodoro fix
All checks were successful
release-tag / release-image (push) Successful in 17s
2025-06-02 10:38:08 +02:00
9914cd2bfb add version command
All checks were successful
release-tag / release-image (push) Successful in 18s
2025-06-02 10:28:37 +02:00
fd2d5f5bff changed name to mo
All checks were successful
release-tag / release-image (push) Successful in 1m33s
2025-06-02 10:18:07 +02:00
762bb989a2 version
All checks were successful
release-tag / release-image (push) Successful in 19s
2025-05-10 00:38:06 +02:00
4031291c38 fixed bug when switching from one pomodoro channel to other
All checks were successful
release-tag / release-image (push) Successful in 18s
2025-05-10 00:36:29 +02:00
fca6927b08 changed version nr
All checks were successful
release-tag / release-image (push) Successful in 18s
2025-05-10 00:21:29 +02:00
56ec6a1ad8 added pomodoro function
All checks were successful
release-tag / release-image (push) Successful in 56s
2025-05-10 00:20:29 +02:00
c1f0ae670d index.ts fix 2025-05-07 23:45:52 +02:00
034d1c076c namefix channel -> vchannel 2025-05-07 23:38:03 +02:00
0d757fef79 added version log
All checks were successful
release-tag / release-image (push) Successful in 27s
2025-02-22 23:19:29 +01:00
ee73dc140a commands support, kofi, disboard, discardia
All checks were successful
release-tag / release-image (push) Successful in 33s
2025-02-18 20:58:25 +01:00
f77e292e27 fixed typo
All checks were successful
release-tag / release-image (push) Successful in 23s
2025-01-25 23:59:46 +01:00
e10bbe4fb4 remove mydb.sqlite
All checks were successful
release-tag / release-image (push) Successful in 32s
2025-01-25 15:14:55 +01:00
94cc914926 i try to fix lmao
All checks were successful
release-tag / release-image (push) Successful in 21s
2025-01-25 14:47:23 +01:00
3b782c02fe now?????????????
All checks were successful
release-tag / release-image (push) Successful in 23s
2025-01-25 02:11:50 +01:00
307027846c fixed mybe?
All checks were successful
release-tag / release-image (push) Successful in 22s
2025-01-25 02:05:42 +01:00
75a2a9b94c reactionRole Mention added
All checks were successful
release-tag / release-image (push) Successful in 30s
2025-01-25 01:39:27 +01:00
eabe37c280 removed tasks
All checks were successful
release-tag / release-image (push) Successful in 22s
2025-01-15 20:24:04 +01:00
f6bbe10bb9 fixed reminder msg
All checks were successful
release-tag / release-image (push) Successful in 21s
2025-01-14 19:32:04 +01:00
76525b73c9 added reminder cmd + fixed custom channel
All checks were successful
release-tag / release-image (push) Successful in 22s
2025-01-14 19:23:03 +01:00
3852ff922e remove mydb sqlite
All checks were successful
release-tag / release-image (push) Successful in 24s
2025-01-13 02:13:16 +01:00
ddb85f10d8 bug fixed
All checks were successful
release-tag / release-image (push) Successful in 22s
2025-01-13 01:55:47 +01:00
3f7864be1b able to duplicate channel with only joining
All checks were successful
release-tag / release-image (push) Successful in 24s
2025-01-13 01:53:40 +01:00
6193865220 remove watchtower from docker compose again
All checks were successful
release-tag / release-image (push) Successful in 20s
2025-01-13 01:42:17 +01:00
50d8d9347c remove unused imports
All checks were successful
release-tag / release-image (push) Successful in 23s
2025-01-13 01:24:05 +01:00
0341a70746 add restart unless stopped to compose
All checks were successful
release-tag / release-image (push) Successful in 22s
add auto update using containerr watchtower
2025-01-13 01:21:58 +01:00
6f0a66d51d remove old db
All checks were successful
release-tag / release-image (push) Successful in 24s
2025-01-13 00:12:54 +00:00
36353aa8a7 add rw
Some checks failed
release-tag / release-image (push) Has been cancelled
2025-01-13 01:11:17 +01:00
49338d1e5c added getWelcomeContent
All checks were successful
release-tag / release-image (push) Successful in 21s
2025-01-12 23:20:32 +01:00
44e6308cc2 fix entrypoint
All checks were successful
release-tag / release-image (push) Successful in 22s
2025-01-12 21:33:08 +01:00
2113755464 fix compose file
All checks were successful
release-tag / release-image (push) Successful in 1m28s
2025-01-12 21:30:34 +01:00
646227fab1 add composefile
All checks were successful
release-tag / release-image (push) Successful in 24s
ignore prod db
2025-01-12 21:27:08 +01:00
907c730172 remove better-sqlite from main deps
All checks were successful
release-tag / release-image (push) Successful in 1m21s
rebuild lock
remove dev deps from dockerfile
2025-01-12 21:15:29 +01:00
0e0317ad6b add latest tag
Some checks failed
release-tag / release-image (push) Failing after 1m53s
2025-01-12 21:02:50 +01:00
1d64e38fd0 add python3
All checks were successful
release-tag / release-image (push) Successful in 18m20s
2025-01-12 20:43:25 +01:00
b6d71eaac0 fix ci
Some checks failed
release-tag / release-image (push) Failing after 3m54s
2025-01-12 20:38:33 +01:00
2022590fb8 fix output name
Some checks failed
release-tag / release-image (push) Failing after 12s
2025-01-12 20:36:35 +01:00
6ca809b175 Merge branch 'main' of git.unom.io:moriese/avocadi-bot
Some checks failed
release-tag / release-image (push) Failing after 2m35s
2025-01-12 20:33:12 +01:00
f928b75678 add ci
containerize
2025-01-12 20:33:10 +01:00
d902cd1042 wip 2025-01-12 19:55:19 +01:00
6707a95648 wip 2025-01-10 14:50:19 +01:00
02b67c6661 test 2025-01-10 11:09:07 +01:00
Moritz Riese
2f6182b588 wip 2025-01-10 10:56:28 +01:00
db6cdfcbb8 fix bug 2025-01-09 23:19:27 +01:00
b8365abce8 fixed bug welcomeCommand 2025-01-09 22:57:50 +01:00
62b3eb070b added createEmbed 2025-01-07 00:31:01 +01:00
78a65511c2 not much 2025-01-06 15:17:51 +01:00
94d2fc40e6 good progress :3 2025-01-05 03:51:02 +01:00
07aa025ba0 wip :) 2025-01-04 01:57:35 +01:00
edacc747b3 added welcome msg 2025-01-03 23:56:37 +01:00
2592aea260 custom message added 2025-01-02 19:56:12 +01:00
b46dc8d3ae added "newYear" 2025-01-01 18:53:33 +01:00
e7cd840c48 added embed for /hilfe 2024-12-31 01:28:46 +01:00
8935e141d2 wip 2024-12-30 22:45:41 +01:00
Moritz Riese
182ce5af6b wip db 2024-12-29 01:20:01 +01:00
Moritz Riese
90341b277c Merge branch 'main' of git.unom.io:moriese/avocadi-bot 2024-12-28 20:09:57 +01:00
Moritz Riese
03d36358bf wip medication and db (sqlite) 2024-12-28 20:09:43 +01:00
59 changed files with 2246 additions and 198 deletions

3
.dockerignore Normal file
View File

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

View File

@@ -0,0 +1,48 @@
name: release-tag
on: push
jobs:
release-image:
runs-on: ubuntu-latest
container:
image: catthehacker/ubuntu:act-latest
# env:
# DOCKER_ORG: teacup
# DOCKER_LATEST: nightly
# RUNNER_TOOL_CACHE: /toolcache
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker BuildX
uses: docker/setup-buildx-action@v2
- name: Login to DockerHub
uses: docker/login-action@v2
with:
registry: git.unom.io # replace it with your local IP
username: ${{ vars.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_TOKEN }}
- name: Get Meta
id: meta
run: |
echo REPO_NAME=$(echo ${GITHUB_REPOSITORY} | awk -F"/" '{print $2}') >> $GITHUB_OUTPUT
echo REPO_VERSION=$(git describe --tags --always | sed 's/^v//') >> $GITHUB_OUTPUT
- name: Build and push
uses: docker/build-push-action@v4
with:
context: .
file: ./Dockerfile
platforms: |
linux/amd64
linux/arm64
push: true
tags: | # replace it with your local IP and tags
git.unom.io/mo/${{ steps.meta.outputs.REPO_NAME }}:${{ steps.meta.outputs.REPO_VERSION }}
git.unom.io/mo/${{ steps.meta.outputs.REPO_NAME }}:latest

4
.gitignore vendored
View File

@@ -173,3 +173,7 @@ dist
# Finder (MacOS) folder config
.DS_Store
# DB
avocadis_diary.sqlite
prod.sqlite

32
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" ]

View File

@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2024 moriese
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:

BIN
bun.lockb

Binary file not shown.

8
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
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

View File

@@ -3,18 +3,27 @@
"module": "src/index.ts",
"type": "module",
"scripts": {
"dev": "bun --watch src/index.ts"
"dev": "bun --watch src/index.ts",
"test": "bun test"
},
"devDependencies": {
"@types/bun": "latest"
"@types/bun": "^1.1.14",
"better-sqlite3": "^11.7.0",
"drizzle-kit": "^0.30.1"
},
"peerDependencies": {
"typescript": "^5.0.0"
"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": "^3.24.1"
}
},
"trustedDependencies": [
"better-sqlite3",
"esbuild"
]
}

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

@@ -5,7 +5,7 @@ export class GreetingService {
async greet() {
const channels = client.channels;
const channel = channels.cache.get(config.discord.channelId);
const channel = channels.cache.get(config.discord.channelIdBot);
if (channel?.isTextBased && channel?.isSendable()) {
await channel.send({ content: "HALLOOOO" });

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

@@ -52,7 +52,7 @@ export class WaterMeService {
async isThirsty() {
const channels = client.channels;
const channel = channels.cache.get(config.discord.channelId);
const channel = channels.cache.get(config.discord.channelIdBot);
if (
channel?.isTextBased &&

View File

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

123
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

@@ -1,15 +0,0 @@
import { SlashCommandBuilder } from "discord.js";
import { z } from "zod";
export const Commands = z.enum(["water-me"]);
export type CommandsType = z.output<typeof Commands>;
export default function getCommands() {
const commands = [
new SlashCommandBuilder()
.setName(Commands.Enum["water-me"])
.setDescription("giess mich mit etwas wasser :3"),
].map((command) => command.toJSON());
return commands;
}

24
src/components/index.ts Normal file
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;
}

View File

@@ -1,12 +1,40 @@
const test = true;
export default {
discord: {
channelId:
(test
? process.env.DISCORD_TEST_CHANNEL_ID
: process.env.DISCORD_CHANNEL_ID) || "",
version: 250602.10,
// avocadi
serverID: process.env.DISCORD_SERVER_ID || "",
// texxt channel
channelIdBot: process.env.DISCORD_CHANNEL_ID_BOT || "",
channelIdFeedback: process.env.DISCORD_CHANNEL_ID_FEEDBACK || "",
channelIdNotification: 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
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 || "",
},
};

View File

@@ -1,93 +0,0 @@
import { Commands, type CommandsType } from "components/commands.component";
import type {
ButtonInteraction,
CacheType,
ChatInputCommandInteraction,
Interaction,
ModalSubmitInteraction,
} from "discord.js";
import client from "lib/client";
import EventEmitter from "node:events";
import DiscordService from "services/discord.service";
import { WaterMeService } from "services/water-me.service";
import config from "config";
export default class DiscordController extends EventEmitter {
private discordService!: DiscordService;
waterMeService: WaterMeService;
constructor() {
super();
this.discordService = new DiscordService();
this.waterMeService = new WaterMeService();
// log when running
client.once("ready", async () => {
console.log("hello :)");
/*const channels = client.channels;
const channel = channels.cache.get(config.discord.channelId);
if (channel?.isTextBased && channel?.isSendable()) {
await channel.send({
content: "bin gerade erst aufgewacht :o",
});
}*/
});
// listen for interactions
client.on("interactionCreate", this.handleInteraction.bind(this));
}
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 === "moreWater") {
await this.waterMeService.handleInteraction(interaction);
}
}
async handleChatInputCommand(
interaction: ChatInputCommandInteraction<CacheType>,
) {
const commandName = interaction.commandName as CommandsType;
// add commands
switch (commandName) {
case Commands.Enum["water-me"]:
await this.waterMeService.handleInteraction(interaction);
return;
default:
break;
}
}
// wenn neues fenster durch buttonclick or so
async handleModalSubmit(interaction: ModalSubmitInteraction<CacheType>) {
const { customId } = interaction;
switch (customId) {
default:
break;
}
}
}

36
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
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),
});

239
src/discord.controller.ts Normal file
View File

@@ -0,0 +1,239 @@
import { Commands, type CommandsType } from "commands";
import {
ChannelType,
Client,
Events,
IntentsBitField,
type VoiceState,
type ButtonInteraction,
type CacheType,
type ChatInputCommandInteraction,
type Interaction,
type ModalSubmitInteraction,
} 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 () => {
await this.setActivity();
console.log("ready");
});
// 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() {
client.user?.setActivity(":3", { type: 4 });
console.log("set activity");
client.user?.setPresence({
status: "online",
});
}
async init() {
await this.discordService.init();
}
async handleInteraction(interaction: Interaction<CacheType>) {
if (interaction.isModalSubmit()) {
await this.handleModalSubmit(interaction);
return;
}
if (interaction.isChatInputCommand()) {
await this.handleChatInputCommand(interaction);
return;
}
if (interaction.isButton()) {
await this.handleButton(interaction);
return;
}
}
async handleButton(interaction: ButtonInteraction<CacheType>) {
const { customId } = interaction;
console.log(interaction.customId);
if (customId.toLowerCase().includes("moreWater")) {
await this.waterMeService.handleInteraction(interaction);
}
if (customId.toLowerCase().includes("medication")) {
await this.medicationService.handleInteraction(interaction);
}
}
async handleChatInputCommand(
interaction: ChatInputCommandInteraction<CacheType>,
) {
const commandName = interaction.commandName as CommandsType;
// add commands
switch (commandName) {
case Commands.Enum.giessen:
await this.waterMeService.handleInteraction(interaction); // zu chatinputcommand wechseln
return;
case Commands.Enum.medikamente:
await this.medicationService.handleChatInputCommand(interaction);
return;
case Commands.Enum.hilfe:
await this.helpService.handleInteraction(interaction); // zu chatinputcommand wechseln
return;
case Commands.Enum.support:
case Commands.Enum.kofi:
case Commands.Enum.disboard:
case Commands.Enum.discadia:
await this.supportService.handleInteraction(interaction);
return;
case Commands.Enum.accept:
await this.greetingService.handleChatInputCommand(interaction);
return;
case Commands.Enum.welcome:
await this.greetingService.handleChatInputCommand(interaction);
return;
case Commands.Enum.embed:
case Commands.Enum.message:
await this.customMessageService.handleChatInputCommand(interaction);
return;
case Commands.Enum.reminder:
await this.greetingService.handleChatInputCommand(interaction);
return;
case Commands.Enum.version:
await this.debugService.handleChatInputCommand(interaction);
return;
default:
break;
}
}
// wenn neues fenster durch buttonclick or so
async handleModalSubmit(interaction: ModalSubmitInteraction<CacheType>) {
const { customId } = interaction;
switch (customId) {
default:
break;
}
}
}

View File

@@ -1,7 +1,7 @@
import { Routes } from "discord.js";
import { REST } from "@discordjs/rest";
import config from "config";
import getCommands from "components/commands.component";
import getCommands from "commands";
export default class DiscordService {
rest: REST;

View File

@@ -1,11 +1,14 @@
import "tasks/water-me.task";
import "tasks/greeting.task";
import "tasks/drink.task";
import DiscordController from "controllers/discord.controller";
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";
// = main file
import "dotenv/config";
// bootstrap application
const discordController = new DiscordController();
discordController.init();
const pomodoroController = new PomodoroController();
discordController.init();

View File

@@ -1,7 +1,21 @@
import config from "config";
import { Client, IntentsBitField } from "discord.js";
import { Client, GatewayIntentBits, Partials, ChannelType, Events, IntentsBitField } from "discord.js";
const client = new Client({ intents: [IntentsBitField.Flags.Guilds] });
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);

13
src/lib/utils.test.ts Normal file
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);
});
});

13
src/permissions/index.ts Normal file
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;
}

View File

@@ -1,35 +0,0 @@
import config from "config";
import client from "lib/client";
import { getRandomInt } from "lib/utils";
export class GreetingService {
private greetContent = ["HALLOOOO"] as const;
private sleepContent = ["zzzzZZ..", "*schnarch*"] as const;
getContent(asleep: boolean) {
if (asleep) {
return this.sleepContent[getRandomInt(0, this.sleepContent.length - 1)];
}
return this.greetContent[getRandomInt(0, this.greetContent.length - 1)];
}
async greet() {
const channels = client.channels;
const channel = channels.cache.get(config.discord.channelId);
if (channel?.isTextBased && channel?.isSendable()) {
await channel.send({ content: "HALLOOOO" });
}
}
async sleep() {
const channels = client.channels;
const channel = channels.cache.get(config.discord.channelId);
if (channel?.isTextBased && channel?.isSendable()) {
await channel.send({ content: this.getContent(true) });
}
}
}

View File

@@ -1,27 +0,0 @@
import { CronJob } from "cron";
import { GreetingService } from "services/greeting.service";
const greetingService = new GreetingService();
new CronJob(
"0 */30 7-23 * * *", // cronTime
async () => {
console.log("called greeting");
await greetingService.greet();
}, // onTick
null, // onComplete
true, // start
"Europe/Berlin", // timeZone
);
new CronJob(
"0 */30 0-6 * * *", // cronTime
async () => {
console.log("called greeting");
await greetingService.sleep();
}, // onTick
null, // onComplete
true, // start
"Europe/Berlin", // timeZone
);
// job.start() is optional here because of the fourth parameter set to true.