From 55e4cf3e6c145747a69cd696e5620bc997d74483 Mon Sep 17 00:00:00 2001 From: thedrewen Date: Wed, 26 Nov 2025 10:57:57 +0100 Subject: [PATCH] feat(database): refactor entities and relationships for service management --- .env.example | 6 +- .gitignore | 2 +- src/commands/utility/follow.command.ts | 93 ++++++------- src/data-source.ts | 6 +- src/entity/follow.entity.ts | 9 +- src/entity/hostslog.entity.ts | 9 +- src/entity/service.entity.ts | 13 +- src/services/status.service.ts | 177 +++++-------------------- src/type.d.ts | 8 -- 9 files changed, 110 insertions(+), 213 deletions(-) diff --git a/.env.example b/.env.example index 740ab0a..58dc932 100644 --- a/.env.example +++ b/.env.example @@ -18,8 +18,4 @@ DB_DATABASE=protojx_manager DB_LOGGING=false # Environment -NODE_ENV=development - -# Protected IPS -PROTOJX_ROUTER_1= -PROTOJX_ROUTER_2= \ No newline at end of file +NODE_ENV=development \ No newline at end of file diff --git a/.gitignore b/.gitignore index 4aa3500..825658c 100644 --- a/.gitignore +++ b/.gitignore @@ -137,4 +137,4 @@ dist # Vite logs files vite.config.js.timestamp-* vite.config.ts.timestamp-* -node.txt \ No newline at end of file +note.txt \ No newline at end of file diff --git a/src/commands/utility/follow.command.ts b/src/commands/utility/follow.command.ts index 45100d1..d7114e8 100644 --- a/src/commands/utility/follow.command.ts +++ b/src/commands/utility/follow.command.ts @@ -1,55 +1,50 @@ -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 - ) - .addStringOption((option) => - option - .setRequired(true) - .addChoices(...statusService.hosts.filter((v) => v.notify).map((s) => ({name: s.name, value: s.host}))) - .setName('host') - .setDescription('Host enable/disable.') - ), - 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.') +// .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 services = await statusService.serviceRepo.find(); +// const realHost = services.find((v) => v.notify && v.host == hostvalue); - const realHost = statusService.hosts.filter((v) => v.host == hostvalue); - if(!hostvalue || realHost.length == 0) { - await interaction.reply({content: '⚠️ Host not found !', flags: [MessageFlags.Ephemeral]}); - }else{ - let follow = await userRepo.findOne({where: {user_discord: interaction.user.id, host: hostvalue}}); - if(!follow) { - follow = new Follow(); - follow.user_discord = interaction.user.id; - follow.host = hostvalue; - await userRepo.save(follow); - } - - follow.enable = !follow.enable; +// 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); +// } - await userRepo.save(follow); +// follow.enable = !follow.enable; - await interaction.reply({content: `✅ Notification successfully ${follow.enable ? 'enabled 🔔' : 'disabled 🔕'} for ${realHost[0]?.name}!`, flags: [MessageFlags.Ephemeral]}); +// await userRepo.save(follow); - if(follow.enable) { - await interaction.user.send({content: `🔔 Notifications have been successfully enabled for ${realHost[0]?.name} ! To disable: /follow host:${realHost[0]?.name}`}) - } - } - } -} +// await interaction.reply({ content: `✅ Notification successfully ${follow.enable ? 'enabled 🔔' : 'disabled 🔕'} for ${realHost.name}!`, flags: [MessageFlags.Ephemeral] }); -export default cmd; \ No newline at end of file +// if (follow.enable) { +// await interaction.user.send({ content: `🔔 Notifications have been successfully enabled for ${realHost.name} ! To disable: /follow host:${realHost.name}` }) +// } +// } +// } +// } + +// export default cmd; \ No newline at end of file diff --git a/src/data-source.ts b/src/data-source.ts index 8159cd4..0bcd240 100644 --- a/src/data-source.ts +++ b/src/data-source.ts @@ -1,6 +1,10 @@ import "reflect-metadata" import { DataSource } from "typeorm" import { configDotenv } from "dotenv" +import { Follow } from "./entity/follow.entity" +import { Guild } from "./entity/guild.entity" +import { HostsLog } from "./entity/hostslog.entity" +import { Service } from "./entity/service.entity" configDotenv() @@ -13,7 +17,7 @@ export const AppDataSource = new DataSource({ database: process.env.DB_DATABASE, synchronize: process.env.NODE_ENV !== "production", logging: process.env.DB_LOGGING === "true", - entities: [__dirname + '/**/*.entity.js'], + entities: [Follow, Guild, HostsLog, Service], migrations: [__dirname + "/**/*.migration.js"], subscribers: [__dirname + "/**/*.subscriber.js"], }) \ No newline at end of file diff --git a/src/entity/follow.entity.ts b/src/entity/follow.entity.ts index 4655c22..16136c1 100644 --- a/src/entity/follow.entity.ts +++ b/src/entity/follow.entity.ts @@ -1,4 +1,5 @@ -import { Column, Entity, PrimaryGeneratedColumn } from "typeorm"; +import { Column, Entity, JoinColumn, ManyToOne, PrimaryGeneratedColumn } from "typeorm"; +import { Service } from "./service.entity"; @Entity({name: 'follows'}) export class Follow { @@ -8,8 +9,12 @@ export class Follow { @Column() user_discord: string; + @ManyToOne(() => Service, service => service.follows) + @JoinColumn({name: 'serviceId'}) + service: Service; + @Column() - host: string; + serviceId: number; @Column({default: false}) enable: boolean; diff --git a/src/entity/hostslog.entity.ts b/src/entity/hostslog.entity.ts index b1847b6..126ed02 100644 --- a/src/entity/hostslog.entity.ts +++ b/src/entity/hostslog.entity.ts @@ -1,12 +1,17 @@ -import { Column, CreateDateColumn, Entity, PrimaryGeneratedColumn } from "typeorm"; +import { Column, CreateDateColumn, Entity, JoinColumn, ManyToOne, PrimaryGeneratedColumn } from "typeorm"; +import { Service } from "./service.entity"; @Entity({name: 'hosts_logs'}) export class HostsLog { @PrimaryGeneratedColumn() id: number; + @ManyToOne(() => Service, service => service.logs) + @JoinColumn({name: 'serviceId'}) + service: Service; + @Column() - host: string; + serviceId: number; @Column() status: boolean; diff --git a/src/entity/service.entity.ts b/src/entity/service.entity.ts index 8cacd3c..32ad2ca 100644 --- a/src/entity/service.entity.ts +++ b/src/entity/service.entity.ts @@ -1,4 +1,6 @@ -import { Column, Entity, PrimaryGeneratedColumn } from "typeorm"; +import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from "typeorm"; +import { HostsLog } from "./hostslog.entity"; +import { Follow } from "./follow.entity"; @Entity({name: 'services'}) export class Service { @@ -21,5 +23,12 @@ export class Service { type: string; @Column() - notify: boolean; + notify: boolean + + + @OneToMany(() => HostsLog, log => log.service) + logs: HostsLog[]; + + @OneToMany(() => Follow, follow => follow.service) + follows: Follow[]; } \ No newline at end of file diff --git a/src/services/status.service.ts b/src/services/status.service.ts index 3f7afbc..082a60a 100644 --- a/src/services/status.service.ts +++ b/src/services/status.service.ts @@ -1,7 +1,7 @@ import ping from "ping"; import * as cron from 'cron'; import { ActivityType, Client, ContainerBuilder, MessageFlags } from "discord.js"; -import { Host, InfraType } from "../type"; +import { InfraType } from "../type"; import { AppDataSource } from "../data-source"; import { HostsLog } from "../entity/hostslog.entity"; import { Repository } from "typeorm"; @@ -11,131 +11,15 @@ import dayjs, { Dayjs } from "dayjs"; import { Canvas } from "canvas"; import { Service } from "../entity/service.entity"; -type Nofity = {time: Date, name : string, alive : boolean, type : InfraType, host: string}; +type Nofity = {time: Date, name : string, alive : boolean, type : string, host: Service}; export class StatusService { - public hosts: Host[] = [ - { - host: 'https://protojx.com', - name: 'Protojx Website', - alive: false, - ping_type: 'website', - type: 'website', - notify: false - }, - { - host: 'https://manager.protojx.com', - name: 'Espace Client', - alive: false, - ping_type: 'website', - type: 'website', - notify: false - }, - { - host: '5.178.99.4', - name: 'RYZEN 01', - alive: false, - ping_type: 'ping', - type: 'ryzen', - notify: true - }, - { - host: '5.178.99.240', - name: 'RYZEN 02', - alive: false, - ping_type: 'ping', - type: 'ryzen', - notify: true - }, - { - host: '5.178.99.5', - name: 'RYZEN 03', - alive: false, - ping_type: 'ping', - type: 'ryzen', - notify: true - }, - { - host: '144.76.35.26', - name: 'RYZEN7 04', - alive: false, - ping_type: 'ping', - type: 'ryzen', - notify: true - }, - { - host: '5.178.99.177', - name: 'XEON 01', - alive: false, - ping_type: 'ping', - type: 'xeon', - notify: true - }, - { - host: '154.16.254.45', - name: 'XEON 02', - alive: false, - ping_type: 'ping', - type: 'xeon', - notify: true - }, - { - host: '5.178.99.232', - name: 'XEON 03', - alive: false, - ping_type: 'ping', - type: 'xeon', - notify: true - }, - { - host: '5.178.99.53', - name: 'RYZEN-GAME 01', - alive: false, - ping_type: 'ping', - type: 'games', - notify: true - }, - { - host: '5.178.99.17', - name: 'RYZEN-GAME 02', - alive: false, - ping_type: 'ping', - type: 'games', - notify: true - }, - { - host: '5.178.99.63', - name: 'XEON-GAME 01', - alive: false, - ping_type: 'ping', - type: 'games', - notify: true - }, - // Routers - { - host: process.env.PROTOJX_ROUTER_1 as string, - name: 'ROUTER-FR 01', - alive: false, - ping_type: 'ping', - type: 'router', - notify: false - }, - { - host: process.env.PROTOJX_ROUTER_2 as string, - name: 'ROUTER-FR 02', - alive: false, - ping_type: 'ping', - type: 'router', - notify: false - } - ]; - private client: Client | null = null; private hostsLogRepo: Repository; private followRepo: Repository; private guildRepo: Repository; - private serviceRepo: Repository; + public serviceRepo: Repository; constructor() { @@ -172,13 +56,14 @@ export class StatusService { if(this.client) { try { const guild = await this.client.guilds.fetch(gdb.guild_id); + console.log(guild.name) const channel = await guild.channels.fetch(gdb.persistent_message_channel_id); if(channel?.isSendable()) { const message = await channel.messages.fetch(gdb.persistent_message_id); await message.edit({components: [await this.getUpdatedContainer(true)]}); } } catch (error) { - console.log(error) + console.log(error + ' GuildIdInDB : '+gdb.id); } } }); @@ -266,8 +151,9 @@ export class StatusService { private async updateClientStatus() { if (this.client) { - const hosts = this.hosts.length; - const hostsAlive = this.hosts.filter((h) => h.alive).length; + const hosts_db = await this.serviceRepo.find(); + const hosts = hosts_db.length; + const hostsAlive = hosts_db.filter((h) => h.alive).length; this.client.user?.setActivity({ name: ( @@ -277,56 +163,59 @@ export class StatusService { } } - private async fetchAlive(host: Host, notifs : Nofity[]) { + private async fetchAlive(service: Service, notifs : Nofity[]) { - const latestLog = await this.hostsLogRepo.findOne({ where: { host: host.host }, order: { created_at: 'DESC' } }); + const latestLog = await this.hostsLogRepo.findOne({ where: { service }, order: { created_at: 'DESC' } }); // ? Ping and Request Hosts - if (host.ping_type === 'ping') { - let res = await ping.promise.probe(host.host, { timeout: 10 }); - host.alive = res.alive; - } else if (host.ping_type === 'website') { + if (service.ping_type === 'ping') { + let res = await ping.promise.probe(service.host, { timeout: 10 }); + service.alive = res.alive; + } else if (service.ping_type === 'website') { try { - const response = await fetch(host.host, { method: 'HEAD', signal: AbortSignal.timeout(10000) }); - host.alive = response.ok; + const response = await fetch(service.host, { method: 'HEAD', signal: AbortSignal.timeout(10000) }); + service.alive = response.ok; } catch (error) { - host.alive = false; + service.alive = false; } } // ? Notification System : - if (!latestLog || latestLog.status != host.alive) { + if (!latestLog || latestLog.status != service.alive) { const log = new HostsLog(); - log.host = host.host; - log.status = host.alive; + log.service = service; + log.status = service.alive; this.hostsLogRepo.save(log); - if(latestLog && host.notify) { - notifs.push({alive: host.alive, name: host.name, time: new Date(), type: host.type, host: host.host}); + if(latestLog && service.notify) { + notifs.push({alive: service.alive, name: service.name, time: new Date(), type: service.type, host: service}); } } - return host; + this.serviceRepo.save(service); + + return service; } private async fetch(max = 1, notifs : Nofity[] = []) { const max_ping = 3; - const hosts = this.hosts.filter((value, index) => index < max * max_ping && index >= (max - 1) * max_ping); + const services = await this.serviceRepo.find(); + const hosts = services.filter((value, index) => index < max * max_ping && index >= (max - 1) * max_ping); const fetchPromises = hosts.map(host => this.fetchAlive(host, notifs)); const updatedHosts = await Promise.all(fetchPromises); updatedHosts.forEach((updatedHost, index) => { const originalIndex = (max - 1) * max_ping + index; - if (originalIndex < this.hosts.length) { - this.hosts[originalIndex] = updatedHost; + if (originalIndex < services.length) { + services[originalIndex] = updatedHost; } }); - if (this.hosts.length > max * max_ping) { + if (services.length > max * max_ping) { await this.fetch(max + 1, notifs); }else if(notifs.length > 0){ // ? Notification System (part 2 !): @@ -341,7 +230,7 @@ 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.host)).forEach(async (user) => { + users.filter(v => hosts.includes(v.service)).forEach(async (user) => { if(!users_ids.includes(user.user_discord)) { users_ids.push(user.user_discord) try { @@ -356,7 +245,9 @@ export class StatusService { } public async getUpdatedContainer(live : boolean = false): Promise { - const hostTexts = this.hosts.map((s) => { + const services = await this.serviceRepo.find({order: {id: 'ASC'}}); + + const hostTexts = services.map((s) => { return { type: s.type, value: `- ${s.name} : ${s.alive ? `${process.env.EMOJI_STATUS_ONLINE} Online` : `${process.env.EMOJI_STATUS_OFFLINE} Offline`}` }; }); diff --git a/src/type.d.ts b/src/type.d.ts index 6c08543..5b13484 100644 --- a/src/type.d.ts +++ b/src/type.d.ts @@ -1,12 +1,4 @@ import { ButtonInteraction, ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js"; export type InfraType = 'website' | 'ryzen' | 'xeon' | 'games' | 'router'; -export type Host = { - host: string, - name: string, - alive: boolean, - ping_type: 'ping' | 'website', - type: InfraType, - notify: boolean; -}; export type CommandDefinition = { data: SlashCommandBuilder | SlashCommandOptionsOnlyBuilder, execute: (interaction: ChatInputCommandInteraction) => void, buttons?: { id: string, handle: (interaction: ButtonInteraction) => void}[]}; \ No newline at end of file