🚀 v0.1.0-alpha – Protection System, Anti-Raid & Core Stability (#39)

* chore(branch/dev): now have a dev branch to test my update

* feat(logs/disable): adding the options to disable the logs
- Adding the option to the logs command

* feat(logs/disable): adding the options to disable the logs
- Adding the option to the logs command

* feat(logs/disable): finishing the disable command for logs
- suppressing all the data stored in database (concerning the logs)
- suppressing all the channels on the logs categories

* feat(logs/bot): Sending a message when ready to all the buyer
- The message is send just the first time after it's just an edit
- The message is an embed

* style(logs/bot): embed for ready updated
- Color is now turned to green (no color saved in database for all the
  bot)

* feat(logs/bot): Sending a message when ready to all the buyer
- The message is send just the first time after it's just an edit
- The message is an embed

* style(logs/bot): embed for ready updated
- Color is now turned to green (no color saved in database for all the
  bot)

* style(ready/msg): now embed use timestamp to give the last restart

* style(ready/msg): norming stuff to be coherent with the code

- Give the backline like the other option

* feat(logs/collect): The collector is now timed out

- If the user pass more than 1 minutes to select the roles the message
will be updated and the collector disable.

* feat(logs/guildCreate): now joining a server is sending a message

- All the buyer will recieve a message when the bot is added to a new
guild.
- The information give by the message is invitation / member count /
owner id

* feat(logs/guildDelete): adding the private message when leaving a server

- An embed with the owner id / guild name / member count

* 🧪 Alpha v0.0.3 – Buyer Logs, Guild Events & UX Polish

💌 Buyer Notifications – Now Real-Time
	•	🟢 On Bot Ready
→ Sends a DM to all buyers when the bot boots up.
→ Reuses the same message for future startups by editing the last one.
→ Includes a timestamp ⏱️ to track the latest restart.
→ Fresh new green embed style for clarity  On Guild Join
→ Buyers are notified when the bot joins a new server.
→ Embed includes:
	•	Server name & ID
	•	Member count 👥
	•	Owner ID 👑
	•	Instant invite link 🔗 On Guild Leave
→ Buyers also receive a DM when the bot is removed from a server.
→ Embed includes:
	•	Guild name
	•	Owner ID
	•	Member count at time of removal

⸻

🔐 Interaction UX Improvements
	•	🕒 Timed Role Collector for /logs
→ The role selection menu now times out after 1 minute.
→ Message is updated to reflect the timeout, and the collector is disabled to avoid confusion.

⸻

🎨 Style & Code Coherence
	•	 Ready embeds now use consistent layout (backline spacing, timestamps).
	•	🎨 Unified color scheme: green embeds for success/startup (no per-bot color in DB yet).
	•	🧼 Normalization pass for embed construction and messaging consistency.

⸻

📌 This alpha lays the foundation for clean bot lifecycle monitoring, robust buyer communication, and better UI behavior for commands.

Next in the pipe? Maybe logging settings persistence or webhook dispatches 👀

* feat(moderation/nuke): adding the command to nuke channel textual

- The command have to goal to delete and recreate a clone of the channel

* fix(moderation/nuke): selecting the people who can nuke channels

- Now only people with the whitelisted status (or more) can renew / nuke
the channel

* fix(moderation/nuke): selecting the people who can nuke channels

- Now only people with the whitelisted status (or more) can renew / nuke
the channel

feat(logs/guildUpdate): now guildUpdate event is catch and send a message to guildlogs

- logging the diff between:
  - premiumTier
  - contentFilter
  - locale
  - name
  - afk

* fix(moderation/nuke): selecting the people who can nuke channels

- Now only people with the whitelisted status (or more) can renew / nuke
the channel

feat(logs/guildUpdate): now guildUpdate event is catch and send a message to guildlogs

- logging the diff between:
  - premiumTier
  - contentFilter
  - locale
  - name
  - afk

* fix(logs/guildCreate): fixing typo on the final console log

- The console logs was pasted from the index.ts and now is updating to
use the interaction.guild instead the counter of the database

* style(logs/guildUpdate): updating the messages instead using integer

- now the verification level will send the same message wrote in the
rules
- the explicit content level will send a message instead just a single
index

* refractor(logs/guildUpdate): adding the subfolder 'guild' in event

* refractor(admin/deletecat): adding the deleteCategories in administration part

* refractor(event/guildUpdate): typing the function parameters

- Adding the update for guild update event, (oldGuild / newGuild)

* refactor(event/guildDelete): typing the parameter of the function

- Typing the execute function of the event guildDelete (in user cat)

* refactor(event/interaction): adding the type on interaction create

* feat(lib/perm): now isWhitelisted can be import on commands

- The command is exportable including the lib/perm.ts. Starting to have
really utils (and so function) on this project

* refactor(client/ready): eslint is now respecting

- The eslint is now respect.

* docs(utils/perm): Adding documentation for utils

- Doxygen will be now required for all the utils

* core(bun/package): update all the package to latest version

- There is now a warning when running the bot cause by an electron
update waiting to discord.js

* refactor(event/ready): format following the eslint

- The code is now setup like the eslint is configurated

* style(event/guildDelete): adding footer to delete guild

- The footer by default for the server is now added when suppressed from
the server

* refactor(commands): starting to adding type on the command

- Now with the lsp configurated is easier to see the type error
(interger instead number) / Guild -> GuildPrisma

* fix(moderation/nuke): correcting the type guildText

- The type was not correct

* fix(client/guildCreate): typing many thing to respect the Type

- The type was not correct

* fix(client/guildUpdate): typing many thing to respect the Type

- The type was not correct

* fix(client/interactino): typing many thing to respect the Type

- The type was not correct

* fix(lib/permission): typing many thing to respect the Type

- The type was not correct

* feat(event/messages): adding the MessageDelete logs

* core: updating packages

* fix(lib/perm): correcting the type integer to number

* fix(internal/deploycommand): adding type to command* variable

* feat(events/messagesDelete): adding type and now sending the log message

* feat(events/messagesBulkDelete): adding the event management

* fix(event/client): adding types on guildCreate

- Adding prisma types

* fix(event/client): adding types on guildDelete

- Adding prisma types

* fix(cmd/administration): adding types on deletecat

- Adding prisma types

* fix(command/rank): adding type on whitelist

* fix(command/rank): import CommandInteraction on whiltelist

* feat(event/messages): adding the MessageCreate event

- No logging stuff but a level system from scratch

* style(event/message): adding the pipe like in other messages.

* style(action/bun): removing the space

* feat(flake/tmux): adding tmux project configuration

- Adding the tmux-setup command how load my own tmux configuration to
work on this project

* fix(flake/tmux): patching the zplug issues when loading to many time

- just adding a exec zsh

* feat(event/messages): adding title on MessageBulkDelete

* feat(event/messages): adding title on MessageDelete

* feat(event/messages): adding logs for MessageUpdate

* fix(event/messages): Fixing the prisma call

- Using the old variable name message become oldMessage / newMessage

* style(event/messages): Adding Italic style if content cannot be load

* feat(cmd/admin): adding protect command

* feat(events/channel): adding channelCreate event

- The event contains:
  - logs for user whitelisted / Owner / Buyer
  - Antiraid for others

* feat(events/channel): adding channelDelete event

- The event contains:
  - logs for user whitelisted / Owner / Buyer
  - Antiraid for others

* feat(events/channel): adding channeUpdate event

- The event contains:
  - logs for users

* feat(src/lib): adding the a function to choose the correction mention
This commit is contained in:
Raphaël 2025-10-01 14:25:03 +02:00 committed by GitHub
parent 13cb14cba3
commit 610e5ea946
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
26 changed files with 1156 additions and 92 deletions

View file

@ -1,6 +1,7 @@
import { MessageFlags, ChannelType, SlashCommandBuilder } from 'discord.js';
import { MessageFlags, ChannelType, SlashCommandBuilder, CommandInteraction } from 'discord.js';
import emoji from '../../../assets/emoji.json' assert { type: 'json' };
import { prisma } from '../../lib/prisma.ts';
import { User as UserPrisma } from '@prisma/client';
export default {
data: new SlashCommandBuilder()
@ -14,7 +15,7 @@ export default {
.addChannelTypes(ChannelType.GuildCategory),
),
async execute(interaction: CommandInteraction) {
let userData: User;
let userData: UserPrisma;
try {
userData = await prisma.user.findUnique({
where: {
@ -40,7 +41,7 @@ export default {
});
return;
}
const category: GuildCategory = interaction.options.getChannel(
const category: ChannelType.GuildCategory = interaction.options.getChannel(
'category',
true,
);

View file

@ -10,8 +10,8 @@ import {
StringSelectMenuOptionBuilder,
SlashCommandBuilder,
MessageFlags,
SlashCommandBuilder,
EmbedBuilder,
CommandInteraction,
} from 'discord.js';
import emoji from '../../../assets/emoji.json' assert { type: 'json' };
@ -72,11 +72,6 @@ export default {
}
catch (err) {
throw `\t⚠ | Cannot get the database connection!\n\t\t(${err}).`;
await interaction.reply({
content: `${emoji.answer.error} | Cannot connect to the database`,
flags: MessageFlags.Ephemeral,
});
return;
}
const choice: string = interaction.options.getString('action');
switch (choice) {
@ -150,7 +145,7 @@ export default {
),
);
const roleSelection =
const roleSelection: ActionRowBuilder<StringSelectMenuBuilder> =
new ActionRowBuilder<StringSelectMenuBuilder>().addComponents(menu);
const permSelector: EmbedBuilder = new EmbedBuilder()
@ -170,6 +165,15 @@ export default {
time: 60_000,
max: 25,
});
collector.on('end', async (collected) => {
if (collected.size === 0) {
await interaction.editReply({
content: '⏰ | Too many time to select roles allowed to see the logs',
embeds: [],
components: [],
});
}
});
collector.on(
'collect',
async (selectInteraction: StringSelectMenuInteraction) => {

View file

@ -0,0 +1,184 @@
import {
SlashCommandBuilder,
ActionRowBuilder,
StringSelectMenuBuilder,
ButtonBuilder,
ButtonStyle,
EmbedBuilder,
ComponentType,
ChatInputCommandInteraction,
MessageFlags,
} from 'discord.js';
import { prisma } from '../../lib/prisma';
import { Guild as GuildPrisma } from '@prisma/client';
const modules = {
'anti-channel': 'Block channel creation/deletion',
'anti-rank': 'Block dangerous rank modifications',
'anti-perm': 'Block dangerous permissions on roles',
'anti-massban': 'Prevent mass bans',
'anti-mass-mention': 'Prevent mass mentions',
'anti-bot': 'Prevent unauthorized bots',
};
function camel(str: string): string {
return str
.split('-')
.map((s) => s.charAt(0).toUpperCase() + s.slice(1))
.join('');
}
function getEmbed(guildData: GuildPrisma): EmbedBuilder {
let description: string = '\n';
for (const [key, label] of Object.entries(modules)) {
const field = `protect${camel(key)}`;
const enabled = guildData[field as keyof typeof guildData] as boolean;
description += `- **${label}**: ${enabled ? '✅' : '❌'}\n`;
}
const baseEmbed = new EmbedBuilder()
.setTitle('🛡️ | Protection Manager')
.setDescription(description)
.setFooter({
text: guildData.footer,
})
.setColor(guildData.color);
return baseEmbed;
}
function getButton(selected: string, active: boolean): ActionRowBuilder<ButtonBuilder> {
const button = new ActionRowBuilder<ButtonBuilder>().addComponents(
new ButtonBuilder()
.setCustomId(`enable_${selected}`)
.setLabel('Enable')
.setEmoji('✅')
.setDisabled(!active)
.setStyle(ButtonStyle.Success),
new ButtonBuilder()
.setCustomId(`disable_${selected}`)
.setLabel('Disable')
.setEmoji('❌')
.setDisabled(!active)
.setStyle(ButtonStyle.Danger),
new ButtonBuilder()
.setCustomId('return')
.setLabel('Return')
.setEmoji('↩️')
.setStyle(ButtonStyle.Secondary),
);
return button;
}
export default {
data: new SlashCommandBuilder()
.setName('protect')
.setDescription('Manage guild protections interactively'),
async execute(interaction: ChatInputCommandInteraction) {
const guildId: string | null = interaction.guildId!;
let guildData: GuildPrisma = await prisma.guild.findUnique({
where: {
id: guildId,
},
});
if (!guildData) {
guildData = await prisma.guild.create({
data: {
id: guildId,
},
});
}
const menu: StringSelectMenuBuilder = new StringSelectMenuBuilder()
.setCustomId('select_protect')
.setPlaceholder('Select a module')
.addOptions(
Object.entries(modules).map(([value, label]) => ({
label,
value,
})),
);
const msg = await interaction.reply({
embeds: [getEmbed(guildData)],
components: [new ActionRowBuilder<StringSelectMenuBuilder>().addComponents(menu)],
flags: MessageFlags.Ephemeral,
});
const collector = msg.createMessageComponentCollector({
componentType: ComponentType.StringSelect,
time: 5 * 60 * 1000,
});
collector.on('collect', async (selectInteraction) => {
if (selectInteraction.user.id !== interaction.user.id) {
return selectInteraction.reply({
content: '❌ You cannot use this menu.',
flags: MessageFlags.Ephemeral,
});
}
const selected: string = selectInteraction.values[0] as keyof typeof modules;
const enabled = guildData![`protect${camel(selected)}`];
const moduleEmbed = new EmbedBuilder()
.setTitle(`⚙️ | Manage ${modules[selected]}`)
.setFooter({
text: guildData.footer,
})
.setDescription(
`This module is currently: **${enabled ? 'Enabled ✅' : 'Disabled ❌'}**`,
)
.setColor(enabled ? '#00ff00' : '#ff0000');
await selectInteraction.update({
embeds: [moduleEmbed],
components: [getButton(selected, true)],
});
});
const buttonCollector = msg.createMessageComponentCollector({
componentType: ComponentType.Button,
time: 5 * 60 * 1000,
});
buttonCollector.on('collect', async (btnInteraction) => {
if (btnInteraction.user.id !== interaction.user.id) {
return btnInteraction.reply({
content: '❌ | You cannot use these buttons.',
flags: MessageFlags.Ephemeral,
});
}
if (btnInteraction.customId === 'return') {
return btnInteraction.update({
embeds: [getEmbed(guildData)],
components: [
new ActionRowBuilder<StringSelectMenuBuilder>().addComponents(menu),
],
});
}
const [action, moduleName] = btnInteraction.customId.split('_');
const field = `protect${camel(moduleName)}`;
await prisma.guild.update({
where: {
id: guildId,
},
data: {
[field]: action === 'enable',
},
});
guildData = await prisma.guild.findUnique({
where: {
id: guildId,
},
});
await btnInteraction.update({
embeds: [
new EmbedBuilder()
.setTitle(`⚙️ | Manage ${modules[moduleName as keyof typeof modules]}`)
.setFooter({
text: guildData.footer,
})
.setDescription(
`This module is now: **${
action === 'enable' ? '✅ Enabled' : '❌ Disabled'
}**`,
)
.setColor(action === 'enable' ? '#00ff00' : '#ff0000'),
],
components: [getButton(null, false)],
});
});
},
};

View file

@ -4,8 +4,10 @@ import {
PresenceUpdateStatus,
MessageFlags,
SlashCommandBuilder,
CommandInteraction,
} from 'discord.js';
import emoji from '../../../assets/emoji.json' assert { type: 'json' };
import { User as UserPrisma } from '@prisma/client';
export default {
data: new SlashCommandBuilder()
@ -108,7 +110,7 @@ export default {
),
),
async execute(interaction: CommandInteraction) {
let userData: User;
let userData: UserPrisma | null;
try {
userData = await prisma.user.findUnique({
where: {

View file

@ -0,0 +1,44 @@
import { MessageFlags, SlashCommandBuilder, ChannelType, CommandInteraction } from 'discord.js';
import emoji from '../../../assets/emoji.json' assert { type: 'json' };
import { isWhitelisted } from '../../lib/perm.ts';
export default {
data: new SlashCommandBuilder()
.setName('nuke')
.setDescription('Allow to delete and recreate a channel')
.addChannelOption((opt) =>
opt
.setName('channel')
.setDescription('Choose the channel you want to renew')
.addChannelTypes(ChannelType.GuildText),
),
async execute(interaction: CommandInteraction) {
if (!(await isWhitelisted(interaction.user.id, interaction.guild.id))) {
interaction.reply({
content: `${emoji.answer.no} | You're not whitelisted on this server`,
flags: MessageFlags.Ephemeral,
});
return;
}
const oldChannel : ChannelType.GuildText = interaction.options.getChannel(
'channel',
) || interaction.channel;
const pos: number = oldChannel.position;
oldChannel.clone().then((newchannel) => {
newchannel.setPosition(pos);
interaction.client.channels.fetch(newchannel.id).then(
channel => channel.send({
content: `${emoji.answer.yes} | ${newchannel} has been nuked by \`${interaction.user.username}\``,
ephermal: true,
}),
);
try {
oldChannel.delete();
}
catch (err) {
console.error(`⚠️ | Error when suppressing the channel\n\t${err}`);
}
});
},
};

View file

@ -1,6 +1,8 @@
import { EmbedBuilder, MessageFlags, SlashCommandBuilder } from 'discord.js';
import { CommandInteraction, EmbedBuilder, MessageFlags, SlashCommandBuilder } from 'discord.js';
import { prisma } from '../../lib/prisma.ts';
import emoji from '../../../assets/emoji.json' assert { type: 'json' };
import { User as UserPrisma } from '@prisma/client';
import { Guild as GuildPrisma } from '@prisma/client';
export default {
data: new SlashCommandBuilder()
@ -33,7 +35,7 @@ export default {
),
async execute(interaction: CommandInteraction) {
const subcommand = interaction.options.getSubcommand();
let userData: User;
let userData: UserPrisma;
try {
userData = await prisma.user.findUnique({
where: {
@ -51,7 +53,7 @@ export default {
});
return;
}
let guildData: Guild;
let guildData: GuildPrisma;
try {
guildData = await prisma.guild.findUnique({
where: {