10 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
ecb12f12b7 fix(status): remove unnecessary console log for guild name in StatusService 2025-11-26 11:01:06 +01:00
55e4cf3e6c feat(database): refactor entities and relationships for service management 2025-11-26 10:57:57 +01:00
104023162a feat(service): add Service entity with properties for service management 2025-11-26 09:45:16 +01:00
58403fd32e fix(deploy): remove redundant line for building new Docker image in deployment script 2025-11-26 09:25:34 +01:00
18 changed files with 207 additions and 354 deletions

View File

@@ -19,7 +19,3 @@ DB_LOGGING=false
# Environment # Environment
NODE_ENV=development NODE_ENV=development
# Protected IPS
PROTOJX_ROUTER_1=
PROTOJX_ROUTER_2=

View File

@@ -36,13 +36,13 @@ jobs:
cd /home/${{ secrets.VPS_USER }}/protojx/protojx-manager && cd /home/${{ secrets.VPS_USER }}/protojx/protojx-manager &&
git pull && git pull &&
# Build new image
docker buildx build -t protojx_manager . &&
# Stop and remove old container if exists # Stop and remove old container if exists
docker stop protojx_manager 2>/dev/null || true && docker stop protojx_manager 2>/dev/null || true &&
docker rm protojx_manager 2>/dev/null || true && docker rm protojx_manager 2>/dev/null || true &&
# Build new image
docker buildx build -t protojx_manager . &&
# Run new container # Run new container
docker run -d \ docker run -d \
--name protojx_manager \ --name protojx_manager \

1
.gitignore vendored
View File

@@ -137,3 +137,4 @@ dist
# Vite logs files # Vite logs files
vite.config.js.timestamp-* vite.config.js.timestamp-*
vite.config.ts.timestamp-* vite.config.ts.timestamp-*
note.txt

View File

@@ -5,8 +5,9 @@ WORKDIR /app
COPY . . COPY . .
RUN apt-get update && apt-get install -y iputils-ping RUN apt-get update && apt-get install -y iputils-ping
RUN npm i RUN npm clean-install
# RUN npm run register # RUN npm run register
ENTRYPOINT [ "npm", "run", "register" ]
CMD [ "npm", "run", "start" ] 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", "resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz",
"integrity": "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==", "integrity": "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"pg-connection-string": "^2.9.1", "pg-connection-string": "^2.9.1",
"pg-pool": "^3.10.1", "pg-pool": "^3.10.1",
@@ -1487,8 +1486,7 @@
"version": "0.2.2", "version": "0.2.2",
"resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz",
"integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==",
"license": "Apache-2.0", "license": "Apache-2.0"
"peer": true
}, },
"node_modules/require-directory": { "node_modules/require-directory": {
"version": "2.1.1", "version": "2.1.1",

View File

@@ -4,10 +4,16 @@ import { AppDataSource } from "../../data-source";
import { Follow } from "../../entity/follow.entity"; import { Follow } from "../../entity/follow.entity";
import statusService from "../../services/status.service"; import statusService from "../../services/status.service";
const cmd : CommandDefinition = { const cmd: CommandDefinition = {
data: new SlashCommandBuilder() data: new SlashCommandBuilder()
.setName('follow') .setName('follow')
.setDescription('Enables/disables the receipt of service status notifications.') .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( .setIntegrationTypes(
ApplicationIntegrationType.UserInstall ApplicationIntegrationType.UserInstall
) )
@@ -15,27 +21,21 @@ const cmd : CommandDefinition = {
InteractionContextType.BotDM, InteractionContextType.BotDM,
InteractionContextType.Guild, InteractionContextType.Guild,
InteractionContextType.PrivateChannel 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) { async execute(interaction: ChatInputCommandInteraction) {
const userRepo = AppDataSource.getRepository(Follow); const userRepo = AppDataSource.getRepository(Follow);
const hostvalue = interaction.options.getString('host'); const hostvalue = interaction.options.getString('service');
const realHost = statusService.hosts.filter((v) => v.host == hostvalue); const realHost = await statusService.serviceRepo.findOne({where: {name: hostvalue+''}});
if(!hostvalue || realHost.length == 0) {
await interaction.reply({content: '⚠️ Host not found !', flags: [MessageFlags.Ephemeral]}); if (!hostvalue || !realHost) {
}else{ await interaction.reply({ content: '⚠️ Host not found !', flags: [MessageFlags.Ephemeral] });
let follow = await userRepo.findOne({where: {user_discord: interaction.user.id, host: hostvalue}}); } else {
if(!follow) { let follow = await userRepo.findOne({ where: { user_discord: interaction.user.id, service: { id: realHost.id } } });
if (!follow) {
follow = new Follow(); follow = new Follow();
follow.user_discord = interaction.user.id; follow.user_discord = interaction.user.id;
follow.host = hostvalue; follow.service = realHost;
await userRepo.save(follow); await userRepo.save(follow);
} }
@@ -43,13 +43,24 @@ const cmd : CommandDefinition = {
await userRepo.save(follow); await userRepo.save(follow);
await interaction.reply({content: `✅ Notification successfully ${follow.enable ? 'enabled 🔔' : 'disabled 🔕'} for ${realHost[0]?.name}!`, flags: [MessageFlags.Ephemeral]}); await interaction.reply({ content: `✅ Notification successfully ${follow.enable ? 'enabled 🔔' : 'disabled 🔕'} for ${realHost.name}!`, flags: [MessageFlags.Ephemeral] });
if(follow.enable) { 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.user.send({ content: `🔔 Notifications have been successfully enabled for ${realHost.name} ! To disable: /follow host:${realHost.name}` })
} }
} }
} },
autocompletes: [
{
name: 'service',
execute: async (interaction) => {
const services = await statusService.serviceRepo.find({where: {notify: true}});
interaction.respond(services.map((v) => ({name: v.name, value: v.name})));
}
}
]
} }
export default cmd; 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

@@ -1,6 +1,10 @@
import "reflect-metadata" import "reflect-metadata"
import { DataSource } from "typeorm" import { DataSource } from "typeorm"
import { configDotenv } from "dotenv" 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() configDotenv()
@@ -13,7 +17,7 @@ export const AppDataSource = new DataSource({
database: process.env.DB_DATABASE, database: process.env.DB_DATABASE,
synchronize: process.env.NODE_ENV !== "production", synchronize: process.env.NODE_ENV !== "production",
logging: process.env.DB_LOGGING === "true", logging: process.env.DB_LOGGING === "true",
entities: [__dirname + '/**/*.entity.js'], entities: [Follow, Guild, HostsLog, Service],
migrations: [__dirname + "/**/*.migration.js"], migrations: [__dirname + "/**/*.migration.js"],
subscribers: [__dirname + "/**/*.subscriber.js"], subscribers: [__dirname + "/**/*.subscriber.js"],
}) })

View File

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

View File

@@ -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'}) @Entity({name: 'follows'})
export class Follow { export class Follow {
@@ -8,8 +9,12 @@ export class Follow {
@Column() @Column()
user_discord: string; user_discord: string;
@ManyToOne(() => Service, service => service.follows)
@JoinColumn({name: 'serviceId'})
service: Service;
@Column() @Column()
host: string; serviceId: number;
@Column({default: false}) @Column({default: false})
enable: boolean; enable: boolean;

View File

@@ -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'}) @Entity({name: 'hosts_logs'})
export class HostsLog { export class HostsLog {
@PrimaryGeneratedColumn() @PrimaryGeneratedColumn()
id: number; id: number;
@ManyToOne(() => Service, service => service.logs)
@JoinColumn({name: 'serviceId'})
service: Service;
@Column() @Column()
host: string; serviceId: number;
@Column() @Column()
status: boolean; status: boolean;

View File

@@ -0,0 +1,34 @@
import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from "typeorm";
import { HostsLog } from "./hostslog.entity";
import { Follow } from "./follow.entity";
@Entity({name: 'services'})
export class Service {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@Column()
host: string;
@Column()
alive: boolean;
@Column()
ping_type: string;
@Column()
type: string;
@Column()
notify: boolean
@OneToMany(() => HostsLog, log => log.service)
logs: HostsLog[];
@OneToMany(() => Follow, follow => follow.service)
follows: Follow[];
}

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; return;
} }

View File

@@ -1,7 +1,7 @@
import ping from "ping"; import ping from "ping";
import * as cron from 'cron'; import * as cron from 'cron';
import { ActivityType, Client, ContainerBuilder, MessageFlags } from "discord.js"; import { ActivityType, Client, ContainerBuilder, MessageFlags } from "discord.js";
import { Host, InfraType } from "../type"; import { InfraType } from "../type";
import { AppDataSource } from "../data-source"; import { AppDataSource } from "../data-source";
import { HostsLog } from "../entity/hostslog.entity"; import { HostsLog } from "../entity/hostslog.entity";
import { Repository } from "typeorm"; import { Repository } from "typeorm";
@@ -9,137 +9,24 @@ import { Follow } from "../entity/follow.entity";
import { Guild } from "../entity/guild.entity"; import { Guild } from "../entity/guild.entity";
import dayjs, { Dayjs } from "dayjs"; import dayjs, { Dayjs } from "dayjs";
import { Canvas } from "canvas"; 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 { 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 client: Client | null = null;
private hostsLogRepo: Repository<HostsLog>; private hostsLogRepo: Repository<HostsLog>;
private followRepo: Repository<Follow>; private followRepo: Repository<Follow>;
private guildRepo: Repository<Guild>; private guildRepo: Repository<Guild>;
public serviceRepo: Repository<Service>;
constructor() { constructor() {
this.hostsLogRepo = AppDataSource.getRepository(HostsLog); this.hostsLogRepo = AppDataSource.getRepository(HostsLog);
this.followRepo = AppDataSource.getRepository(Follow); this.followRepo = AppDataSource.getRepository(Follow);
this.guildRepo = AppDataSource.getRepository(Guild); this.guildRepo = AppDataSource.getRepository(Guild);
this.serviceRepo = AppDataSource.getRepository(Service);
setTimeout(async () => { setTimeout(async () => {
await this.fetch() await this.fetch()
@@ -175,7 +62,7 @@ export class StatusService {
await message.edit({components: [await this.getUpdatedContainer(true)]}); await message.edit({components: [await this.getUpdatedContainer(true)]});
} }
} catch (error) { } catch (error) {
console.log(error) console.log(error + ' GuildIdInDB : '+gdb.id);
} }
} }
}); });
@@ -194,13 +81,12 @@ export class StatusService {
this.client.user?.setActivity({ name: '💭 Server load and status...' }) 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() 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) => { const uptimes : { up: boolean, date: Dayjs }[] = datas.map((log) => {
return { return {
up: log.status, up: log.status,
date: dayjs(log.created_at) date: dayjs(log.created_at)
@@ -263,8 +149,9 @@ export class StatusService {
private async updateClientStatus() { private async updateClientStatus() {
if (this.client) { if (this.client) {
const hosts = this.hosts.length; const hosts_db = await this.serviceRepo.find();
const hostsAlive = this.hosts.filter((h) => h.alive).length; const hosts = hosts_db.length;
const hostsAlive = hosts_db.filter((h) => h.alive).length;
this.client.user?.setActivity({ this.client.user?.setActivity({
name: ( name: (
@@ -274,56 +161,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 // ? Ping and Request Hosts
if (host.ping_type === 'ping') { if (service.ping_type === 'ping') {
let res = await ping.promise.probe(host.host, { timeout: 10 }); let res = await ping.promise.probe(service.host, { timeout: 10 });
host.alive = res.alive; service.alive = res.alive;
} else if (host.ping_type === 'website') { } else if (service.ping_type === 'website') {
try { try {
const response = await fetch(host.host, { method: 'HEAD', signal: AbortSignal.timeout(10000) }); const response = await fetch(service.host, { method: 'HEAD', signal: AbortSignal.timeout(10000) });
host.alive = response.ok; service.alive = response.ok;
} catch (error) { } catch (error) {
host.alive = false; service.alive = false;
} }
} }
// ? Notification System : // ? Notification System :
if (!latestLog || latestLog.status != host.alive) { if (!latestLog || latestLog.status != service.alive) {
const log = new HostsLog(); const log = new HostsLog();
log.host = host.host; log.service = service;
log.status = host.alive; log.status = service.alive;
this.hostsLogRepo.save(log); this.hostsLogRepo.save(log);
if(latestLog && host.notify) { if(latestLog && service.notify) {
notifs.push({alive: host.alive, name: host.name, time: new Date(), type: host.type, host: host.host}); 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[] = []) { private async fetch(max = 1, notifs : Nofity[] = []) {
const max_ping = 3; 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 fetchPromises = hosts.map(host => this.fetchAlive(host, notifs));
const updatedHosts = await Promise.all(fetchPromises); const updatedHosts = await Promise.all(fetchPromises);
updatedHosts.forEach((updatedHost, index) => { updatedHosts.forEach((updatedHost, index) => {
const originalIndex = (max - 1) * max_ping + index; const originalIndex = (max - 1) * max_ping + index;
if (originalIndex < this.hosts.length) { if (originalIndex < services.length) {
this.hosts[originalIndex] = updatedHost; services[originalIndex] = updatedHost;
} }
}); });
if (this.hosts.length > max * max_ping) { if (services.length > max * max_ping) {
await this.fetch(max + 1, notifs); await this.fetch(max + 1, notifs);
}else if(notifs.length > 0){ }else if(notifs.length > 0){
// ? Notification System (part 2 !): // ? Notification System (part 2 !):
@@ -338,7 +228,8 @@ export class StatusService {
const users = await this.followRepo.find({where: {enable: true}}); const users = await this.followRepo.find({where: {enable: true}});
const hosts = notifs.map((n) => n.host); const hosts = notifs.map((n) => n.host);
const users_ids : string[] = []; const users_ids : string[] = [];
users.filter(v => hosts.includes(v.host)).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)) { if(!users_ids.includes(user.user_discord)) {
users_ids.push(user.user_discord) users_ids.push(user.user_discord)
try { try {
@@ -353,7 +244,9 @@ export class StatusService {
} }
public async getUpdatedContainer(live : boolean = false): Promise<ContainerBuilder> { 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`}` }; return { type: s.type, value: `- ${s.name} : ${s.alive ? `${process.env.EMOJI_STATUS_ONLINE} Online` : `${process.env.EMOJI_STATUS_OFFLINE} Offline`}` };
}); });

12
src/type.d.ts vendored
View File

@@ -1,12 +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 InfraType = 'website' | 'ryzen' | 'xeon' | 'games' | 'router';
export type Host = { export type CommandDefinition = { data: SlashCommandBuilder | SlashCommandOptionsOnlyBuilder, execute: (interaction: ChatInputCommandInteraction) => void, buttons?: { id: string, handle: (interaction: ButtonInteraction) => void}[], autocompletes?: {name: string, execute: (interaction: AutocompleteInteraction) => void}[]};
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}[]};

77
test.js
View File

@@ -1,77 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
var canvas_1 = require("canvas");
var dayjs = require("dayjs");
var fs_1 = require("fs");
function createUptimeBar(uptimes) {
var now = dayjs();
var week = now.clone().subtract(1, 'week');
var canvas = new canvas_1.Canvas(100, 2, "image");
var ctx = canvas.getContext('2d');
ctx.fillStyle = "#27FF00";
ctx.fillRect(0, 0, canvas.width, canvas.height);
var maxTime = (now.unix() - week.unix());
var ranges = [];
var minTime = null;
uptimes.map(function (element, index) {
var positionForMaxTime = (element.date.unix() - week.unix());
var percent = Math.round((positionForMaxTime / maxTime) * 100);
if (ranges.length == 0 && minTime == null) {
if (element.up && minTime == null) {
ranges.push({
min: 0,
max: percent
});
}
else {
minTime = percent;
}
}
else {
if (!element.up) {
minTime = percent;
if (minTime != null && index == uptimes.length - 1) {
ranges.push({
min: minTime,
max: 100
});
}
}
else {
if (minTime) {
ranges.push({
min: minTime,
max: percent
});
}
}
}
});
ctx.fillStyle = '#ff0000';
ranges.map(function (value) {
ctx.fillRect(value.min, 0, value.max - value.min, canvas.height);
});
(0, fs_1.writeFile)('test.png', canvas.toBuffer('image/png'), function (err) {
if (err)
throw err;
console.log('Image saved!');
});
}
createUptimeBar([
{
up: true,
date: dayjs().subtract(6, 'day')
},
{
up: false,
date: dayjs().subtract(3, 'day')
},
{
up: true,
date: dayjs().subtract(1, 'day')
},
{
up: false,
date: dayjs().subtract(1, 'hour')
}
]);

BIN
test.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 115 B

81
test.ts
View File

@@ -1,81 +0,0 @@
import { Canvas } from "canvas";
import * as dayjs from "dayjs";
import { Dayjs } from "dayjs";
import { writeFile } from "fs";
function createUptimeBar(uptimes: { up: boolean, date: Dayjs }[]) {
const now = dayjs();
const week = now.clone().subtract(1, 'week');
const canvas = new Canvas(100, 2, "image");
const ctx = canvas.getContext('2d');
ctx.fillStyle = "#27FF00";
ctx.fillRect(0, 0, canvas.width, canvas.height);
const maxTime = (now.unix() - week.unix());
const ranges: { min: number, max: number }[] = [];
let minTime: number | null = null;
uptimes.map((element, index) => {
const positionForMaxTime = (element.date.unix() - week.unix());
const percent = Math.round((positionForMaxTime / maxTime) * 100);
if (ranges.length == 0 && minTime == null) {
if (element.up && minTime == null) {
ranges.push({
min: 0,
max: percent
});
} else {
minTime = percent;
}
} else {
if (!element.up) {
minTime = percent;
if(minTime != null && index == uptimes.length - 1) {
ranges.push({
min: minTime,
max: 100
});
}
} else {
if (minTime) {
ranges.push({
min: minTime,
max: percent
});
}
}
}
});
ctx.fillStyle = '#ff0000';
ranges.map((value) => {
ctx.fillRect(value.min, 0, value.max - value.min, canvas.height);
});
writeFile('test.png', canvas.toBuffer('image/png'), (err) => {
if (err) throw err;
console.log('Image saved!');
});
}
createUptimeBar([
{
up: true,
date: dayjs().subtract(6, 'day')
},
{
up: false,
date: dayjs().subtract(3, 'day')
},
{
up: true,
date: dayjs().subtract(1, 'day')
},
{
up: false,
date: dayjs().subtract(1, 'hour')
}
]);