Compare commits

...

48 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
43 changed files with 1458 additions and 238 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

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

@@ -8,6 +8,13 @@
"when": 1735431085265,
"tag": "0000_needy_nightshade",
"breakpoints": true
},
{
"idx": 1,
"version": "6",
"when": 1736688734822,
"tag": "0001_dusty_wolverine",
"breakpoints": true
}
]
}

Binary file not shown.

View File

@@ -3,10 +3,12 @@
"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": "^1.1.14",
"better-sqlite3": "^11.7.0",
"drizzle-kit": "^0.30.1"
},
"peerDependencies": {
@@ -14,7 +16,6 @@
},
"dependencies": {
"@discordjs/rest": "^2.4.0",
"better-sqlite3": "^11.7.0",
"cron": "^3.3.1",
"discord.js": "^14.16.3",
"dotenv": "^16.4.7",

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

@@ -13,8 +13,24 @@ import {
} from "discord.js";
export class DmService {
async handleInteraction(interaction: Interaction<CacheType>) {
// todo ?
// 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) {
@@ -22,33 +38,66 @@ export class DmService {
try {
await client.users.send(member, dmWelcomeContent);
} catch (error) {
console.error("error while sending a welcome msg:", error);
}
}
async welcomePrivateTest() {
console.log("test welcome private");
try {
await client.users.send(config.discord.myId, 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>>) {
console.log("forward message");
if (message.channel.isDMBased()) {
const author = message.author.id;
const recipient = message.channel.recipient?.id;
console.log("forward message");
let context = "";
const context = `<@${message.author.id}> hat geschrieben: " ${message.content} "`;
if (message.author.bot) {
context = `<@${author}> hat an <@${recipient}> geschrieben:\n"${message.content}"`;
}
else {
context = `<@${author}> hat geschrieben:\n"${message.content}"`;
}
console.log(context);
client.users.send(config.discord.myId, context);
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("acceept dm");
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

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

@@ -1,26 +1,38 @@
import config from "config";
import { EmbedBuilder } from "discord.js";
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 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 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 createEmbed() {
// ({ embeds: [exampleEmbed] })
console.log("createEmbed()");
const exampleEmbed = new EmbedBuilder()
.setColor(0x004400)
.setAuthor({
name: "avocadi - neuigkeiten",
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(customContent)
.setTimestamp();
//.setFooter({ text: 'Some footer text here', iconURL: 'https://i.imgur.com/AfFp7pu.png' });
return exampleEmbed;
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

@@ -2,10 +2,9 @@ import config from "config";
import client from "lib/client";
import { getRandomInt } from "lib/utils";
import {
customContent,
getWelcomeContent,
greetContent,
sleepContent,
createEmbed,
} from "./greeting.components.ts";
import {
type ChatInputCommandInteraction,
@@ -14,8 +13,12 @@ import {
type CacheType,
type GuildMember,
type Interaction,
GuildMemberRoleManager,
type APIInteractionGuildMember,
} from "discord.js";
import { DmService } from "actions/dm/dm.service.ts";
import { Commands, type CommandsType } from "commands/index.ts";
import { checkPermission } from "permissions/index.ts";
export class GreetingService {
dmService: DmService;
@@ -27,36 +30,33 @@ export class GreetingService {
async handleInteraction(
interaction: Interaction<CacheType>
) {
console.log("accept");
if (interaction.isChatInputCommand()) {
await this.acceptUser(interaction);
await this.handleChatInputCommand(interaction);
return;
}
}
async custom() {
console.log("custom message");
client.users.send(config.discord.myId, "hat funktioniert :)");
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 welcomeContents = [
`willkommen auf dem server, ${member}! 💕`,
`hey ${member}! schoen, dass du hier bist! 😊`,
`hi ${member}, willkommen! viel spass hier! 💖`,
`willkommen, ${member}! schoen, dass du da bist! 🥳`,
`moin ${member}! viel spass im server! c:`,
`hey ${member}, herzlich willkommen! fuehl dich wie zu hause! <3`,
`hi ${member}! cool, dass du da bist! 👏`,
`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! ✨`,
];
const welcomeContent =
welcomeContents[Math.floor(Math.random() * welcomeContents.length)];
const welcomeContent = getWelcomeContent(member);
try {
const channels = client.channels;
@@ -65,6 +65,8 @@ export class GreetingService {
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);
}
@@ -81,10 +83,9 @@ export class GreetingService {
//console.log(input);
// permission check
const userIdCommand = interaction.user.id;
if (userIdCommand !== config.discord.myId) {
if (await checkPermission(interaction.member) !== true) {
await interaction.reply({
content: "you have no permission for that command",
content: "du hast keine rechte fuer diesen befehl",
ephemeral: true,
});
return;
@@ -103,14 +104,19 @@ export class GreetingService {
return;
}
const username = (await guild.members.fetch(userId)).user.username;
console.log(username);
// get member from id
const member = await guild.members.fetch(userId);
const username = member.user.username;
console.log(username);
const role = config.discord.roleStudy;
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(role);
await member.roles.add(config.discord.roleStudy);
await interaction.reply({
content: `die rolle *lernende:r* wurde erfolgreich an ${member.user.username} vergeben`,
@@ -128,6 +134,7 @@ export class GreetingService {
}
}
// unused
async greet() {
client.user?.setActivity("guten morgen! :3", { type: 4 });
console.log("set activity: awake");
@@ -137,13 +144,14 @@ export class GreetingService {
const channels = client.channels;
const channel = channels.cache.get(config.discord.testChannel);
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");
@@ -153,7 +161,7 @@ export class GreetingService {
const channels = client.channels;
const channel = channels.cache.get(config.discord.testChannel);
const channel = channels.cache.get(config.discord.channelIdOffTopic);
if (channel?.isTextBased && channel?.isSendable()) {
await channel.send({ content: this.getContent(true) });
@@ -162,14 +170,15 @@ export class GreetingService {
async newYear() {
client.user?.setActivity("frohes neues! :)", { type: 4 });
console.log("set activity: happy new Year");
console.log("set activity: happy new year");
client.user?.setPresence({
status: "online",
});
// unused
/*const channels = client.channels;
const channel = channels.cache.get(config.discord.channelId);
const channel = channels.cache.get(config.discord.channelIdOffTopic);
if (channel?.isTextBased && channel?.isSendable()) {
await channel.send({ content: "frohes neues! @everyone" });
@@ -182,4 +191,149 @@ export class GreetingService {
}
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

@@ -2,7 +2,7 @@ import { CronJob } from "cron";
import { GreetingService } from "actions/greeting/greeting.service";
const greetingService = new GreetingService();
/*
new CronJob(
"0 30 6 * * *", // cronTime
async () => {
@@ -24,7 +24,7 @@ new CronJob(
null,
true,
"Europe/Berlin",
);
);*/
new CronJob(
"0 0 0 1 1 *",

View File

@@ -2,18 +2,22 @@ import { Commands, CommandsMeta } from 'commands';
import { AttachmentBuilder, EmbedBuilder } from 'discord.js';
export default function createEmbed() { // ({ embeds: [exampleEmbed] })
console.log("createEmbed()");
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/moriese/avocadi-bot' })
.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' });

View File

@@ -15,6 +15,7 @@ export class HelpService {
if (interaction.isChatInputCommand()) {
await interaction.reply({
embeds: [this.exampleEmbed],
ephemeral: true,
});
}
}

View File

@@ -115,6 +115,7 @@ export class MedicationService {
await interaction.reply({
content:
"es gab einen fehler beim verarbeiten deiner anfrage :( versuch es bitte spaeter nochmal, oki? c:",
ephemeral: true,
});
return;
}

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

@@ -2,7 +2,7 @@ import { CronJob } from "cron";
import { WaterMeService } from "actions/waterMe/waterMe.service";
const waterMeService = new WaterMeService();
/*
new CronJob(
"0 0 20 * * *", // cronTime
async () => {
@@ -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.

View File

@@ -1,7 +1,7 @@
import { SlashCommandBuilder, userMention } from "discord.js";
import { z } from "zod";
export const Commands = z.enum(["giessen", "medikamente", "hilfe", "accept"]);
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: {
@@ -13,8 +13,35 @@ export const CommandsMeta: Record<z.output<typeof Commands>, { description: stri
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"
}
}
@@ -31,6 +58,18 @@ export default function getCommands() {
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)
@@ -38,6 +77,46 @@ export default function getCommands() {
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,18 +1,34 @@
export default {
discord: {
// test
testChannel: process.env.DISCORD_TEST_CHANNEL_ID || "",
channelIdBot: process.env.DISCORD_CHANNEL_ID_BOT || "",
version: 250602.10,
// avocadi
serverID: process.env.DISCORD_SERVER_ID || "",
channelIdNews: process.env.DISCORD_CHANNEL_ID_NEWS || "",
// texxt channel
channelIdBot: process.env.DISCORD_CHANNEL_ID_BOT || "",
channelIdFeedback: process.env.DISCORD_CHANNEL_ID_FEEDBACK || "",
channelIdIntroduction: process.env.DISCORD_CHANNEL_ID_INTRODUCTION || "",
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 || "",

View File

@@ -1,141 +0,0 @@
import { Commands, type CommandsType } from "commands";
import {
ChannelType,
Client,
Events,
IntentsBitField,
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 "services/discord.service";
import config from "config";
import { WaterMeService } from "actions/waterMe/waterMe.service";
import { MedicationService } from "actions/medication/medication.service";
import { HelpService } from "actions/help/help.service";
import { custom } from "zod";
import { GreetingService } from "actions/greeting/greeting.service";
import { ActivityService } from "actions/activity/activity.service";
import { DmService } from "actions/dm/dm.service";
export default class DiscordController extends EventEmitter {
private discordService!: DiscordService;
waterMeService: WaterMeService;
greetingService: GreetingService;
medicationService: MedicationService;
helpService: HelpService;
activityService: ActivityService;
dmService: DmService;
constructor() {
super();
this.discordService = new DiscordService();
this.waterMeService = new WaterMeService();
this.greetingService = new GreetingService();
this.medicationService = new MedicationService();
this.helpService = new HelpService();
this.activityService = new ActivityService();
this.dmService = new DmService();
// 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
&& message.author.id !== config.discord.botId
) {
console.log("got msg");
this.dmService.forward(message);
}
});
client.on("guildMemberAdd", async (member) => {
await this.greetingService.welcome(member);
await this.dmService.welcomePrivate(member);
});
}
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);
return;
case Commands.Enum.medikamente:
await this.medicationService.handleInteraction(interaction);
return;
case Commands.Enum.hilfe:
await this.helpService.handleInteraction(interaction);
return;
case Commands.Enum.accept:
await this.greetingService.handleInteraction(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,4 +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>
export const db = drizzle(process.env.DB_FILE_NAME!);
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);

View File

@@ -1,8 +1,10 @@
import { int, sqliteTable, text } from "drizzle-orm/sqlite-core";
import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
export const usersTable = sqliteTable("users_table", {
id: int().primaryKey({ autoIncrement: true }),
name: text().notNull(),
discord_id: int().notNull().unique(),
took_medication_today: int().notNull().default(0),
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

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

View File

@@ -8,14 +8,13 @@ const client = new Client({
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,
GatewayIntentBits.DirectMessages,
GatewayIntentBits.MessageContent],
partials: [Partials.Channel, Partials.Message]
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;
}