begin rewriting first services to be domain agnostic
more rewrites
This commit is contained in:
2
adapters/discord/.gitignore
vendored
2
adapters/discord/.gitignore
vendored
@@ -21,6 +21,8 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.local
|
||||
.env.dev
|
||||
.env.prod
|
||||
|
||||
# caches
|
||||
.eslintcache
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
console.log("Hello via Bun!");
|
||||
@@ -1,16 +1,30 @@
|
||||
{
|
||||
"name": "@avocadi/bot-adapter-discord",
|
||||
"module": "index.ts",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "tsdown",
|
||||
"dev:prod": "NODE_ENV=production tsdown --watch & node --watch ./dist/index.js",
|
||||
"dev": "NODE_ENV=development tsdown --watch & node --watch ./dist/index.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest"
|
||||
"@types/bun": "latest",
|
||||
"tsdown": "catalog:"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5"
|
||||
},
|
||||
"dependencies": {
|
||||
"zod": "catalog:",
|
||||
"@avocadi/bot-core": "workspace:*"
|
||||
"@avocadi/bot-core": "workspace:*",
|
||||
"@discordjs/rest": "^2.6.0",
|
||||
"cron": "^4.4.0",
|
||||
"discord.js": "^14.25.1",
|
||||
"dotenv": "^17.3.1",
|
||||
"dotenv-expand": "^12.0.3",
|
||||
"zod": "catalog:"
|
||||
},
|
||||
"exports": {
|
||||
".": "./dist/index.js",
|
||||
"./package.json": "./package.json"
|
||||
}
|
||||
}
|
||||
|
||||
18
adapters/discord/src/actions/publish-commands.ts
Normal file
18
adapters/discord/src/actions/publish-commands.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { config } from "config";
|
||||
import { Routes } from "discord.js";
|
||||
import getCommands from "entitites/commands";
|
||||
import { discordRestClient } from "lib/rest-client";
|
||||
|
||||
export const publishCommands = async () => {
|
||||
try {
|
||||
await discordRestClient.put(
|
||||
Routes.applicationCommands(config.discord.applicationId),
|
||||
{
|
||||
body: getCommands(),
|
||||
},
|
||||
);
|
||||
console.log("Successfully added commands");
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
};
|
||||
@@ -11,6 +11,9 @@ export const ConfigSchema = z.object({
|
||||
voice: z.record(VoiceChannels, z.string()),
|
||||
}),
|
||||
roleMapping: z.record(Roles, z.string()),
|
||||
reactionRoles: z.object({
|
||||
allowedMessageIds: z.array(z.string()),
|
||||
}),
|
||||
serverId: z.string(),
|
||||
version: z.number(),
|
||||
discord: z.object({
|
||||
|
||||
6
adapters/discord/src/config/env/env.schema.ts
vendored
Normal file
6
adapters/discord/src/config/env/env.schema.ts
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
import z from "zod";
|
||||
|
||||
export const EnvSchema = z.object({
|
||||
DISCORD_APPLICATION_ID: z.string(),
|
||||
DISCORD_TOKEN: z.string(),
|
||||
});
|
||||
29
adapters/discord/src/config/env/index.ts
vendored
Normal file
29
adapters/discord/src/config/env/index.ts
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import dotenv from "dotenv";
|
||||
import dotenvExpand from "dotenv-expand";
|
||||
import { EnvSchema } from "./env.schema";
|
||||
|
||||
const envFile =
|
||||
process.env.NODE_ENV === "production" ? ".env.prod" : ".env.dev";
|
||||
|
||||
const envPath = join(process.cwd(), envFile);
|
||||
|
||||
const rawEnv = Buffer.from(
|
||||
readFileSync(envPath, {
|
||||
encoding: "utf8",
|
||||
}),
|
||||
);
|
||||
|
||||
const envJson = { processEnv: dotenv.parse(rawEnv) };
|
||||
|
||||
const targetObj = {};
|
||||
|
||||
const expandedEnv = dotenvExpand.expand({
|
||||
processEnv: targetObj,
|
||||
parsed: envJson.processEnv,
|
||||
}) as { processEnv: Record<string, string> };
|
||||
|
||||
const env = EnvSchema.parse(expandedEnv.processEnv);
|
||||
|
||||
export default env;
|
||||
@@ -1,5 +1,6 @@
|
||||
import type z from "zod";
|
||||
import type { ConfigSchema } from "./config.schema";
|
||||
import env from "./env";
|
||||
|
||||
export const config: z.output<typeof ConfigSchema> = {
|
||||
channelMapping: {
|
||||
@@ -34,10 +35,15 @@ export const config: z.output<typeof ConfigSchema> = {
|
||||
people: "1321470720424939662",
|
||||
bot: "1321491461111283722",
|
||||
},
|
||||
reactionRoles: {
|
||||
allowedMessageIds: [
|
||||
"1321491461111283722", // Example message ID for reaction roles
|
||||
],
|
||||
},
|
||||
serverId: "1316153371899592774",
|
||||
version: 1,
|
||||
discord: {
|
||||
token: process.env.DISCORD_TOKEN || "",
|
||||
applicationId: process.env.DISCORD_APPLICATION_ID || "",
|
||||
token: env.DISCORD_TOKEN,
|
||||
applicationId: env.DISCORD_APPLICATION_ID,
|
||||
},
|
||||
};
|
||||
245
adapters/discord/src/discord.controller.ts
Normal file
245
adapters/discord/src/discord.controller.ts
Normal file
@@ -0,0 +1,245 @@
|
||||
import EventEmitter from "node:events";
|
||||
import {
|
||||
type WaterMeController,
|
||||
waterMeController,
|
||||
} from "features/water-me/water-me.controller";
|
||||
import client from "lib/client";
|
||||
|
||||
export default class DiscordController extends EventEmitter {
|
||||
private waterMeController: WaterMeController = waterMeController;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
let channelListeners = new Map();
|
||||
|
||||
// log when running
|
||||
client.once("ready", async () => {
|
||||
const channels = client.channels;
|
||||
const logChannel = channels.cache.get(config.discord.channelIdLog);
|
||||
|
||||
if (logChannel?.isTextBased() && logChannel?.isSendable()) {
|
||||
try {
|
||||
console.log("bot is online");
|
||||
await logChannel.send("wieder online!!!");
|
||||
} catch (error) {
|
||||
console.error("failed to send online message:", error);
|
||||
}
|
||||
} else {
|
||||
console.error("log channel is not valid or sendable.");
|
||||
}
|
||||
await this.setActivity(100);
|
||||
console.log("ready");
|
||||
});
|
||||
|
||||
process.on("exit", async () => {
|
||||
const channels = client.channels;
|
||||
const logChannel = channels.cache.get(config.discord.channelIdLog);
|
||||
await this.handleShutdown(logChannel);
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
process.on("SIGINT", async () => {
|
||||
const channels = client.channels;
|
||||
const logChannel = channels.cache.get(config.discord.channelIdLog);
|
||||
await this.handleShutdown(logChannel);
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
process.on("SIGTERM", async () => {
|
||||
const channels = client.channels;
|
||||
const logChannel = channels.cache.get(config.discord.channelIdLog);
|
||||
await this.handleShutdown(logChannel);
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
// listen for interactions
|
||||
client.on("interactionCreate", this.handleInteraction.bind(this));
|
||||
|
||||
client.on("messageCreate", async (message) => {
|
||||
console.log(message.id);
|
||||
if (message.channel.type === ChannelType.DM) {
|
||||
console.log("got msg");
|
||||
await this.dmService.forward(message);
|
||||
}
|
||||
});
|
||||
|
||||
client.on("guildMemberAdd", async (member) => {
|
||||
await this.greetingService.welcome(member);
|
||||
});
|
||||
|
||||
client.on(
|
||||
Events.VoiceStateUpdate,
|
||||
async (oldState: VoiceState, newState: VoiceState) => {
|
||||
// check if user joined a vc
|
||||
if (
|
||||
(!oldState.channelId && newState.channelId) ||
|
||||
oldState.channelId !== newState.channelId
|
||||
) {
|
||||
// check if right vc
|
||||
if (
|
||||
newState.channelId === config.discord.vchannelIdForTwo ||
|
||||
newState.channelId === config.discord.vchannelIdForThree ||
|
||||
newState.channelId === config.discord.vchannelIdForFour ||
|
||||
newState.channelId === config.discord.vchannelIdForGroup
|
||||
) {
|
||||
const channel = newState.channel;
|
||||
if (!channel) {
|
||||
console.error("channel not found");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const newChannel =
|
||||
await this.dynamicVChannelService.createVChannel(
|
||||
newState,
|
||||
channel,
|
||||
);
|
||||
// move user in new channel
|
||||
await newState.setChannel(newChannel);
|
||||
// create specific listener for channel
|
||||
const channelListener = async (
|
||||
oldState: VoiceState,
|
||||
newState: VoiceState,
|
||||
) => {
|
||||
channelListeners =
|
||||
await this.dynamicVChannelService.deleteVChannel(
|
||||
oldState,
|
||||
newState,
|
||||
newChannel,
|
||||
channelListeners,
|
||||
channelListener,
|
||||
);
|
||||
};
|
||||
// save listener in map
|
||||
channelListeners.set(newChannel.id, channelListener);
|
||||
// add listener
|
||||
client.on(Events.VoiceStateUpdate, channelListener);
|
||||
} catch (error) {
|
||||
console.error("error while duplicating channel", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
console.log(
|
||||
`----------------\nversion ${config.discord.version}\n----------------`,
|
||||
);
|
||||
}
|
||||
|
||||
async setActivity(state: number) {
|
||||
switch (state) {
|
||||
case 0:
|
||||
client.user?.setActivity(" ", { type: 0 });
|
||||
console.log("set activity");
|
||||
client.user?.setPresence({
|
||||
status: "invisible",
|
||||
});
|
||||
break;
|
||||
default:
|
||||
client.user?.setActivity("spielt sudoku", { type: 0 });
|
||||
console.log("set activity");
|
||||
client.user?.setPresence({
|
||||
status: "online",
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
async handleShutdown(logChannel: Channel | undefined) {
|
||||
if (logChannel?.isTextBased() && logChannel?.isSendable()) {
|
||||
try {
|
||||
await logChannel.send("bot is going offline...");
|
||||
} catch (error) {
|
||||
console.error("failed to send offline message:", error);
|
||||
}
|
||||
} else {
|
||||
console.error("log channel is not valid or sendable.");
|
||||
}
|
||||
await this.setActivity(0);
|
||||
console.log("bot is offline.");
|
||||
}
|
||||
|
||||
async init() {
|
||||
await this.discordService.init();
|
||||
}
|
||||
|
||||
async handleInteraction(interaction: Interaction<CacheType>) {
|
||||
if (interaction.isModalSubmit()) {
|
||||
await this.handleModalSubmit(interaction);
|
||||
return;
|
||||
}
|
||||
|
||||
if (interaction.isChatInputCommand()) {
|
||||
await this.handleChatInputCommand(interaction);
|
||||
return;
|
||||
}
|
||||
if (interaction.isButton()) {
|
||||
await this.handleButton(interaction);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
async handleButton(interaction: ButtonInteraction<CacheType>) {
|
||||
const { customId } = interaction;
|
||||
console.log(interaction.customId);
|
||||
|
||||
if (customId.toLowerCase().includes("moreWater")) {
|
||||
await this.waterMeService.handleInteraction(interaction);
|
||||
}
|
||||
if (customId.toLowerCase().includes("medication")) {
|
||||
await this.medicationService.handleInteraction(interaction);
|
||||
}
|
||||
}
|
||||
|
||||
async handleChatInputCommand(
|
||||
interaction: ChatInputCommandInteraction<CacheType>,
|
||||
) {
|
||||
const commandName = interaction.commandName as CommandsType;
|
||||
|
||||
// add commands
|
||||
switch (commandName) {
|
||||
case Commands.Enum.giessen:
|
||||
await this.waterMeService.handleInteraction(interaction); // zu chatinputcommand wechseln
|
||||
return;
|
||||
case Commands.Enum.medikamente:
|
||||
await this.medicationService.handleChatInputCommand(interaction);
|
||||
return;
|
||||
case Commands.Enum.hilfe:
|
||||
await this.helpService.handleInteraction(interaction); // zu chatinputcommand wechseln
|
||||
return;
|
||||
case Commands.Enum.support:
|
||||
case Commands.Enum.kofi:
|
||||
case Commands.Enum.disboard:
|
||||
case Commands.Enum.discadia:
|
||||
await this.supportService.handleInteraction(interaction);
|
||||
return;
|
||||
case Commands.Enum.accept:
|
||||
await this.greetingService.handleChatInputCommand(interaction);
|
||||
return;
|
||||
case Commands.Enum.welcome:
|
||||
await this.greetingService.handleChatInputCommand(interaction);
|
||||
return;
|
||||
case Commands.Enum.embed:
|
||||
case Commands.Enum.message:
|
||||
await this.customMessageService.handleChatInputCommand(interaction);
|
||||
return;
|
||||
case Commands.Enum.reminder:
|
||||
await this.greetingService.handleChatInputCommand(interaction);
|
||||
return;
|
||||
case Commands.Enum.version:
|
||||
await this.debugService.handleChatInputCommand(interaction);
|
||||
return;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// wenn neues fenster durch buttonclick or so
|
||||
async handleModalSubmit(interaction: ModalSubmitInteraction<CacheType>) {
|
||||
const { customId } = interaction;
|
||||
|
||||
switch (customId) {
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
32
adapters/discord/src/entitites/commands/index.ts
Normal file
32
adapters/discord/src/entitites/commands/index.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { CommandsCollection } from "@avocadi/bot-core/entities/commands/commands.entity";
|
||||
import type { Command } from "@avocadi/bot-core/entities/commands/commands.schema";
|
||||
import { SlashCommandBuilder } from "discord.js";
|
||||
import type { z } from "zod";
|
||||
|
||||
const convertCommandToDiscordFormat = (
|
||||
command: z.output<typeof Command>,
|
||||
key: string,
|
||||
) => {
|
||||
const slashCommand = new SlashCommandBuilder()
|
||||
.setName(command.name || key)
|
||||
.setDescription(command.description);
|
||||
|
||||
if (command.options) {
|
||||
command.options.forEach((option) => {
|
||||
slashCommand.addStringOption((opt) =>
|
||||
opt
|
||||
.setName(option.name)
|
||||
.setDescription(option.description)
|
||||
.setRequired(option.required),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
return slashCommand;
|
||||
};
|
||||
|
||||
export default function getCommands() {
|
||||
return Object.entries(CommandsCollection).map(([key, command]) =>
|
||||
convertCommandToDiscordFormat(command, key),
|
||||
);
|
||||
}
|
||||
20
adapters/discord/src/entitites/messages/messages.service.ts
Normal file
20
adapters/discord/src/entitites/messages/messages.service.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import type { MessagesServiceInterface } from "@avocadi/bot-core/entities/messages/messages.service";
|
||||
import { createLogger } from "@avocadi/bot-core/lib/logger";
|
||||
import type { User } from "discord.js";
|
||||
import client from "lib/client";
|
||||
|
||||
export class MessagesService implements MessagesServiceInterface<User> {
|
||||
private logger = createLogger("MessagesService");
|
||||
|
||||
async sendToUser(userInput: User, message: string): Promise<void> {
|
||||
const user = await client.users.fetch(userInput.id);
|
||||
|
||||
if (user) {
|
||||
await user.send(message);
|
||||
} else {
|
||||
this.logger.error(`User with ID ${userInput.id} not found.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const messagesService = new MessagesService();
|
||||
33
adapters/discord/src/entitites/roles/roles.service.ts
Normal file
33
adapters/discord/src/entitites/roles/roles.service.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import type { RolesServiceInterface } from "@avocadi/bot-core/entities/roles/roles.service";
|
||||
import { createLogger } from "@avocadi/bot-core/lib/logger";
|
||||
import type { GuildMember } from "discord.js";
|
||||
|
||||
export class RolesService implements RolesServiceInterface<GuildMember> {
|
||||
private logger = createLogger("RolesService");
|
||||
|
||||
async assignRole(user: GuildMember, role: string) {
|
||||
const roleToAssign = user.guild.roles.cache.find((r) => r.name === role);
|
||||
if (!roleToAssign) {
|
||||
this.logger.error(`Role ${role} not found in guild ${user.guild.name}.`);
|
||||
return;
|
||||
}
|
||||
await user.roles.add(roleToAssign);
|
||||
}
|
||||
|
||||
async removeRole(user: GuildMember, role: string) {
|
||||
const roleToRemove = user.guild.roles.cache.find((r) => r.name === role);
|
||||
if (!roleToRemove) {
|
||||
this.logger.error(`Role ${role} not found in guild ${user.guild.name}.`);
|
||||
return;
|
||||
}
|
||||
await user.roles.remove(roleToRemove);
|
||||
}
|
||||
|
||||
async getRoles(user: GuildMember) {
|
||||
return user.roles.cache.map((role) => role.name);
|
||||
}
|
||||
|
||||
async hasRole(user: GuildMember, role: string) {
|
||||
return user.roles.cache.some((r) => r.name === role);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import { GreetingService } from "@avocadi/bot-core/features/greeting/greeting.service";
|
||||
import { messagesService } from "entitites/messages/messages.service";
|
||||
|
||||
export const greetingsService = new GreetingService(messagesService);
|
||||
@@ -0,0 +1,128 @@
|
||||
import type { Roles } from "@avocadi/bot-core/entities/roles/roles.schema";
|
||||
import { createLogger } from "@avocadi/bot-core/lib/logger";
|
||||
import { config } from "config";
|
||||
import type {
|
||||
GuildMember,
|
||||
MessageReaction,
|
||||
PartialMessageReaction,
|
||||
PartialUser,
|
||||
User,
|
||||
} from "discord.js";
|
||||
import type z from "zod";
|
||||
|
||||
export class ReactionRolesService {
|
||||
private logger = createLogger("ReactionRolesService");
|
||||
|
||||
/**
|
||||
* This method validates if the reaction is on an allowed message for reaction roles.
|
||||
* @param reaction
|
||||
* @returns
|
||||
*/
|
||||
async validateReaction(reaction: MessageReaction | PartialMessageReaction) {
|
||||
this.logger.info(
|
||||
`Validating reaction ${reaction.emoji.name} on message ${reaction.message.id}`,
|
||||
);
|
||||
|
||||
const message = await reaction.message.fetch();
|
||||
|
||||
if (!message) {
|
||||
this.logger.error(`Message with ID ${reaction.message.id} not found.`);
|
||||
throw new Error("Message not found");
|
||||
}
|
||||
|
||||
if (!config.reactionRoles.allowedMessageIds.includes(message.id)) {
|
||||
this.logger.error(
|
||||
`Message with ID ${message.id} is not allowed for reaction roles.`,
|
||||
);
|
||||
throw new Error("Message not allowed for reaction roles");
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes a reaction, validates it, and assigns or removes the given role.
|
||||
* @param reaction
|
||||
* @param guild
|
||||
* @param user
|
||||
* @param role
|
||||
*/
|
||||
async handleReaction(
|
||||
reaction: MessageReaction | PartialMessageReaction,
|
||||
user: User | PartialUser,
|
||||
targetRole: z.output<typeof Roles>,
|
||||
action: "add" | "remove",
|
||||
) {
|
||||
const guild = reaction.message.guild;
|
||||
|
||||
if (!guild) {
|
||||
this.logger.error(`Guild not found for message ${reaction.message.id}.`);
|
||||
throw new Error("Guild not found");
|
||||
}
|
||||
|
||||
await this.validateReaction(reaction);
|
||||
|
||||
const role = guild.roles.cache.get(config.roleMapping[targetRole]);
|
||||
|
||||
if (!role) {
|
||||
this.logger.error(`Role ${targetRole} not found in guild ${guild.name}.`);
|
||||
throw new Error("Role not found");
|
||||
}
|
||||
|
||||
const member = await guild.members.fetch(user.id);
|
||||
|
||||
if (!member) {
|
||||
this.logger.error(
|
||||
`User with ID ${user.id} not found in guild ${guild.name}.`,
|
||||
);
|
||||
throw new Error("User not found");
|
||||
}
|
||||
|
||||
if (member.roles.cache.has(role.id)) {
|
||||
if (action === "remove") {
|
||||
this.removeRoleByReaction(reaction, member, targetRole);
|
||||
} else {
|
||||
this.logger.info(
|
||||
`User ${member.user.tag} already has role ${targetRole}. No action taken.`,
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (action === "remove") {
|
||||
this.logger.info(
|
||||
`User ${member.user.tag} does not have role ${targetRole}. No action taken.`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.assignRoleByReaction(reaction, member, targetRole);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
async assignRoleByReaction(
|
||||
reaction: MessageReaction | PartialMessageReaction,
|
||||
member: GuildMember,
|
||||
role: z.output<typeof Roles>,
|
||||
) {
|
||||
this.logger.info(
|
||||
`Assigning role ${role} based on reaction ${reaction.emoji.name} by user ${member.user.tag}`,
|
||||
);
|
||||
|
||||
await member.roles.add(config.roleMapping[role]);
|
||||
}
|
||||
|
||||
async removeRoleByReaction(
|
||||
reaction: MessageReaction | PartialMessageReaction,
|
||||
member: GuildMember,
|
||||
role: z.output<typeof Roles>,
|
||||
) {
|
||||
this.logger.info(
|
||||
`Removing role ${role} based on reaction ${reaction.emoji.name} by user ${member.user.tag}`,
|
||||
);
|
||||
|
||||
await member.roles.remove(config.roleMapping[role]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import type { WaterMeService } from "@avocadi/bot-core/features/water-me/water-me.service";
|
||||
import {
|
||||
ActionRowBuilder,
|
||||
ButtonBuilder,
|
||||
ButtonStyle,
|
||||
type CacheType,
|
||||
type Interaction,
|
||||
} from "discord.js";
|
||||
import { waterMeService } from "./water-me.service";
|
||||
|
||||
class WaterMeController {
|
||||
waterMeService: WaterMeService;
|
||||
|
||||
constructor() {
|
||||
this.waterMeService = waterMeService;
|
||||
}
|
||||
|
||||
async handleInteraction(interaction: Interaction<CacheType>) {
|
||||
const result = this.waterMeService.waterMe();
|
||||
|
||||
const moreButton = new ButtonBuilder()
|
||||
.setCustomId("moreWater")
|
||||
.setLabel("mehr")
|
||||
.setStyle(ButtonStyle.Secondary);
|
||||
|
||||
const row = new ActionRowBuilder<ButtonBuilder>().addComponents(moreButton);
|
||||
|
||||
if (interaction.isChatInputCommand()) {
|
||||
await interaction.reply({
|
||||
content: result.reply,
|
||||
components: [row],
|
||||
});
|
||||
} else if (interaction.isButton()) {
|
||||
await interaction.reply({
|
||||
content: result.reply,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const waterMeController = new WaterMeController();
|
||||
@@ -0,0 +1,8 @@
|
||||
import { WaterMeService } from "@avocadi/bot-core/features/water-me/water-me.service";
|
||||
|
||||
export const waterMeService = new WaterMeService({
|
||||
channelId: "123",
|
||||
handleMessageSend: async (message, channelId) => {
|
||||
console.log(`Sending message to channel ${channelId}: ${message}`);
|
||||
},
|
||||
});
|
||||
12
adapters/discord/src/features/water-me/water-me.tasks.ts
Normal file
12
adapters/discord/src/features/water-me/water-me.tasks.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { CronJob } from "cron";
|
||||
import { waterMeService } from "./water-me.service";
|
||||
|
||||
new CronJob(
|
||||
"0 0 20 * * *", // cronTime
|
||||
async () => {
|
||||
await waterMeService.notifyIfThirsty();
|
||||
}, // onTick
|
||||
null, // onComplete
|
||||
true, // start
|
||||
"Europe/Berlin", // timeZone
|
||||
);
|
||||
4
adapters/discord/src/index.ts
Normal file
4
adapters/discord/src/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { publishCommands } from "actions/publish-commands";
|
||||
|
||||
// Publish commands when the application starts
|
||||
await publishCommands();
|
||||
24
adapters/discord/src/lib/client.ts
Normal file
24
adapters/discord/src/lib/client.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { config } from "config";
|
||||
import { Client, IntentsBitField, Partials } from "discord.js";
|
||||
|
||||
const client = new Client({
|
||||
intents: [
|
||||
IntentsBitField.Flags.Guilds,
|
||||
IntentsBitField.Flags.GuildMembers,
|
||||
IntentsBitField.Flags.GuildModeration,
|
||||
IntentsBitField.Flags.GuildMessages,
|
||||
IntentsBitField.Flags.GuildMessageReactions,
|
||||
IntentsBitField.Flags.GuildMessagePolls,
|
||||
IntentsBitField.Flags.GuildVoiceStates,
|
||||
IntentsBitField.Flags.MessageContent,
|
||||
IntentsBitField.Flags.DirectMessages,
|
||||
IntentsBitField.Flags.DirectMessageReactions,
|
||||
IntentsBitField.Flags.DirectMessageTyping,
|
||||
IntentsBitField.Flags.DirectMessagePolls,
|
||||
],
|
||||
partials: [Partials.Channel, Partials.Message, Partials.Reaction],
|
||||
});
|
||||
|
||||
await client.login(config.discord.token);
|
||||
|
||||
export default client;
|
||||
6
adapters/discord/src/lib/rest-client.ts
Normal file
6
adapters/discord/src/lib/rest-client.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { REST } from "@discordjs/rest";
|
||||
import { config } from "config";
|
||||
|
||||
export const discordRestClient = new REST({ version: "10" }).setToken(
|
||||
config.discord.token,
|
||||
);
|
||||
@@ -0,0 +1,12 @@
|
||||
import { greetingsService } from "features/greeting/greetings.service";
|
||||
import client from "lib/client";
|
||||
|
||||
client.on("guildMemberAdd", async (member) => {
|
||||
if (member.user.bot) {
|
||||
// Don't send a welcome message for bots (sorry tom)
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
greetingsService.sendGreeting(member.user, member.user.username);
|
||||
});
|
||||
@@ -0,0 +1,20 @@
|
||||
import { Roles } from "@avocadi/bot-core/entities/roles/roles.schema";
|
||||
import { ReactionRolesService } from "features/reaction-roles/reaction-roles.service";
|
||||
import client from "lib/client";
|
||||
|
||||
const reactionRolesService = new ReactionRolesService();
|
||||
|
||||
// Currently only used for the "people" role, but can be extended to handle multiple roles based on the reaction emoji
|
||||
|
||||
client.on("messageReactionAdd", async (reaction, user) => {
|
||||
reactionRolesService.handleReaction(reaction, user, Roles.enum.people, "add");
|
||||
});
|
||||
|
||||
client.on("messageReactionRemove", async (reaction, user) => {
|
||||
reactionRolesService.handleReaction(
|
||||
reaction,
|
||||
user,
|
||||
Roles.enum.people,
|
||||
"remove",
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,6 @@
|
||||
import { Events } from "discord.js";
|
||||
import client from "lib/client";
|
||||
|
||||
client.on(Events.VoiceStateUpdate, async (oldState, newState) => {
|
||||
// TODO: handle updates
|
||||
});
|
||||
@@ -1,29 +1,31 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
// Environment setup & latest features
|
||||
"lib": ["ESNext"],
|
||||
"target": "ESNext",
|
||||
"module": "Preserve",
|
||||
"moduleDetection": "force",
|
||||
"jsx": "react-jsx",
|
||||
"allowJs": true,
|
||||
"compilerOptions": {
|
||||
// Environment setup & latest features
|
||||
"lib": ["ESNext"],
|
||||
"target": "ESNext",
|
||||
"module": "Preserve",
|
||||
"moduleDetection": "force",
|
||||
"jsx": "react-jsx",
|
||||
"allowJs": true,
|
||||
|
||||
// Bundler mode
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"noEmit": true,
|
||||
// Bundler mode
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"noEmit": true,
|
||||
|
||||
// Best practices
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"noImplicitOverride": true,
|
||||
// Best practices
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"noImplicitOverride": true,
|
||||
|
||||
// Some stricter flags (disabled by default)
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"noPropertyAccessFromIndexSignature": false
|
||||
}
|
||||
"baseUrl": "src",
|
||||
|
||||
// Some stricter flags (disabled by default)
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"noPropertyAccessFromIndexSignature": false
|
||||
}
|
||||
}
|
||||
|
||||
9
adapters/discord/tsdown.config.ts
Normal file
9
adapters/discord/tsdown.config.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { defineConfig } from "tsdown";
|
||||
|
||||
export default defineConfig({
|
||||
entry: ["./src/index.ts"],
|
||||
format: "esm",
|
||||
dts: true,
|
||||
exports: true,
|
||||
fixedExtension: false,
|
||||
});
|
||||
Reference in New Issue
Block a user