diff --git a/README.md b/README.md index d94fc0f..d6892a3 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ 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. | ➖ | +| Ability to create persistent status messages that update automatically. (/live_status) | ✅ | | Deployment workflow on Raspberry Pi. | ➖ | - 🌐 -> In production diff --git a/src/commands/utility/live_status.command.ts b/src/commands/utility/live_status.command.ts new file mode 100644 index 0000000..7861ec1 --- /dev/null +++ b/src/commands/utility/live_status.command.ts @@ -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; \ No newline at end of file diff --git a/src/deploy-commands.ts b/src/deploy-commands.ts index f1344af..ced451e 100644 --- a/src/deploy-commands.ts +++ b/src/deploy-commands.ts @@ -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) { diff --git a/src/entity/guild.entity.ts b/src/entity/guild.entity.ts index 0de662e..b28b763 100644 --- a/src/entity/guild.entity.ts +++ b/src/entity/guild.entity.ts @@ -10,4 +10,7 @@ export class Guild { @Column() persistent_message_id: string; + + @Column() + persistent_message_channel_id: string; } \ No newline at end of file diff --git a/src/services/status.service.ts b/src/services/status.service.ts index 200c224..f55eb52 100644 --- a/src/services/status.service.ts +++ b/src/services/status.service.ts @@ -6,6 +6,7 @@ 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}; @@ -97,11 +98,13 @@ export class StatusService { private client: Client | null = null; private hostsLogRepo: Repository; private followRepo: Repository; + private guildRepo: Repository; constructor() { this.hostsLogRepo = AppDataSource.getRepository(HostsLog); this.followRepo = AppDataSource.getRepository(Follow); + this.guildRepo = AppDataSource.getRepository(Guild); setTimeout(async () => { await this.fetch() @@ -123,6 +126,23 @@ export class StatusService { 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); @@ -223,14 +243,14 @@ export class StatusService { } } - public getUpdatedContainer(): ContainerBuilder { + 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')); + .addTextDisplayComponents((text) => text.setContent('# Status of protojx services'+(live ? ' (live)' : ''))); const sections: { title: string, type: InfraType, thumbnail: string }[] = [ { @@ -271,7 +291,7 @@ export class StatusService { }); const now = new Date(); - container.addTextDisplayComponents((text) => text.setContent(`${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 !`)); + 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; } diff --git a/src/type.d.ts b/src/type.d.ts index 1d86eab..647af85 100644 --- a/src/type.d.ts +++ b/src/type.d.ts @@ -9,4 +9,4 @@ export type Host = { type: InfraType, notify: boolean; }; -export type CommandDefinition = { data: SlashCommandBuilder, execute: (interaction: ChatInputCommandInteraction) => void, buttons?: { id: string, handle: (interaction: ButtonInteraction) => void}[]}; \ No newline at end of file +export type CommandDefinition = { data: SlashCommandBuilder | SlashCommandOptionsOnlyBuilder, execute: (interaction: ChatInputCommandInteraction) => void, buttons?: { id: string, handle: (interaction: ButtonInteraction) => void}[]}; \ No newline at end of file