mirror of
https://github.com/thedrewen/protojx-manager.git
synced 2026-03-21 01:48:54 +01:00
feat(database): refactor entities and relationships for service management
This commit is contained in:
@@ -18,8 +18,4 @@ DB_DATABASE=protojx_manager
|
||||
DB_LOGGING=false
|
||||
|
||||
# Environment
|
||||
NODE_ENV=development
|
||||
|
||||
# Protected IPS
|
||||
PROTOJX_ROUTER_1=
|
||||
PROTOJX_ROUTER_2=
|
||||
NODE_ENV=development
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -137,4 +137,4 @@ dist
|
||||
# Vite logs files
|
||||
vite.config.js.timestamp-*
|
||||
vite.config.ts.timestamp-*
|
||||
node.txt
|
||||
note.txt
|
||||
@@ -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;
|
||||
// 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;
|
||||
@@ -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"],
|
||||
})
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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[];
|
||||
}
|
||||
@@ -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<HostsLog>;
|
||||
private followRepo: Repository<Follow>;
|
||||
private guildRepo: Repository<Guild>;
|
||||
private serviceRepo: Repository<Service>;
|
||||
public serviceRepo: Repository<Service>;
|
||||
|
||||
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<ContainerBuilder> {
|
||||
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`}` };
|
||||
});
|
||||
|
||||
|
||||
8
src/type.d.ts
vendored
8
src/type.d.ts
vendored
@@ -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}[]};
|
||||
Reference in New Issue
Block a user