27 Commits

Author SHA1 Message Date
956536a717 fix(status): prevent duplicate notifications by tracking user IDs 2025-11-03 11:53:02 +01:00
0f22892816 fix(status): filter users by enabled status in follow repository query 2025-11-03 11:46:53 +01:00
a593e05f5c feat(follow): add host option for notifications and update follow entity 2025-11-03 11:45:42 +01:00
d417e7334b fix(readme): update status of notification system and persistent messages 2025-11-03 10:15:27 +01:00
96fcb53e0b fix(status): simplify name for XEON 01 host entry in status service 2025-11-03 10:02:35 +01:00
fc5c2b1e63 feat(status): add XEON 02 and XEON 03 host entries to status service 2025-11-03 09:59:28 +01:00
3821583d1d refactor(status): comment out XEON 02 host entry in status service 2025-11-03 09:11:28 +01:00
91e95127c1 fix(status): correct spelling of 'Protojx' in status display message 2025-11-02 16:55:31 +01:00
f05a35965e chore(docker): add WORKDIR instruction to set application directory 2025-10-31 17:36:16 +01:00
b9930cbc8f feat(live_status): add command to generate and update persistent status messages 2025-10-31 15:09:12 +01:00
d3fba3668e feat(status): enhance notification system to include status change alerts for all hosts 2025-10-31 13:31:06 +01:00
49da70082e feat(status): ensure notifications are sent only for enabled hosts 2025-10-31 12:30:20 +01:00
37fbfd1c8c feat(status): update notification message in text display for outage alerts 2025-10-31 11:04:42 +01:00
b13c77d9a5 feat(dependencies): update discord.js to version 14.24.2 2025-10-31 10:57:08 +01:00
24ed5e6a62 feat(readme): update notification system status to completed 2025-10-31 10:24:20 +01:00
d4e90640b6 feat(status): add follow notifications for status changes and integrate follow repository 2025-10-31 10:24:02 +01:00
522ed2ba81 feat(status): implement log cleanup and enhance status fetching in StatusService 2025-10-31 10:04:06 +01:00
6b10aaa009 feat: refactor command imports, enhance status service with logging and notification features, and update entity definitions 2025-10-31 10:02:44 +01:00
03769b14fa feat(status): refactor status command to utilize centralized container generation in status service 2025-10-31 09:31:35 +01:00
d964ec7963 feat(status): enhance server status command with structured response and improved host categorization 2025-10-31 08:40:11 +01:00
17c00211da feat(status): add emoji support for RYZEN and XEON hosts in status monitoring 2025-10-31 08:06:48 +01:00
4bb33bea89 Merge branch 'main' of https://github.com/thedrewen/protojx-manager-non-official 2025-10-31 07:52:47 +01:00
bbd071c44f fix(follow): correct integration type from GuildInstall to UserInstall 2025-10-31 07:52:45 +01:00
3c6fd92cc2 fix(status): increase ping timeout from 3 to 10 seconds for improved reliability 2025-10-30 21:16:43 +01:00
5f770d861d fix(fetch): adjust host filtering logic to use max_ping for improved accuracy 2025-10-30 19:55:21 +01:00
2822908ea0 feat(follow): implement follow command for service status notifications
refactor(data-source): update entity and migration paths for consistency
fix(readme): update status feature notification system icon
delete(guilds): remove unused Guild entity
add(follow): create Follow entity for managing user notification preferences
2025-10-30 08:41:21 +01:00
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
18 changed files with 433 additions and 88 deletions

View File

@@ -6,6 +6,9 @@ CLIENT_ID=your_discord_client_id_here
EMOJI_STATUS_ONLINE=<a:online:1432684754276323431>
EMOJI_STATUS_OFFLINE=<a:offline:1432684900175183882>
EMOJI_RYZEN=<:ryzen:1433711892009848833>
EMOJI_XEON=<:xeon:1433711864168054855>
# Database Configuration
DB_HOST=localhost
DB_PORT=5432

View File

@@ -1,5 +1,7 @@
FROM node:22
WORKDIR /app
COPY . .
RUN apt-get update && apt-get install -y iputils-ping

View File

@@ -10,9 +10,10 @@ A status bot and other features for protojx.
|-------------|--------|
| /status command | 🌐 |
| 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. | |
| Notification system in case of downtime. | 🌐 |
| Ability to create persistent status messages that update automatically. (/live_status) | 🌐 |
| Deployment workflow on Oracle VPS. | |
| Filter for notifs. | ✅ |
- 🌐 -> In production
- ✅ -> Done

8
package-lock.json generated
View File

@@ -11,7 +11,7 @@
"dependencies": {
"@types/ping": "^0.4.4",
"cron": "^4.3.3",
"discord.js": "^14.24.1",
"discord.js": "^14.24.2",
"dotenv": "^17.2.2",
"pg": "^8.16.3",
"ping": "^1.0.0",
@@ -615,9 +615,9 @@
]
},
"node_modules/discord.js": {
"version": "14.24.1",
"resolved": "https://registry.npmjs.org/discord.js/-/discord.js-14.24.1.tgz",
"integrity": "sha512-LzL+MTGxB9mBwD8FjvkMwcIL4UtgG04e713U3+euqPCvOphhoVEoPpUNTvBPw4iJOas2uiuuh3JcveYSxIn8Tg==",
"version": "14.24.2",
"resolved": "https://registry.npmjs.org/discord.js/-/discord.js-14.24.2.tgz",
"integrity": "sha512-VMEDbmguRdX/EeMaTsf9Mb0IQA90WdYF2cn4QDfslQFXgQ6LFtmlPn0FSotnS0kcFbFp+JBSIxtnF+bnAHG/hQ==",
"license": "Apache-2.0",
"dependencies": {
"@discordjs/builders": "^1.13.0",

View File

@@ -30,7 +30,7 @@
"dependencies": {
"@types/ping": "^0.4.4",
"cron": "^4.3.3",
"discord.js": "^14.24.1",
"discord.js": "^14.24.2",
"dotenv": "^17.2.2",
"pg": "^8.16.3",
"ping": "^1.0.0",

View File

@@ -0,0 +1,55 @@
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 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;
await userRepo.save(follow);
await interaction.reply({content: `✅ Notification successfully ${follow.enable ? 'enabled 🔔' : 'disabled 🔕'} for ${realHost[0]?.name}!`, flags: [MessageFlags.Ephemeral]});
if(follow.enable) {
await interaction.user.send({content: `🔔 Notifications have been successfully enabled for ${realHost[0]?.name} ! To disable: /follow host:${realHost[0]?.name}`})
}
}
}
}
export default cmd;

View File

@@ -0,0 +1,75 @@
import { ApplicationIntegrationType, ChannelType, ContainerBuilder, MessageFlags, SlashCommandBuilder } from "discord.js";
import { CommandDefinition } from "../../type";
import statusService from "../../services/status.service";
import { AppDataSource } from "../../data-source";
import { Guild } from "../../entity/guild.entity";
const cmd : CommandDefinition = {
data: new SlashCommandBuilder()
.setName('live_status')
.setDescription('Generate a permanent status message that updates every 2 minutes.')
.addChannelOption((option) => option
.setName('channel')
.setDescription('The message will be generated')
.addChannelTypes(ChannelType.GuildText)
.setRequired(true)
)
.setIntegrationTypes(
ApplicationIntegrationType.GuildInstall
),
async execute(interaction) {
await interaction.deferReply({flags: [MessageFlags.Ephemeral]});
const channel_options = await interaction.options.getChannel("channel");
if(channel_options && interaction.guildId){
const channel = await interaction.guild?.channels.fetch(channel_options?.id);
if(channel?.isSendable()) {
const message = await channel.send({components: [statusService.getUpdatedContainer(true)], flags: [MessageFlags.IsComponentsV2]});
try {
const guildRepo = AppDataSource.getRepository(Guild);
let guild = await guildRepo.findOne({where: {guild_id: interaction.guildId}});
if(guild) {
const messageId = guild.persistent_message_id;
const channelId = guild.persistent_message_channel_id;
try {
const beforeChannel = await interaction.guild?.channels.fetch(channelId);
if(beforeChannel && beforeChannel.isSendable()) {
try {
const beforeMessage = await beforeChannel.messages.fetch(messageId);
const container = new ContainerBuilder()
.addTextDisplayComponents((t) => t.setContent('This message is no longer valid!'));
beforeMessage.edit({components: [container]});
} catch (error) {}
}
} catch (error) {
}
}else{
guild = new Guild();
guild.guild_id = interaction.guildId;
}
guild.persistent_message_channel_id = channel.id;
guild.persistent_message_id = message.id;
await guildRepo.save(guild);
await interaction.editReply('Message successfully generated!')
} catch (error) {
interaction.editReply('An error has occured ! '+error);
}
}else{
interaction.editReply('The selected channel is invalid!');
}
}else{
interaction.editReply('The selected channel is invalid!');
}
},
}
export default cmd;

View File

@@ -1,4 +1,4 @@
import { ApplicationIntegrationType, ButtonInteraction, ButtonStyle, ChatInputCommandInteraction, CommandInteraction, ComponentType, ContainerBuilder, InteractionContextType, MessageFlags, SlashCommandBuilder } from "discord.js";
import { ApplicationIntegrationType, ButtonInteraction, ButtonStyle, ChatInputCommandInteraction, ContainerBuilder, InteractionContextType, MessageFlags, SlashCommandBuilder } from "discord.js";
import { CommandDefinition } from "../../type";
const cmd : CommandDefinition = {

View File

@@ -1,7 +1,6 @@
import { ApplicationIntegrationType, ChatInputCommandInteraction, CommandInteraction, EmbedBuilder, InteractionContextType, SlashCommandBuilder } from "discord.js";
import ping from "ping";
import { ApplicationIntegrationType, ChatInputCommandInteraction, InteractionContextType, MessageFlags, SlashCommandBuilder } from "discord.js";
import statusService from "../../services/status.service";
import { CommandDefinition } from "../../type";
import { CommandDefinition} from "../../type";
const cmd : CommandDefinition = {
data: new SlashCommandBuilder()
@@ -18,18 +17,7 @@ const cmd : CommandDefinition = {
),
async execute(interaction : ChatInputCommandInteraction) {
await interaction.deferReply();
const embed = new EmbedBuilder();
embed.setTitle('Status of protojx servers');
embed.setColor(0xffffff);
embed.setTimestamp(new Date());
embed.setThumbnail(interaction.client.user.avatarURL())
for(let host of statusService.hosts){
embed.addFields({name: host.name, value: host.alive ? `${process.env.EMOJI_STATUS_ONLINE} Online` : `${process.env.EMOJI_STATUS_OFFLINE} Offline`, inline: false});
}
await interaction.editReply({embeds: [embed]});
await interaction.editReply({components: [statusService.getUpdatedContainer()], flags: MessageFlags.IsComponentsV2});
}
}

View File

@@ -11,9 +11,9 @@ export const AppDataSource = new DataSource({
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
synchronize: process.env.NODE_ENV !== "production",
logging: process.env.DB_LOGGING === "true",
entities: ["./entity/**/*.js"],
migrations: ["./migration/**/*.js"],
subscribers: ["./subscriber/**/*.js"],
entities: [__dirname + '/**/*.entity.js'],
migrations: [__dirname + "/**/*.migration.js"],
subscribers: [__dirname + "/**/*.subscriber.js"],
})

View File

@@ -21,7 +21,7 @@ for (const folder of commandFolders) {
if (!fs.statSync(commandsPath).isDirectory()) continue;
const commandFiles = fs.readdirSync(commandsPath).filter(file =>
file.endsWith('.js')
file.endsWith('.command.js')
);
for (const file of commandFiles) {

View File

@@ -0,0 +1,16 @@
import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";
@Entity({name: 'follows'})
export class Follow {
@PrimaryGeneratedColumn()
id: number;
@Column()
user_discord: string;
@Column()
host: string;
@Column({default: false})
enable: boolean;
}

View File

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

View File

@@ -0,0 +1,16 @@
import { Column, CreateDateColumn, Entity, PrimaryGeneratedColumn } from "typeorm";
@Entity({name: 'hosts_logs'})
export class HostsLog {
@PrimaryGeneratedColumn()
id: number;
@Column()
host: string;
@Column()
status: boolean;
@CreateDateColumn()
created_at: Date;
}

View File

@@ -1,4 +1,4 @@
import { ButtonInteraction, ChatInputCommandInteraction, Client, Collection, Events, GatewayIntentBits, MessageFlags, SlashCommandBuilder } from "discord.js";
import { Client, Collection, Events, GatewayIntentBits, MessageFlags } from "discord.js";
import { configDotenv } from "dotenv";
import path from "path";
import fs from "fs";

View File

@@ -1,79 +1,156 @@
import ping from "ping";
import * as cron from 'cron';
import { ActivityType, Client } from "discord.js";
import { Host } from "../type";
import { ActivityType, Client, ContainerBuilder, MessageFlags } from "discord.js";
import { Host, InfraType } from "../type";
import { AppDataSource } from "../data-source";
import { HostsLog } from "../entity/hostslog.entity";
import { Repository } from "typeorm";
import { Follow } from "../entity/follow.entity";
import { Guild } from "../entity/guild.entity";
type Nofity = {time: Date, name : string, alive : boolean, type : InfraType, host: string};
export class StatusService {
public hosts : Host[] = [
public hosts: Host[] = [
{
'host': 'https://protojx.com',
'name': 'Protojx Website 🌐',
'name': 'Protojx Website',
alive: false,
type: 'website'
ping_type: 'website',
type: 'website',
notify: false
},
{
'host': 'https://manager.protojx.com',
'name': 'Espace Client 💻',
'name': 'Espace Client',
alive: false,
type: 'website'
ping_type: 'website',
type: 'website',
notify: false
},
{
host: '5.178.99.4',
name: 'RYZEN 01 🖥️',
name: 'RYZEN 01',
alive: false,
type: 'ping'
ping_type: 'ping',
type: 'ryzen',
notify: true
},
{
host: '5.178.99.6',
name: 'RYZEN 02 🖥️',
name: 'RYZEN 02',
alive: false,
type: 'ping'
ping_type: 'ping',
type: 'ryzen',
notify: true
},
{
host: '5.178.99.5',
name: 'RYZEN 03 🖥️',
name: 'RYZEN 03',
alive: false,
type: 'ping'
ping_type: 'ping',
type: 'ryzen',
notify: true
},
{
host: '154.16.254.10',
name: 'RYZEN7 04',
alive: false,
ping_type: 'ping',
type: 'ryzen',
notify: true
},
{
host: '5.178.99.177',
name: 'XEON 01 (2697A V4) 🖥️',
name: 'XEON 01',
alive: false,
type: 'ping'
ping_type: 'ping',
type: 'xeon',
notify: true
},
{
host: '5.178.99.248',
name: 'XEON 02 (2687W V4) 🖥️',
host: '154.16.254.45',
name: 'XEON 02',
alive: false,
type: 'ping'
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 👾',
name: 'RYZEN-GAME 01',
alive: false,
type: 'ping'
ping_type: 'ping',
type: 'games',
notify: true
},
{
host: '5.178.99.63',
name: 'XEON-GAME 01 👾',
name: 'XEON-GAME 01',
alive: false,
type: 'ping'
ping_type: 'ping',
type: 'games',
notify: true
}
]
];
private client: Client | null = null;
private hostsLogRepo: Repository<HostsLog>;
private followRepo: Repository<Follow>;
private guildRepo: Repository<Guild>;
private client : Client|null = null;
constructor() {
this.hostsLogRepo = AppDataSource.getRepository(HostsLog);
this.followRepo = AppDataSource.getRepository(Follow);
this.guildRepo = AppDataSource.getRepository(Guild);
setTimeout(async () => {
await this.fetch()
this.updateClientStatus();
}, 3000);
const cronJob = new cron.CronJob('*/2 * * * *', async () => {
// ? cleanup logs
const oneMonthAgo = new Date();
oneMonthAgo.setMonth(oneMonthAgo.getMonth() - 1);
this.hostsLogRepo.
createQueryBuilder()
.delete()
.from(HostsLog)
.where("created_at < :date", { date: oneMonthAgo })
.execute();
// ? Get status
try {
await this.fetch();
await this.updateClientStatus();
// ? Message Live
const guilds = await this.guildRepo.find();
guilds.forEach(async (gdb) => {
if(this.client) {
try {
const guild = await this.client.guilds.fetch(gdb.guild_id);
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: [this.getUpdatedContainer(true)]});
}
} catch (error) {}
}
});
console.log('Status check completed at:', new Date().toISOString());
} catch (error) {
console.error('Error during status check:', error);
@@ -81,57 +158,155 @@ export class StatusService {
});
cronJob.start();
console.log('Status monitoring started - checking every 2 minutes');
}
}
setClient(client : Client) {
setClient(client: Client) {
this.client = client;
this.client.user?.setActivity({name: '💭 Server load and status...'})
this.client.user?.setActivity({ name: '💭 Server load and status...' })
}
private async updateClientStatus() {
if(this.client) {
if (this.client) {
const hosts = this.hosts.length;
const hostsAlive = this.hosts.filter((h) => h.alive).length;
this.client.user?.setActivity({name: (
hosts == hostsAlive ? '✅ All services are online.' : `📛 ${hosts - hostsAlive} service${hosts - hostsAlive > 1 ? 's' : ''} offline.`
), type: ActivityType.Watching});
this.client.user?.setActivity({
name: (
hosts == hostsAlive ? '✅ All services are online.' : `📛 ${hosts - hostsAlive} service${hosts - hostsAlive > 1 ? 's' : ''} offline.`
), type: ActivityType.Watching
});
}
}
private async fetch(max = 1): Promise<Host[]> {
const hosts = this.hosts.filter((value, index) => index < max * 10 && index >= (max - 1) * 10);
async function fetchAlive(host: Host) {
if(host.type === 'ping'){
let res = await ping.promise.probe(host.host, {timeout: 3});
host.alive = res.alive;
}else if(host.type === 'website'){
try {
const response = await fetch(host.host, { method: 'HEAD', signal: AbortSignal.timeout(3000) });
host.alive = response.ok;
} catch (error) {
host.alive = false;
}
private async fetchAlive(host: Host, notifs : Nofity[]) {
const latestLog = await this.hostsLogRepo.findOne({ where: { host: host.host }, 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') {
try {
const response = await fetch(host.host, { method: 'HEAD', signal: AbortSignal.timeout(10000) });
host.alive = response.ok;
} catch (error) {
host.alive = false;
}
return host;
}
const fetchPromises = hosts.map(host => fetchAlive(host));
// ? Notification System :
if (!latestLog || latestLog.status != host.alive) {
const log = new HostsLog();
log.host = host.host;
log.status = host.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});
}
}
return host;
}
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 fetchPromises = hosts.map(host => this.fetchAlive(host, notifs));
const updatedHosts = await Promise.all(fetchPromises);
updatedHosts.forEach((updatedHost, index) => {
const originalIndex = (max - 1) * 10 + index;
const originalIndex = (max - 1) * max_ping + index;
if (originalIndex < this.hosts.length) {
this.hosts[originalIndex] = updatedHost;
}
});
if(this.hosts.length > max * 10) {
await this.fetch(max + 1);
}
if (this.hosts.length > max * max_ping) {
await this.fetch(max + 1, notifs);
}else if(notifs.length > 0){
// ? Notification System (part 2 !):
const container = new ContainerBuilder()
.addTextDisplayComponents((t) => t.setContent(`### 🔔 Status change alert`));
notifs.map(async (n) => {
container.addSeparatorComponents((s)=>s);
container.addTextDisplayComponents((text) => text.setContent(`${n.alive ? process.env.EMOJI_STATUS_ONLINE : process.env.EMOJI_STATUS_OFFLINE} **${n.name}** is now **${n.alive ? 'online' : 'offline'}**\n🏷 Type : ${n.type}\n🕒 Time : <t:${Math.round(new Date().getTime()/1000)}:R>`));
});
return this.hosts;
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) => {
if(!users_ids.includes(user.user_discord)) {
users_ids.push(user.user_discord)
try {
const userdc = await this.client?.users.fetch(user.user_discord);
if(userdc) {
userdc.send({components: [container], flags: [MessageFlags.IsComponentsV2]})
}
} catch (error) {}
}
});
}
}
public getUpdatedContainer(live : boolean = false): ContainerBuilder {
const hostTexts = this.hosts.map((s) => {
return { type: s.type, value: `- ${s.name} : ${s.alive ? `${process.env.EMOJI_STATUS_ONLINE} Online` : `${process.env.EMOJI_STATUS_OFFLINE} Offline`}` };
});
const container = new ContainerBuilder()
.setAccentColor(0x0000ed)
.addTextDisplayComponents((text) => text.setContent('# Status of Protojx services'+(live ? ' (live)' : '')));
const sections: { title: string, type: InfraType, thumbnail: string }[] = [
{
title: 'Websites',
type: 'website',
thumbnail: 'https://protojx.com/assets/img/home2/agent.png'
},
{
title: 'Ryzens',
type: 'ryzen',
thumbnail: 'https://iconape.com/wp-content/png_logo_vector/ryzen.png'
},
{
title: 'Xeons',
type: 'xeon',
thumbnail: 'https://upload.wikimedia.org/wikipedia/commons/3/31/Intel-Xeon-Badge-2024.jpg'
},
{
title: 'Games',
type: 'games',
thumbnail: 'https://protojx.com/assets/img/hero-img.png'
}
]
sections.map((sectionData) => {
container.addSeparatorComponents((s) => s)
container.addSectionComponents(
(section) =>
section.addTextDisplayComponents(
(text) =>
text.setContent('## ' + sectionData.title + '\n' + hostTexts.filter((v) => v.type == sectionData.type).map((v) => v.value).join('\n'))
)
.setThumbnailAccessory(
(acc) =>
acc.setURL(sectionData.thumbnail)
)
)
});
const now = new Date();
container.addTextDisplayComponents((text) => text.setContent(`${live ? 'Last update : ' : ''}${now.getDate()}-${now.getMonth() + 1}-${now.getFullYear()} ${(now.getHours() + '').padStart(2, "0")}:${(now.getMinutes() + '').padStart(2, "0")} - Receive automatic notifications when there is an outage with /follow !`));
return container;
}
}

14
src/type.d.ts vendored
View File

@@ -1,2 +1,12 @@
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}[]};
import { ButtonInteraction, ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js";
export type InfraType = 'website' | 'ryzen' | 'xeon' | 'games';
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}[]};

View File

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