10 Commits

Author SHA1 Message Date
0449654edd fix(status): increase timeout for fetch request to 10 seconds
feat(types): add missing import for ButtonInteraction in type definitions
2025-10-29 22:12:01 +01:00
CL TheDreWen
e9565a9552 Update README.md 2025-10-29 17:31:56 +01:00
0504e8262d fix(command): correct export statement in ping command and update import in status command 2025-10-29 16:27:18 +01:00
978d21a120 feat(status): import Host type for improved type safety 2025-10-29 16:26:22 +01:00
9cadfb2734 feat(command): refactor ping and status commands to use CommandDefinition type 2025-10-29 15:47:33 +01:00
5b3167bf14 feat(types): define CommandDefinition type for command handling 2025-10-29 15:42:11 +01:00
7f9468bc99 feat(command): enhance ping command with button interaction and improved response format 2025-10-29 15:36:45 +01:00
86a7429711 feat(entity): add Guild entity with primary key and columns for guild_id and persistent_message_id 2025-10-29 11:45:37 +01:00
43b7bdd8b4 feat(database): add TypeORM configuration and initialize data source
- Added TypeORM and PostgreSQL dependencies to package.json.
- Created data-source.ts to configure the database connection.
- Initialized the data source in index.ts and added logging for successful connection.
- Updated devDependencies to include @types/node for better type support.
2025-10-29 10:44:12 +01:00
58ecaf3c4c Add description for persistent status messages in README 2025-10-29 10:19:23 +01:00
12 changed files with 1644 additions and 27 deletions

View File

@@ -1,5 +1,18 @@
TOKEN=
CLIENT_ID=
# Discord Bot Configuration
TOKEN=your_discord_bot_token_here
CLIENT_ID=your_discord_client_id_here
# Custom Emojis
EMOJI_STATUS_ONLINE=<a:online:1432684754276323431>
EMOJI_STATUS_OFFLINE=<a:offline:1432684900175183882>
EMOJI_STATUS_OFFLINE=<a:offline:1432684900175183882>
# Database Configuration
DB_HOST=localhost
DB_PORT=5432
DB_USERNAME=postgres
DB_PASSWORD=your_database_password_here
DB_DATABASE=protojx_manager
DB_LOGGING=false
# Environment
NODE_ENV=development

View File

@@ -9,8 +9,9 @@ A status bot and other features for protojx.
| Description | Status |
|-------------|--------|
| /status command | 🌐 |
| Number of services down in the bot's status. | |
| Number of services down in the bot's status. | 🌐 |
| Notification system in case of downtime. | |
| Ability to create persistent status messages that update automatically. | |
| Deployment workflow on Raspberry Pi. | |
- 🌐 -> In production
@@ -92,4 +93,4 @@ npm run start
```
---
**Note**: This template is based on community best practices and has been customized for Discord bot development.
**Note**: This template is based on community best practices and has been customized for Discord bot development.

1513
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -24,6 +24,7 @@
},
"homepage": "https://github.com/Under-scape/discordbot-ts-template#readme",
"devDependencies": {
"@types/node": "^24.9.2",
"typescript": "^5.9.2"
},
"dependencies": {
@@ -31,6 +32,9 @@
"cron": "^4.3.3",
"discord.js": "^14.24.1",
"dotenv": "^17.2.2",
"ping": "^1.0.0"
"pg": "^8.16.3",
"ping": "^1.0.0",
"reflect-metadata": "^0.2.2",
"typeorm": "^0.3.27"
}
}

View File

@@ -1,6 +1,7 @@
import { ApplicationIntegrationType, ChatInputCommandInteraction, CommandInteraction, InteractionContextType, SlashCommandBuilder } from "discord.js";
import { ApplicationIntegrationType, ButtonInteraction, ButtonStyle, ChatInputCommandInteraction, CommandInteraction, ComponentType, ContainerBuilder, InteractionContextType, MessageFlags, SlashCommandBuilder } from "discord.js";
import { CommandDefinition } from "../../type";
export default {
const cmd : CommandDefinition = {
data: new SlashCommandBuilder()
.setName('ping')
.setDescription('Pong again!')
@@ -14,6 +15,35 @@ export default {
InteractionContextType.PrivateChannel
),
async execute(interaction : ChatInputCommandInteraction) {
await interaction.reply(`🏓 Latency is ${Date.now() - interaction.createdTimestamp}ms. API Latency : ${interaction.client.ws.ping}ms`);
}
}
const container = new ContainerBuilder()
.addTextDisplayComponents((textDisplay) => textDisplay.setContent(`🏓 Latency is ${Date.now() - interaction.createdTimestamp}ms. API Latency : ${interaction.client.ws.ping}ms`))
.addSeparatorComponents((s) => s)
.addSectionComponents((section) =>
section
.addTextDisplayComponents((textDisplay) =>
textDisplay
.setContent('Oh, that\'s a beautiful button!')
)
.setButtonAccessory((button) =>
button
.setCustomId('testClick')
.setLabel('Click Me !')
.setStyle(ButtonStyle.Success)
)
)
// await interaction.reply();
await interaction.reply({
components: [container],
flags: MessageFlags.IsComponentsV2
})
},
buttons: [
{id: 'testClick', handle: (interaction : ButtonInteraction) => {
interaction.reply({content: 'Ho !', flags: [MessageFlags.Ephemeral]})
}}
]
}
export default cmd;

View File

@@ -1,8 +1,9 @@
import { ApplicationIntegrationType, ChatInputCommandInteraction, CommandInteraction, EmbedBuilder, InteractionContextType, SlashCommandBuilder } from "discord.js";
import ping from "ping";
import statusService from "../../services/status.service";
import { CommandDefinition } from "../../type";
export default {
const cmd : CommandDefinition = {
data: new SlashCommandBuilder()
.setName('status')
.setDescription('Give statut of servers.')
@@ -30,4 +31,6 @@ export default {
await interaction.editReply({embeds: [embed]});
}
}
}
export default cmd;

19
src/data-source.ts Normal file
View File

@@ -0,0 +1,19 @@
import "reflect-metadata"
import { DataSource } from "typeorm"
import { configDotenv } from "dotenv"
configDotenv()
export const AppDataSource = new DataSource({
type: "postgres",
host: process.env.DB_HOST || "localhost",
port: parseInt(process.env.DB_PORT || "5432"),
username: process.env.DB_USERNAME || "postgres",
password: process.env.DB_PASSWORD,
database: process.env.DB_DATABASE,
synchronize: process.env.NODE_ENV !== "production", // false en production
logging: process.env.DB_LOGGING === "true",
entities: ["./entity/**/*.js"],
migrations: ["./migration/**/*.js"],
subscribers: ["./subscriber/**/*.js"],
})

13
src/entity/Guilds.ts Normal file
View File

@@ -0,0 +1,13 @@
import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";
@Entity()
export class Guild {
@PrimaryGeneratedColumn()
id: number;
@Column()
guild_id: string;
@Column()
persistent_message_id: string;
}

View File

@@ -1,11 +1,21 @@
import { Client, Collection, Events, GatewayIntentBits, MessageFlags } from "discord.js";
import { ButtonInteraction, ChatInputCommandInteraction, Client, Collection, Events, GatewayIntentBits, MessageFlags, SlashCommandBuilder } from "discord.js";
import { configDotenv } from "dotenv";
import path from "path";
import fs from "fs";
import statusService from "./services/status.service";
import "reflect-metadata";
import { AppDataSource } from "./data-source";
import { CommandDefinition } from "./type";
configDotenv();
AppDataSource.initialize()
.then(() => {
console.log("Data Source initialized !")
})
.catch((error) => console.log(error));
const client = new Client({
intents: [
GatewayIntentBits.Guilds,
@@ -15,8 +25,7 @@ const client = new Client({
]
});
// @ts-expect-error
client.commands = new Collection();
const commands = new Collection<string, CommandDefinition>();
const foldersPath = path.join(__dirname, 'commands');
const commandFolders = fs.readdirSync(foldersPath);
@@ -29,8 +38,7 @@ for (const folder of commandFolders) {
const command = require(filePath);
const commandModule = command.default || command;
if ('data' in commandModule && 'execute' in commandModule) {
// @ts-expect-error
client.commands.set(commandModule.data.name, commandModule);
commands.set(commandModule.data.name, commandModule);
} else {
console.log(`[WARNING] The command at ${filePath} is missing a required "data" or "execute" property.`);
}
@@ -38,10 +46,26 @@ for (const folder of commandFolders) {
}
client.on(Events.InteractionCreate, async interaction => {
if(interaction.isButton()) {
const id = interaction.customId;
commands.forEach((value, key) => {
if(value.buttons) {
const button = value.buttons.filter((b) => b.id == id);
if(button.length == 1) {
button[0]?.handle(interaction);
}
}
});
return;
}
if (!interaction.isChatInputCommand()) return;
// @ts-expect-error
const command = interaction.client.commands.get(interaction.commandName);
const command = commands.get(interaction.commandName);
if (!command) {
console.error(`No command matching ${interaction.commandName} was found.`);

View File

@@ -1,6 +1,7 @@
import ping from "ping";
import * as cron from 'cron';
import { ActivityType, Client } from "discord.js";
import { Host } from "../type";
export class StatusService {
@@ -107,7 +108,7 @@ export class StatusService {
host.alive = res.alive;
}else if(host.type === 'website'){
try {
const response = await fetch(host.host, { method: 'HEAD', signal: AbortSignal.timeout(3000) });
const response = await fetch(host.host, { method: 'HEAD', signal: AbortSignal.timeout(10000) });
host.alive = response.ok;
} catch (error) {
host.alive = false;

5
src/type.d.ts vendored
View File

@@ -1 +1,4 @@
type Host = { host: string, name: string, alive: boolean, type: 'ping' | 'website' };
import { ButtonInteraction, ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js";
export type Host = { host: string, name: string, alive: boolean, type: 'ping' | 'website' };
export type CommandDefinition = { data: SlashCommandBuilder, execute: (interaction: ChatInputCommandInteraction) => void, buttons?: { id: string, handle: (interaction: ButtonInteraction) => void}[]};

View File

@@ -24,7 +24,8 @@
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"removeComments": false,
"preserveConstEnums": true
"preserveConstEnums": true,
"strictPropertyInitialization": false
},
"include": [
"src/**/*"