6 Commits

Author SHA1 Message Date
ccc439d30a feat(commands): add uptime command with service selection and autocomplete
fix(Dockerfile): ensure entrypoint runs register command
fix(status.service): update getStatusImageBar to use serviceId
refactor(follow.command): simplify service lookup logic
refactor(deploy-commands): remove unnecessary DataSource initialization
2025-12-15 09:40:24 +01:00
1bffb9a971 fix(status): update notification sending logic to use service IDs 2025-12-13 19:14:53 +01:00
034921d00d fix(status): add log statement for notification sending process 2025-12-13 19:11:48 +01:00
b0ced298c0 fix(Dockerfile): comment out unused npm register command 2025-12-13 18:51:24 +01:00
5fda9afb83 feat(commands): implement follow command with service selection and autocomplete
fix(deploy): initialize and destroy DataSource for command deployment
fix(index): handle autocomplete interactions in command execution
refactor(types): add autocomplete support to CommandDefinition type
fix(Dockerfile): ensure npm run register is executed during build
2025-12-13 18:47:40 +01:00
CL TheDreWen
3d2a1125b3 Update Dockerfile 2025-11-28 14:13:42 +01:00
8 changed files with 140 additions and 54 deletions

View File

@@ -5,8 +5,9 @@ WORKDIR /app
COPY . .
RUN apt-get update && apt-get install -y iputils-ping
RUN npm i
RUN npm clean-install
# RUN npm run register
ENTRYPOINT [ "npm", "run", "register" ]
CMD [ "npm", "run", "start" ]

4
package-lock.json generated
View File

@@ -1276,7 +1276,6 @@
"resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz",
"integrity": "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==",
"license": "MIT",
"peer": true,
"dependencies": {
"pg-connection-string": "^2.9.1",
"pg-pool": "^3.10.1",
@@ -1487,8 +1486,7 @@
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz",
"integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==",
"license": "Apache-2.0",
"peer": true
"license": "Apache-2.0"
},
"node_modules/require-directory": {
"version": "2.1.1",

View File

@@ -1,50 +1,66 @@
// import { ApplicationIntegrationType, ChatInputCommandInteraction, InteractionContextType, MessageFlags, SlashCommandBuilder } from "discord.js";
// import { CommandDefinition } from "../../type";
// import { AppDataSource } from "../../data-source";
// import { Follow } from "../../entity/follow.entity";
// import statusService from "../../services/status.service";
import { ApplicationIntegrationType, ChatInputCommandInteraction, InteractionContextType, MessageFlags, SlashCommandBuilder } from "discord.js";
import { CommandDefinition } from "../../type";
import { AppDataSource } from "../../data-source";
import { Follow } from "../../entity/follow.entity";
import statusService from "../../services/status.service";
// const cmd: CommandDefinition = {
// data: new SlashCommandBuilder()
// .setName('follow')
// .setDescription('Enables/disables the receipt of service status notifications.')
// .setIntegrationTypes(
// ApplicationIntegrationType.UserInstall
// )
// .setContexts(
// InteractionContextType.BotDM,
// InteractionContextType.Guild,
// InteractionContextType.PrivateChannel
// ),
// async execute(interaction: ChatInputCommandInteraction) {
// const userRepo = AppDataSource.getRepository(Follow);
// const hostvalue = interaction.options.getString('host');
const cmd: CommandDefinition = {
data: new SlashCommandBuilder()
.setName('follow')
.setDescription('Enables/disables the receipt of service status notifications.')
.addStringOption((o) =>
o.setName('service')
.setDescription('Select a service to follow')
.setRequired(true)
.setAutocomplete(true)
)
.setIntegrationTypes(
ApplicationIntegrationType.UserInstall
)
.setContexts(
InteractionContextType.BotDM,
InteractionContextType.Guild,
InteractionContextType.PrivateChannel
),
async execute(interaction: ChatInputCommandInteraction) {
const userRepo = AppDataSource.getRepository(Follow);
const hostvalue = interaction.options.getString('service');
// const services = await statusService.serviceRepo.find();
// const realHost = services.find((v) => v.notify && v.host == hostvalue);
const realHost = await statusService.serviceRepo.findOne({where: {name: hostvalue+''}});
// if (!hostvalue || !realHost) {
// await interaction.reply({ content: '⚠️ Host not found !', flags: [MessageFlags.Ephemeral] });
// } else {
// let follow = await userRepo.findOne({ where: { user_discord: interaction.user.id, service: { id: realHost.id } } });
// if (!follow) {
// follow = new Follow();
// follow.user_discord = interaction.user.id;
// follow.service = realHost;
// await userRepo.save(follow);
// }
if (!hostvalue || !realHost) {
await interaction.reply({ content: '⚠️ Host not found !', flags: [MessageFlags.Ephemeral] });
} else {
let follow = await userRepo.findOne({ where: { user_discord: interaction.user.id, service: { id: realHost.id } } });
if (!follow) {
follow = new Follow();
follow.user_discord = interaction.user.id;
follow.service = realHost;
await userRepo.save(follow);
}
// follow.enable = !follow.enable;
follow.enable = !follow.enable;
// await userRepo.save(follow);
await userRepo.save(follow);
// await interaction.reply({ content: `✅ Notification successfully ${follow.enable ? 'enabled 🔔' : 'disabled 🔕'} for ${realHost.name}!`, flags: [MessageFlags.Ephemeral] });
await interaction.reply({ content: `✅ Notification successfully ${follow.enable ? 'enabled 🔔' : 'disabled 🔕'} for ${realHost.name}!`, flags: [MessageFlags.Ephemeral] });
// if (follow.enable) {
// await interaction.user.send({ content: `🔔 Notifications have been successfully enabled for ${realHost.name} ! To disable: /follow host:${realHost.name}` })
// }
// }
// }
// }
if (follow.enable) {
await interaction.user.send({ content: `🔔 Notifications have been successfully enabled for ${realHost.name} ! To disable: /follow host:${realHost.name}` })
}
}
},
autocompletes: [
{
name: 'service',
execute: async (interaction) => {
// export default cmd;
const services = await statusService.serviceRepo.find({where: {notify: true}});
interaction.respond(services.map((v) => ({name: v.name, value: v.name})));
}
}
]
}
export default cmd;

View File

@@ -0,0 +1,54 @@
import { ApplicationIntegrationType, ChatInputCommandInteraction, ContainerBuilder, InteractionContextType, MessageFlags, SlashCommandBuilder } from "discord.js";
import { CommandDefinition } from "../../type";
import statusService from "../../services/status.service";
const cmd: CommandDefinition = {
data: new SlashCommandBuilder()
.setName('uptime')
.setDescription('Get more info for a host.')
.addStringOption((o) =>
o.setName('service')
.setDescription('Select a service')
.setRequired(true)
.setAutocomplete(true)
)
.setIntegrationTypes(
ApplicationIntegrationType.UserInstall
)
.setContexts(
InteractionContextType.BotDM,
InteractionContextType.Guild,
InteractionContextType.PrivateChannel
),
async execute(interaction: ChatInputCommandInteraction) {
const hostvalue = interaction.options.getString('service');
const realHost = await statusService.serviceRepo.findOne({where: {name: hostvalue+''}});
if (!hostvalue || !realHost) {
await interaction.reply({ content: '⚠️ Host not found !', flags: [MessageFlags.Ephemeral] });
} else {
const img = await statusService.getStatusImageBar(realHost.id);
const container = new ContainerBuilder()
.setAccentColor(0x0000ed)
.addTextDisplayComponents((t) => t
.setContent(`## ${realHost.alive ? `${process.env.EMOJI_STATUS_ONLINE}` : `${process.env.EMOJI_STATUS_OFFLINE}`} ${realHost.name}\nService status over 7 days :`)
)
.addMediaGalleryComponents((m) =>
m.addItems((i) => i.setURL('attachment://uptime.png'))
);
await interaction.reply({components: [container], flags: [MessageFlags.IsComponentsV2], files: [{attachment: img, name: 'uptime.png'}]})
}
},
autocompletes: [
{
name: 'service',
execute: async (interaction) => {
const services = await statusService.serviceRepo.find();
interaction.respond(services.map((v) => ({name: v.name, value: v.name})));
}
}
]
}
export default cmd;

View File

@@ -57,6 +57,8 @@ const rest = new REST().setToken(process.env.TOKEN);
(async () => {
try {
console.log("Data Source initialized for command deployment!");
console.log(`Started refreshing ${commands.length} application (/) commands.`);
const data = await rest.put(
@@ -65,6 +67,7 @@ const rest = new REST().setToken(process.env.TOKEN);
) as any[];
console.log(`Successfully reloaded ${data.length} application (/) commands.`);
process.exit(0);
} catch (error) {
console.error('[ERROR] Failed to deploy commands:', error);
process.exit(1);

View File

@@ -60,6 +60,20 @@ client.on(Events.InteractionCreate, async interaction => {
}
});
return;
}else if(interaction.isAutocomplete()){
const option = interaction.options.getFocused(true);
commands.filter((c) => c.data.name == interaction.commandName).forEach((value) => {
if(value.autocompletes) {
const auto = value.autocompletes.filter((a) => a.name == option.name);
if(auto.length >= 1){
auto.forEach((a) => {
a.execute(interaction)
})
}
}
});
return;
}

View File

@@ -81,13 +81,12 @@ export class StatusService {
this.client.user?.setActivity({ name: '💭 Server load and status...' })
}
public async getStatusImageBar(host: string) {
public async getStatusImageBar(serviceId: number) {
const datas = await this.hostsLogRepo.createQueryBuilder()
.where('host = :host AND created_at > :date', {host, date: dayjs().subtract(1, 'week').toDate()}).getMany();
.where('HostsLog.serviceId = :serviceId AND HostsLog.created_at > :date ORDER BY HostsLog.created_at ASC', {serviceId, date: dayjs().subtract(1, 'week').toDate()}).getMany();
const uptimes : { up: boolean, date: Dayjs }[] = datas.map((log) => {
return {
up: log.status,
date: dayjs(log.created_at)
@@ -229,7 +228,8 @@ export class StatusService {
const users = await this.followRepo.find({where: {enable: true}});
const hosts = notifs.map((n) => n.host);
const users_ids : string[] = [];
users.filter(v => hosts.includes(v.service)).forEach(async (user) => {
console.log("Sending notifs...")
users.filter(v => hosts.map((h) => h.id).includes(v.serviceId)).forEach(async (user) => {
if(!users_ids.includes(user.user_discord)) {
users_ids.push(user.user_discord)
try {

4
src/type.d.ts vendored
View File

@@ -1,4 +1,4 @@
import { ButtonInteraction, ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js";
import { AutocompleteInteraction, ButtonInteraction, ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js";
export type InfraType = 'website' | 'ryzen' | 'xeon' | 'games' | 'router';
export type CommandDefinition = { data: SlashCommandBuilder | SlashCommandOptionsOnlyBuilder, execute: (interaction: ChatInputCommandInteraction) => void, buttons?: { id: string, handle: (interaction: ButtonInteraction) => void}[]};
export type CommandDefinition = { data: SlashCommandBuilder | SlashCommandOptionsOnlyBuilder, execute: (interaction: ChatInputCommandInteraction) => void, buttons?: { id: string, handle: (interaction: ButtonInteraction) => void}[], autocompletes?: {name: string, execute: (interaction: AutocompleteInteraction) => void}[]};