This commit is contained in:
moriese 2024-12-27 01:23:35 +01:00
parent 7d349a37d4
commit c06ac10d09
18 changed files with 543 additions and 0 deletions

175
.gitignore vendored Normal file
View File

@ -0,0 +1,175 @@
# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore
# Logs
logs
_.log
npm-debug.log_
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Caches
.cache
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
# Runtime data
pids
_.pid
_.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
# IntelliJ based IDEs
.idea
# Finder (MacOS) folder config
.DS_Store

3
Biomefile Normal file
View File

@ -0,0 +1,3 @@
{
"name": "avocadi-bot"
}

BIN
bun.lockb Executable file

Binary file not shown.

20
package.json Normal file
View File

@ -0,0 +1,20 @@
{
"name": "avocadi-bot",
"module": "src/index.ts",
"type": "module",
"scripts": {
"dev": "bun --watch src/index.ts"
},
"devDependencies": {
"@types/bun": "latest"
},
"peerDependencies": {
"typescript": "^5.0.0"
},
"dependencies": {
"@discordjs/rest": "^2.4.0",
"cron": "^3.3.1",
"discord.js": "^14.16.3",
"zod": "^3.24.1"
}
}

View File

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

12
src/config.ts Normal file
View File

@ -0,0 +1,12 @@
const test = true;
export default {
discord: {
channelId:
(test
? process.env.DISCORD_TEST_CHANNEL_ID
: process.env.DISCORD_CHANNEL_ID) || "",
applicationId: process.env.DISCORD_APPLICATION_ID || "",
token: process.env.DISCORD_TOKEN || "",
},
};

View File

@ -0,0 +1,83 @@
import { Commands, type CommandsType } from "components/commands.component";
import type {
ButtonInteraction,
CacheType,
ChatInputCommandInteraction,
Interaction,
ModalSubmitInteraction,
} from "discord.js";
import client from "lib/client";
import EventEmitter from "node:events";
import DiscordService from "services/discord.service";
import { WaterMeService } from "services/water-me.service";
export default class DiscordController extends EventEmitter {
private discordService!: DiscordService;
waterMeService: WaterMeService;
constructor() {
super();
this.discordService = new DiscordService();
this.waterMeService = new WaterMeService();
// log when running
client.once("ready", () => {
console.log("Listening...");
});
// listen for interactions
client.on("interactionCreate", this.handleInteraction.bind(this));
}
async init() {
await this.discordService.init();
}
async handleInteraction(interaction: Interaction<CacheType>) {
if (interaction.isModalSubmit()) {
await this.handleModalSubmit(interaction);
return;
}
if (interaction.isChatInputCommand()) {
await this.handleChatInputCommand(interaction);
return;
}
if (interaction.isButton()) {
await this.handleButton(interaction);
return;
}
}
async handleButton(interaction: ButtonInteraction<CacheType>) {
const { customId } = interaction;
console.log(interaction.customId);
if (customId === "moreWater") {
await this.waterMeService.handleInteraction(interaction);
}
}
async handleChatInputCommand(
interaction: ChatInputCommandInteraction<CacheType>,
) {
const commandName = interaction.commandName as CommandsType;
// add commands
switch (commandName) {
case Commands.Enum["water-me"]:
await this.waterMeService.handleInteraction(interaction);
return;
default:
break;
}
}
// wenn neues fenster durch buttonclick or so
async handleModalSubmit(interaction: ModalSubmitInteraction<CacheType>) {
const { customId } = interaction;
switch (customId) {
default:
break;
}
}
}

11
src/index.ts Normal file
View File

@ -0,0 +1,11 @@
import "tasks/water-me.task";
import "tasks/greeting.task";
import "tasks/drink.task";
import DiscordController from "controllers/discord.controller";
// = main file
// bootstrap application
const discordController = new DiscordController();
discordController.init();

8
src/lib/client.ts Normal file
View File

@ -0,0 +1,8 @@
import config from "config";
import { Client, IntentsBitField } from "discord.js";
const client = new Client({ intents: [IntentsBitField.Flags.Guilds] });
await client.login(config.discord.token);
export default client;

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

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

View File

@ -0,0 +1,26 @@
import { Routes } from "discord.js";
import { REST } from "@discordjs/rest";
import config from "config";
import getCommands from "components/commands.component";
export default class DiscordService {
rest: REST;
constructor() {
this.rest = new REST({ version: "10" }).setToken(config.discord.token);
}
async init() {
try {
await this.rest.put(
Routes.applicationCommands(config.discord.applicationId),
{
body: getCommands(),
},
);
console.log("Successfully added commands");
} catch (e) {
console.error(e);
}
}
}

View File

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

View File

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

View File

@ -0,0 +1,98 @@
import { CronJob } from "cron";
import { getRandomInt } from "lib/utils";
import config from "config";
import client from "lib/client";
import {
ActionRowBuilder,
ButtonBuilder,
ButtonStyle,
type CacheType,
type Interaction,
} from "discord.js";
export class WaterMeService {
waterLevel: number;
private thirsty = 3 as const;
private enough = 10 as const;
constructor() {
this.waterLevel = 0;
}
getReply() {
const thirstyReplies = [
"... wow das wars schon???",
"dankeeeee!!!! ich waer fast verdurstet :(((",
"*roelpssssss*",
];
const fullReplies = [
"langsam reicht es :o",
"poah, das hat gut getan",
"das ist krass :3",
];
const tooMuchReplies = [
"ES REICHT!!!!",
"bitte hoer auf, ich platze gleich :(",
];
if (this.waterLevel <= this.thirsty) {
return thirstyReplies[getRandomInt(0, thirstyReplies.length - 1)];
}
if (this.waterLevel > this.thirsty && this.waterLevel <= this.enough) {
return fullReplies[getRandomInt(0, fullReplies.length - 1)];
}
if (this.waterLevel > this.enough) {
return tooMuchReplies[getRandomInt(0, tooMuchReplies.length - 1)];
}
}
async isThirsty() {
const channels = client.channels;
const channel = channels.cache.get(config.discord.channelId);
if (
channel?.isTextBased &&
channel?.isSendable() &&
this.waterLevel <= this.thirsty
) {
await channel.send({ content: "ich brauche wasser :(" });
}
}
waterMe() {
const reply = this.getReply();
this.waterLevel++;
return {
reply,
};
}
async handleInteraction(interaction: Interaction<CacheType>) {
const result = this.waterMe();
const moreButton = new ButtonBuilder()
.setCustomId("more")
.setLabel("mehr")
.setStyle(ButtonStyle.Secondary);
const row = new ActionRowBuilder().addComponents(moreButton);
if (interaction.isChatInputCommand()) {
await interaction.reply({
content: result.reply,
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
components: [row as any],
});
} else if (interaction.isButton()) {
await interaction.reply({
content: result.reply,
});
}
}
}

0
src/tasks/drink.task.ts Normal file
View File

View File

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

View File

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

27
tsconfig.json Normal file
View File

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