🚀 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:
parent
13cb14cba3
commit
610e5ea946
26 changed files with 1156 additions and 92 deletions
2
.github/workflows/bun.yml
vendored
2
.github/workflows/bun.yml
vendored
|
|
@ -6,7 +6,7 @@ on:
|
|||
- "**"
|
||||
pull_request:
|
||||
branches:
|
||||
- "**"
|
||||
- "**"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
|
|
|||
77
flake.nix
77
flake.nix
|
|
@ -1,33 +1,52 @@
|
|||
{
|
||||
description = "Flake de développement pour un bot Discord en TypeScript avec Bun";
|
||||
description = "Flake de développement pour un bot Discord en TypeScript avec Bun";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
};
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
};
|
||||
|
||||
outputs = { self, nixpkgs, flake-utils }:
|
||||
flake-utils.lib.eachDefaultSystem (system:
|
||||
let
|
||||
pkgs = import nixpkgs {
|
||||
inherit system;
|
||||
};
|
||||
in {
|
||||
devShells.default = pkgs.mkShell {
|
||||
name = "discord-bot-bun-ts";
|
||||
buildInputs = with pkgs; [
|
||||
bun
|
||||
git
|
||||
curl
|
||||
wget
|
||||
nodejs_latest
|
||||
mariadb
|
||||
];
|
||||
shellHook = ''
|
||||
export NIX_SHOW_STATS=0
|
||||
export NIX_HIDE_STATS=1
|
||||
printf "\n\033[0;90m Typescript + Bun env loaded for: \033[38;5;220m${system}\033[0m\n"
|
||||
'';
|
||||
};
|
||||
});
|
||||
outputs = { self, nixpkgs, flake-utils }:
|
||||
flake-utils.lib.eachDefaultSystem (system:
|
||||
let
|
||||
pkgs = import nixpkgs { inherit system; };
|
||||
tmux-setup = pkgs.writeShellScriptBin "tmux-setup" ''
|
||||
#!/usr/bin/env sh
|
||||
SESSION="TTY"
|
||||
DIR=$(git rev-parse --show-toplevel 2>/dev/null || pwd)
|
||||
if ! tmux has-session -t $SESSION 2>/dev/null; then
|
||||
tmux new-session -d -s $SESSION -c "$DIR" -n dev
|
||||
tmux send-keys -t $SESSION:0 'vim' C-m
|
||||
tmux split-window -h -p 30 -t $SESSION:0 -c "$DIR"
|
||||
tmux send-keys -t $SESSION:0.2 'exec zsh' C-m
|
||||
tmux split-window -v -p 30 -t $SESSION:0.1 -c "$DIR"
|
||||
tmux send-keys -t $SESSION:0.2 'watch -n0.5 bunx eslint ./src' C-m
|
||||
tmux split-window -v -p 50 -t $SESSION:0.2 -c "$DIR"
|
||||
tmux send-keys -t $SESSION:0.3 'bunx prisma studio' C-m
|
||||
tmux new-window -t $SESSION:1 -n git -c "$DIR"
|
||||
tmux send-keys -t $SESSION:1 'lazygit' C-m
|
||||
fi
|
||||
tmux select-window -t $SESSION:0
|
||||
tmux select-pane -t $SESSION:0.0
|
||||
tmux attach -t $SESSION
|
||||
'';
|
||||
in {
|
||||
devShells.default = pkgs.mkShell {
|
||||
name = "discord-bot-bun-ts";
|
||||
buildInputs = with pkgs; [
|
||||
bun
|
||||
git
|
||||
curl
|
||||
wget
|
||||
nodejs_latest
|
||||
mariadb
|
||||
tmux-setup
|
||||
];
|
||||
shellHook = ''
|
||||
export NIX_SHOW_STATS=0
|
||||
export NIX_HIDE_STATS=1
|
||||
printf "\n\033[0;90m Typescript + Bun env loaded for: \033[38;5;220m${system}\033[0m\n"
|
||||
'';
|
||||
};
|
||||
});
|
||||
}
|
||||
|
|
|
|||
14
package.json
14
package.json
|
|
@ -15,22 +15,22 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest",
|
||||
"eslint": "^9.33.0",
|
||||
"eslint": "^9.36.0",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-prettier": "^5.5.4",
|
||||
"husky": "^9.1.7",
|
||||
"lint-staged": "^16.1.5",
|
||||
"lint-staged": "^16.2.3",
|
||||
"prettier": "^3.6.2",
|
||||
"prisma": "^6.14.0",
|
||||
"typescript-eslint": "^8.39.1"
|
||||
"prisma": "^6.16.2",
|
||||
"typescript-eslint": "^8.45.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5.8.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@prisma/client": "^6.14.0",
|
||||
"discord.js": "^14.21.0",
|
||||
"dotenv": "^17.2.1",
|
||||
"@prisma/client": "^6.16.2",
|
||||
"discord.js": "^14.22.1",
|
||||
"dotenv": "^17.2.2",
|
||||
"mariadb": "^3.4.5"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,6 +46,14 @@ model Guild {
|
|||
logMsg String?
|
||||
logServer String?
|
||||
|
||||
protectEnabled Boolean @default(false)
|
||||
protectAntiChannel Boolean @default(false)
|
||||
protectAntiRank Boolean @default(false)
|
||||
protectAntiPerm Boolean @default(false)
|
||||
protectAntiMassban Boolean @default(false)
|
||||
protectAntiMassMention Boolean @default(false)
|
||||
protectAntiBot Boolean @default(false)
|
||||
|
||||
footer String @default("© EniumTeam ~ 2025")
|
||||
color String @default("#000000")
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
);
|
||||
|
|
@ -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) => {
|
||||
|
|
|
|||
184
src/commands/administration/protect.ts
Normal file
184
src/commands/administration/protect.ts
Normal 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)],
|
||||
});
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
|
|
@ -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: {
|
||||
|
|
|
|||
44
src/commands/moderation/nuke.ts
Normal file
44
src/commands/moderation/nuke.ts
Normal 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}`);
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
|
|
@ -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: {
|
||||
|
|
|
|||
75
src/events/channel/channelCreate.ts
Normal file
75
src/events/channel/channelCreate.ts
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
import { Events, AuditLogEvent, TextChannel, EmbedBuilder, Channel } from 'discord.js';
|
||||
import { prisma } from '../../lib/prisma';
|
||||
import { Guild as GuildPrisma } from '@prisma/client';
|
||||
import { isWhitelisted } from '../../lib/perm.ts';
|
||||
|
||||
export default {
|
||||
name: Events.ChannelCreate,
|
||||
async execute(channel: Channel) {
|
||||
if (!channel.guild) return;
|
||||
try {
|
||||
const auditLogs = await channel.guild.fetchAuditLogs({
|
||||
type: AuditLogEvent.ChannelCreate,
|
||||
limit: 1,
|
||||
});
|
||||
const entry = auditLogs.entries.first();
|
||||
if (!entry) return;
|
||||
const executor = entry.executor;
|
||||
if (!executor) return;
|
||||
const guildData: GuildPrisma = await prisma.guild.findUnique({
|
||||
where: {
|
||||
id: channel.guild.id,
|
||||
},
|
||||
});
|
||||
if (!guildData) return;
|
||||
if (!isWhitelisted(executor.id, channel.guild.id)) {
|
||||
await channel.delete(`Unauthorized channel creation by ${executor.tag}`);
|
||||
const member = await channel.guild.members.fetch(executor.id).catch(() => null);
|
||||
if (member) {
|
||||
const rolesToRemove = member.roles.cache.filter(r => r.id !== channel.guild.id);
|
||||
for (const [id] of rolesToRemove) {
|
||||
await member.roles.remove(id, 'Unauthorized channel creation [TTY AntiRaid]');
|
||||
}
|
||||
}
|
||||
if (guildData.logMod) {
|
||||
const logChannel = await channel.guild.channels.fetch(guildData.logMod).catch(() => null);
|
||||
if (logChannel?.isTextBased()) {
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle('⚠️ | Anti-Channel Protection')
|
||||
.setDescription(
|
||||
`**${channel.name}** created by <@${executor.id}> is now **deleted**.\n__Sanction:__ Unranked.`,
|
||||
)
|
||||
.setColor(guildData.color)
|
||||
.setTimestamp()
|
||||
.setFooter({
|
||||
text: guildData.footer,
|
||||
});
|
||||
(logChannel as TextChannel).send({
|
||||
embeds: [embed],
|
||||
});
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (guildData.logChannel) {
|
||||
const logChannel = await channel.guild.channels.fetch(guildData.logChannel).catch(() => null);
|
||||
if (logChannel?.isTextBased()) {
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle('📢 Channel Created')
|
||||
.setDescription(`Channel **${channel.name}** has been created by <@${executor.id}>.`)
|
||||
.setColor(guildData.color)
|
||||
.setTimestamp()
|
||||
.setFooter({
|
||||
text: guildData.footer,
|
||||
});
|
||||
(logChannel as TextChannel).send({
|
||||
embeds: [embed],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
console.error(`⚠️ | ChannelCreate protection error: ${err}`);
|
||||
}
|
||||
},
|
||||
};
|
||||
77
src/events/channel/channelDelete.ts
Normal file
77
src/events/channel/channelDelete.ts
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
import { Events, AuditLogEvent, TextChannel, EmbedBuilder, Channel } from 'discord.js';
|
||||
import { prisma } from '../../lib/prisma';
|
||||
import { Guild as GuildPrisma } from '@prisma/client';
|
||||
import { isWhitelisted } from '../../lib/perm.ts';
|
||||
|
||||
export default {
|
||||
name: Events.ChannelDelete,
|
||||
async execute(channel: Channel) {
|
||||
if (!channel.guild) return;
|
||||
try {
|
||||
const auditLogs = await channel.guild.fetchAuditLogs({
|
||||
type: AuditLogEvent.ChannelDelete,
|
||||
limit: 1,
|
||||
});
|
||||
const entry = auditLogs.entries.first();
|
||||
if (!entry) return;
|
||||
const executor = entry.executor;
|
||||
if (!executor) return;
|
||||
const guildData: GuildPrisma = await prisma.guild.findUnique({
|
||||
where: {
|
||||
id: channel.guild.id,
|
||||
},
|
||||
});
|
||||
if (!guildData) return;
|
||||
if (!(await isWhitelisted(executor.id, channel.guild.id))) {
|
||||
const member = await channel.guild.members.fetch(executor.id).catch(() => null);
|
||||
if (member) {
|
||||
const rolesToRemove = member.roles.cache.filter(r => r.id !== channel.guild.id);
|
||||
for (const [id] of rolesToRemove) {
|
||||
await member.roles.remove(id, 'Unauthorized channel deletion [TTY AntiRaid]');
|
||||
}
|
||||
}
|
||||
channel.clone().then((newchannel) => {
|
||||
newchannel.setPosition(channel.position);
|
||||
});
|
||||
if (guildData.logMod) {
|
||||
const logChannel = await channel.guild.channels.fetch(guildData.logMod).catch(() => null);
|
||||
if (logChannel instanceof TextChannel) {
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle('⚠️ | Anti-Channel Protection')
|
||||
.setDescription(
|
||||
`**${channel.name}** deleted by <@${executor.id}> is now **recreated**.\n__Sanction:__ Unranked.`,
|
||||
)
|
||||
.setColor(guildData.color)
|
||||
.setTimestamp()
|
||||
.setFooter({
|
||||
text: guildData.footer,
|
||||
});
|
||||
(logChannel as TextChannel).send({
|
||||
embeds: [embed],
|
||||
});
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (guildData.logChannels) {
|
||||
const logChannel = await channel.guild.channels.fetch(guildData.logChannel).catch(() => null);
|
||||
if (logChannel instanceof TextChannel) {
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle('🗑️ | Channel Deleted')
|
||||
.setDescription(`A channel was deleted by <@${executor.id}>.`)
|
||||
.setColor(guildData.color)
|
||||
.setTimestamp()
|
||||
.setFooter({
|
||||
text: guildData.footer,
|
||||
});
|
||||
(logChannel as TextChannel).send({
|
||||
embeds: [embed],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
console.error(`⚠️ | ChannelDelete protection error: ${err}`);
|
||||
}
|
||||
},
|
||||
};
|
||||
84
src/events/channel/channelUpdate.ts
Normal file
84
src/events/channel/channelUpdate.ts
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
import {
|
||||
Events,
|
||||
AuditLogEvent,
|
||||
TextChannel,
|
||||
EmbedBuilder,
|
||||
Channel,
|
||||
GuildChannel,
|
||||
} from 'discord.js';
|
||||
import { prisma } from '../../lib/prisma';
|
||||
import { Guild as GuildPrisma } from '@prisma/client';
|
||||
import { getCorrectMention } from '../../lib/mention';
|
||||
|
||||
export default {
|
||||
name: Events.ChannelUpdate,
|
||||
async execute(oldChannel: Channel, newChannel: Channel) {
|
||||
if (!newChannel.guild) return;
|
||||
try {
|
||||
const logs = await newChannel.guild.fetchAuditLogs({
|
||||
type: AuditLogEvent.ChannelUpdate | AuditLogEvent.ChannelOverwriteCreate | AuditLogEvent.ChannelOverwriteDelete | AuditLogEvent.ChannelOverwriteUpdate,
|
||||
limit: 5,
|
||||
});
|
||||
const entry = [...logs.entries.values()]
|
||||
.filter(e => (e.target as GuildChannel)?.id === newChannel.id)
|
||||
.sort((a, b) => b.createdTimestamp - a.createdTimestamp)[0];
|
||||
const executor = entry?.executor;
|
||||
const guildData: GuildPrisma | null = await prisma.guild.findUnique({
|
||||
where: { id: newChannel.guild.id },
|
||||
});
|
||||
if (!guildData) return;
|
||||
const changes: string[] = [];
|
||||
if (oldChannel.name !== newChannel.name) {
|
||||
changes.push(`**Name:** \`${oldChannel.name}\` → \`${newChannel.name}\``);
|
||||
}
|
||||
if ('topic' in oldChannel && 'topic' in newChannel) {
|
||||
if (oldChannel.topic !== newChannel.topic) {
|
||||
changes.push(
|
||||
`**Topic:** \`${oldChannel.topic ?? 'None'}\` → \`${newChannel.topic ?? 'None'}\``,
|
||||
);
|
||||
}
|
||||
}
|
||||
const oldPerms = oldChannel.permissionOverwrites.cache;
|
||||
const newPerms = newChannel.permissionOverwrites.cache;
|
||||
newPerms.forEach((overwrite, id) => {
|
||||
const old = oldPerms.get(id);
|
||||
if (!old) {
|
||||
changes.push(`New overwrite added for ${getCorrectMention(oldChannel.guild, id)}`);
|
||||
return;
|
||||
}
|
||||
if (
|
||||
overwrite.allow.bitfield !== old.allow.bitfield ||
|
||||
overwrite.deny.bitfield !== old.deny.bitfield
|
||||
) {
|
||||
changes.push(`Overwrite changed for <@&${id}> / <@${id}>`);
|
||||
}
|
||||
});
|
||||
oldPerms.forEach((overwrite, id) => {
|
||||
if (!newPerms.has(id)) {
|
||||
changes.push(`Overwrite removed for <@&${id}> / <@${id}>`);
|
||||
}
|
||||
});
|
||||
if (guildData.logChannels) {
|
||||
const logChannel = await newChannel.guild.channels
|
||||
.fetch(guildData.logChannels)
|
||||
.catch(() => null);
|
||||
if (logChannel instanceof TextChannel) {
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle('✏️ Channel Updated')
|
||||
.setDescription(
|
||||
`Channel **${newChannel.name}** ${
|
||||
executor ? `was updated by <@${executor.id}>` : 'was updated'
|
||||
}.\n\n${changes.join('\n') || 'No details'}`,
|
||||
)
|
||||
.setColor(guildData.color)
|
||||
.setTimestamp()
|
||||
.setFooter({ text: guildData.footer });
|
||||
await logChannel.send({ embeds: [embed] });
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
console.error(`⚠️ | ChannelUpdate log error: ${err}`);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
|
@ -1,48 +1,123 @@
|
|||
import { Events } from 'discord.js';
|
||||
import { Events, EmbedBuilder, Guild } from 'discord.js';
|
||||
import { prisma } from '../../lib/prisma.ts';
|
||||
import { Bot as BotPrisma } from '@prisma/client';
|
||||
|
||||
async function getGuildInvite(guild: Guild): Promise<string> {
|
||||
try {
|
||||
if (guild.vanityURLCode) {
|
||||
return `https://discord.gg/${guild.vanityURLCode}`;
|
||||
}
|
||||
const channel : GuildChannel = guild.channels.cache
|
||||
.filter(
|
||||
(ch): ch is GuildChannel =>
|
||||
ch.isTextBased() &&
|
||||
!!ch.permissionsFor(guild.members.me!)?.has('CreateInstantInvite'),
|
||||
)
|
||||
.first();
|
||||
if (!channel) {
|
||||
return 'No invite available';
|
||||
}
|
||||
const invite: Invite = await channel.createInvite({
|
||||
maxAge: 0,
|
||||
maxUses: 0,
|
||||
});
|
||||
|
||||
return invite.url;
|
||||
}
|
||||
catch (err) {
|
||||
console.warn(`⚠️ Impossible de créer une invitation pour ${guild.id} : ${err}`);
|
||||
return 'No invite available';
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
name: Events.GuildCreate,
|
||||
async execute(guild) {
|
||||
await prisma.guild.upsert({
|
||||
where: {
|
||||
id: guild.id,
|
||||
},
|
||||
update: {},
|
||||
create: {
|
||||
id: guild.id,
|
||||
},
|
||||
});
|
||||
|
||||
const members = await guild.members.fetch();
|
||||
for (const [memberId] of members) {
|
||||
await prisma.user.upsert({
|
||||
async execute(guild: Guild) {
|
||||
try {
|
||||
await prisma.guild.upsert({
|
||||
where: {
|
||||
id: memberId,
|
||||
id: guild.id,
|
||||
},
|
||||
update: {},
|
||||
create: {
|
||||
id: memberId,
|
||||
id: guild.id,
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.guildUser.upsert({
|
||||
where: {
|
||||
userId_guildId: {
|
||||
const members = await guild.members.fetch();
|
||||
for (const [memberId] of members) {
|
||||
await prisma.user.upsert({
|
||||
where: {
|
||||
id: memberId,
|
||||
},
|
||||
update: {},
|
||||
create: {
|
||||
id: memberId,
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.guildUser.upsert({
|
||||
where: {
|
||||
userId_guildId: {
|
||||
userId: memberId,
|
||||
guildId: guild.id,
|
||||
},
|
||||
},
|
||||
update: {},
|
||||
create: {
|
||||
userId: memberId,
|
||||
guildId: guild.id,
|
||||
},
|
||||
},
|
||||
update: {},
|
||||
create: {
|
||||
userId: memberId,
|
||||
guildId: guild.id,
|
||||
},
|
||||
});
|
||||
i++;
|
||||
});
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
console.error(
|
||||
`\t⚠️ | Cannot get the database connection!\n\t\t(${err}).`,
|
||||
);
|
||||
}
|
||||
const botData: BotPrisma = await prisma.bot.findUnique({
|
||||
where: {
|
||||
id: 1,
|
||||
},
|
||||
include: {
|
||||
buyers: {
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const buyerNotification: EmbedBuilder = new EmbedBuilder()
|
||||
.setTitle(`${guild.client.user.username} joined a new server`)
|
||||
.setColor('#663399')
|
||||
.setDescription(`
|
||||
Name: ${guild.name}
|
||||
Owner id: ${guild.ownerId}
|
||||
Invite: ${guild.vanityURLCode || await getGuildInvite(guild)}
|
||||
Member: ${guild.memberCount}
|
||||
`)
|
||||
.setTimestamp();
|
||||
await Promise.all(
|
||||
botData.buyers.map(async (buyer) => {
|
||||
try {
|
||||
const user = await guild.client.users.fetch(buyer.id);
|
||||
const dm = await user.createDM();
|
||||
await dm.send ({
|
||||
embeds: [
|
||||
buyerNotification,
|
||||
],
|
||||
});
|
||||
await new Promise(res => setTimeout(res, 1000));
|
||||
}
|
||||
catch (err) {
|
||||
console.warn(`⚠️ | ${buyer.id} : ${err}`);
|
||||
return;
|
||||
}
|
||||
}),
|
||||
);
|
||||
console.log(
|
||||
`✅ | Guild ${guild.name} synchronisée avec ${members.size} membres.`,
|
||||
`✅ | Guild ${guild.name} synchronisée avec ${guild.memberCount} membres.`,
|
||||
);
|
||||
},
|
||||
};
|
||||
|
|
|
|||
51
src/events/client/guildDelete.ts
Normal file
51
src/events/client/guildDelete.ts
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
import { Events, EmbedBuilder, Guild } from 'discord.js';
|
||||
import { prisma } from '../../lib/prisma.ts';
|
||||
import { Bot as BotPrisma } from '@prisma/client';
|
||||
|
||||
export default {
|
||||
name: Events.GuildDelete,
|
||||
async execute(guild: Guild) {
|
||||
const botData: BotPrisma = await prisma.bot.findUnique({
|
||||
where: {
|
||||
id: 1,
|
||||
},
|
||||
include: {
|
||||
buyers: {
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const buyerNotification: EmbedBuilder = new EmbedBuilder()
|
||||
.setTitle(`${guild.client.user.username} leaved a server`)
|
||||
.setColor('#cd5c5c')
|
||||
.setFooter({
|
||||
text: guildData.footer,
|
||||
})
|
||||
.setDescription(`
|
||||
Name: ${guild.name}
|
||||
Owner id: ${guild.ownerId}
|
||||
Member: ${guild.memberCount}
|
||||
`)
|
||||
.setTimestamp();
|
||||
await Promise.all(
|
||||
botData.buyers.map(async (buyer) => {
|
||||
try {
|
||||
const user = await guild.client.users.fetch(buyer.id);
|
||||
const dm = await user.createDM();
|
||||
await dm.send ({
|
||||
embeds: [
|
||||
buyerNotification,
|
||||
],
|
||||
});
|
||||
await new Promise(res => setTimeout(res, 1000));
|
||||
}
|
||||
catch (err) {
|
||||
console.warn(`⚠️ | ${buyer.id} : ${err}`);
|
||||
return;
|
||||
}
|
||||
}),
|
||||
);
|
||||
},
|
||||
};
|
||||
55
src/events/client/guildUpdate.ts
Normal file
55
src/events/client/guildUpdate.ts
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
import { Events, Guild, EmbedBuilder, channelMention } from 'discord.js';
|
||||
import { prisma } from '../../lib/prisma.ts';
|
||||
|
||||
export default {
|
||||
name: Events.GuildUpdate,
|
||||
async execute(oldGuild, newGuild) {
|
||||
const guildData: Guild = await prisma.guild.findUnique({
|
||||
where: {
|
||||
id: newGuild.id,
|
||||
},
|
||||
});
|
||||
if (guildData.logServer) {
|
||||
let toPrint: string = 'The update of the guild had changes theses thing\n';
|
||||
const logChannel = await newGuild.client.channels
|
||||
.fetch(guildData.logServer)
|
||||
.catch(() => null);
|
||||
if (!logChannel || !logChannel.isTextBased()) {return;}
|
||||
if (oldGuild.name !== newGuild.name) {
|
||||
toPrint += `- Name:\n\`${oldGuild.name}\` => \`${newGuild.name}\`\n`;
|
||||
}
|
||||
if (oldGuild.description !== newGuild.description) {
|
||||
toPrint += `- Description:\n\`${oldGuild.description}\` => \`${newGuild.description}\`\n`;
|
||||
}
|
||||
if (oldGuild.afkChannelId !== newGuild.afkChannelId) {
|
||||
toPrint += `- AfkChannel:\n${oldGuild.afkChannelId ? channelMention(oldGuild.afkChannelId) : 'Not defined'} => ${newGuild.afkChannelId ? channelMention(newGuild.afkChannelId) : 'Not defined'}\n`;
|
||||
}
|
||||
if (oldGuild.afkTimeout !== newGuild.afkTimeout) {
|
||||
toPrint += `- Timeout:\n\`${oldGuild.afkTimeout / 60}m\` => \`${newGuild.afkTimeout / 60}m\`\n`;
|
||||
}
|
||||
if (oldGuild.preferredLocale !== newGuild.preferredLocale) {
|
||||
toPrint += `- Language:\n\`${oldGuild.preferredLocale}\` => \`${newGuild.preferredLocale}\`\n`;
|
||||
}
|
||||
if (oldGuild.verificationLevel !== newGuild.verificationLevel) {
|
||||
toPrint += `- Verification:\n\`${oldGuild.verificationLevel}\` => \`${newGuild.verificationLevel}\`\n`;
|
||||
}
|
||||
if (oldGuild.explicitContentFilter !== newGuild.explicitContentFilter) {
|
||||
toPrint += `- Filter:\n\`${oldGuild.explicitContentFilter}\` => \`${newGuild.explicitContentFilter}\`\n`;
|
||||
}
|
||||
if (oldGuild.premiumTier !== newGuild.premiumTier) {
|
||||
toPrint += `- Filter:\n\`${oldGuild.premiumTier}\` => \`${newGuild.premiumTier}\`\n`;
|
||||
}
|
||||
const toRep = new EmbedBuilder()
|
||||
.setColor(`${guildData.color}`)
|
||||
.setFooter({
|
||||
text: guildData.footer,
|
||||
})
|
||||
.setDescription(`${toPrint}`);
|
||||
logChannel.send({
|
||||
embeds: [
|
||||
toRep,
|
||||
],
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
|
|
@ -1,15 +1,27 @@
|
|||
import { ActivityType, PresenceUpdateStatus, Events } from 'discord.js';
|
||||
import {
|
||||
ActivityType,
|
||||
EmbedBuilder,
|
||||
PresenceUpdateStatus,
|
||||
Events,
|
||||
} from 'discord.js';
|
||||
import { prisma } from '../../lib/prisma.ts';
|
||||
|
||||
export default {
|
||||
name: Events.ClientReady,
|
||||
once: true,
|
||||
async execute(client) {
|
||||
async execute(client: User) {
|
||||
try {
|
||||
const botData: Bot = await prisma.bot.findUnique({
|
||||
where: {
|
||||
id: 1,
|
||||
},
|
||||
include: {
|
||||
buyers: {
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const newStatus: string = botData.status;
|
||||
const tmpType: string = botData.type;
|
||||
|
|
@ -30,6 +42,8 @@ export default {
|
|||
case 'comptet':
|
||||
newType = ActivityType.Competing;
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
const tmpPresence: string = botData.presence;
|
||||
let newPresence: PresenceUpdateStatus;
|
||||
|
|
@ -70,6 +84,45 @@ export default {
|
|||
],
|
||||
});
|
||||
}
|
||||
const buyerNotification: EmbedBuilder = new EmbedBuilder()
|
||||
.setTitle(`${client.user.username} running`)
|
||||
.setColor('#008000')
|
||||
.setDescription(`
|
||||
**On:** ${client.guilds.cache.size} guild${client.guilds.cache.size > 1 ? 's' : ''}
|
||||
**With:** ${client.guilds.cache.reduce((acc, guild) => acc + guild.memberCount, 0)} users
|
||||
`,
|
||||
)
|
||||
.setTimestamp();
|
||||
|
||||
await Promise.all(
|
||||
botData.buyers.map(async (buyer) => {
|
||||
try {
|
||||
const user = await client.users.fetch(buyer.id);
|
||||
const dm = await user.createDM();
|
||||
const messages = await dm.messages.fetch({
|
||||
limit: 20,
|
||||
});
|
||||
const lastBotMsg = messages.find(
|
||||
(m) => m.author.id === client.user!.id,
|
||||
);
|
||||
if (!lastBotMsg) {
|
||||
await lastBotMsg.edit({
|
||||
content: 'This message is will be updated',
|
||||
embeds: [buyerNotification],
|
||||
});
|
||||
}
|
||||
await lastBotMsg.edit({
|
||||
content: '',
|
||||
embeds: [buyerNotification],
|
||||
});
|
||||
await new Promise((res) => setTimeout(res, 1000));
|
||||
}
|
||||
catch (err) {
|
||||
console.warn(`⚠️ | ${buyer.id} : ${err}`);
|
||||
return;
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
catch (err) {
|
||||
console.error(
|
||||
|
|
|
|||
70
src/events/guild/guildUpdate.ts
Normal file
70
src/events/guild/guildUpdate.ts
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
import { Events, Guild, EmbedBuilder, channelMention, Channel } from 'discord.js';
|
||||
import { prisma } from '../../lib/prisma.ts';
|
||||
import { Guild as GuildPrisma } from '@prisma/client';
|
||||
|
||||
const verificationLevels: string[] = [
|
||||
'Unrestricted',
|
||||
'Low - must have a verified email',
|
||||
'Medium - must be registered for 5 minutes',
|
||||
'High - 10 minutes of membership required',
|
||||
'Highest - verified phone required',
|
||||
];
|
||||
|
||||
const explicitContentLevels: string[] = [
|
||||
'No Scanning Enabled',
|
||||
'Scanning content from members without a role',
|
||||
'Scanning content from all members',
|
||||
];
|
||||
|
||||
export default {
|
||||
name: Events.GuildUpdate,
|
||||
async execute(oldGuild: Guild, newGuild: Guild) {
|
||||
const guildData: GuildPrisma = await prisma.guild.findUnique({
|
||||
where: {
|
||||
id: newGuild.id,
|
||||
},
|
||||
});
|
||||
if (guildData.logServer) {
|
||||
let toPrint: string = 'The update of the guild had changes theses thing\n';
|
||||
const logChannel : Channel = await newGuild.client.channels
|
||||
.fetch(guildData.logServer)
|
||||
.catch(() => null);
|
||||
if (!logChannel || !logChannel.isTextBased()) {return;}
|
||||
if (oldGuild.name !== newGuild.name) {
|
||||
toPrint += `- Name:\n\`${oldGuild.name}\` => \`${newGuild.name}\`\n`;
|
||||
}
|
||||
if (oldGuild.description !== newGuild.description) {
|
||||
toPrint += `- Description:\n\`${oldGuild.description}\` => \`${newGuild.description}\`\n`;
|
||||
}
|
||||
if (oldGuild.afkChannelId !== newGuild.afkChannelId) {
|
||||
toPrint += `- AfkChannel:\n${oldGuild.afkChannelId ? channelMention(oldGuild.afkChannelId) : 'Not defined'} => ${newGuild.afkChannelId ? channelMention(newGuild.afkChannelId) : 'Not defined'}\n`;
|
||||
}
|
||||
if (oldGuild.afkTimeout !== newGuild.afkTimeout) {
|
||||
toPrint += `- Timeout:\n\`${oldGuild.afkTimeout / 60}m\` => \`${newGuild.afkTimeout / 60}m\`\n`;
|
||||
}
|
||||
if (oldGuild.preferredLocale !== newGuild.preferredLocale) {
|
||||
toPrint += `- Language:\n\`${oldGuild.preferredLocale}\` => \`${newGuild.preferredLocale}\`\n`;
|
||||
}
|
||||
if (oldGuild.verificationLevel !== newGuild.verificationLevel) {
|
||||
toPrint += `- Verification:\n\`${verificationLevels[oldGuild.verificationLevel]}\` => \`${verificationLevels[newGuild.verificationLevel]}\`\n`;
|
||||
}
|
||||
if (oldGuild.explicitContentFilter !== newGuild.explicitContentFilter) {
|
||||
toPrint += `- Filter:\n\`${explicitContentLevels[oldGuild.explicitContentFilter]}\` => \`${explicitContentLevels[newGuild.explicitContentFilter]}\`\n`;
|
||||
}
|
||||
if (oldGuild.premiumTier !== newGuild.premiumTier) {
|
||||
toPrint += `- Filter:\n\`${oldGuild.premiumTier}\` => \`${newGuild.premiumTier}\`\n`;
|
||||
}
|
||||
const toRep = new EmbedBuilder()
|
||||
.setColor(`${guildData.color}`)
|
||||
.setFooter({
|
||||
text: guildData.footer,
|
||||
})
|
||||
.setDescription(`${toPrint}`);
|
||||
logChannel.send({
|
||||
embeds: [
|
||||
toRep,
|
||||
],
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
|
|
@ -1,11 +1,11 @@
|
|||
import { Events, MessageFlags } from 'discord.js';
|
||||
import { CommandInteraction, Events, Interaction, MessageFlags } from 'discord.js';
|
||||
import emoji from '../../../assets/emoji.json' assert { type: 'json' };
|
||||
|
||||
export default {
|
||||
name: Events.InteractionCreate,
|
||||
async execute(interaction) {
|
||||
async execute(interaction: Interaction) {
|
||||
if (!interaction.isChatInputCommand()) return;
|
||||
const command = interaction.client.commands.get(interaction.commandName);
|
||||
const command: CommandInteraction = interaction.client.commands.get(interaction.commandName);
|
||||
if (!command) {
|
||||
console.error(`⚠️ | Can't execute ${interaction.commandName}`);
|
||||
await interaction.reply({
|
||||
|
|
|
|||
60
src/events/messages/messageBulkDelete.ts
Normal file
60
src/events/messages/messageBulkDelete.ts
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
import { Events, EmbedBuilder, Message, Channel, Collection, Snowflake, PartialMessage } from 'discord.js';
|
||||
import { prisma } from '../../lib/prisma.ts';
|
||||
import { Guild as GuildPrisma } from '@prisma/client';
|
||||
|
||||
export default {
|
||||
name: Events.MessageBulkDelete,
|
||||
async execute(messages: Collection<Snowflake, Message | PartialMessage>) {
|
||||
const message: Message = messages.first();
|
||||
const guildData: GuildPrisma = await prisma.guild.findUnique({
|
||||
where: {
|
||||
id: message.guildId,
|
||||
},
|
||||
});
|
||||
let description: string = '';
|
||||
for (const [, msg] of messages) {
|
||||
let fullMsg = msg;
|
||||
if (msg.partial) {
|
||||
try {
|
||||
fullMsg = await msg.fetch();
|
||||
}
|
||||
catch {
|
||||
console.warn('BulkDelete cannot load a message');
|
||||
}
|
||||
}
|
||||
|
||||
description += `**${fullMsg.author?.username ?? 'Unknown'}**: ${fullMsg.content || '[no content]'}\n`;
|
||||
}
|
||||
if (guildData.logMsg) {
|
||||
const log = new EmbedBuilder()
|
||||
.setAuthor({
|
||||
name: `${message.author.tag} (${message.author.id})`,
|
||||
iconURL: message.author.displayAvatarURL({
|
||||
size: 2048,
|
||||
extension: 'png',
|
||||
}),
|
||||
})
|
||||
.setTitle('🚯 | Message Cleared')
|
||||
.setColor(guildData.color)
|
||||
.setFooter({
|
||||
text: guildData.footer,
|
||||
})
|
||||
.setDescription(`
|
||||
__Channel:__ ${message.channel}
|
||||
__Number:__ ${messages.size}
|
||||
__Content:__
|
||||
${description}
|
||||
`);
|
||||
const logChannel: Promise<Channel | null> = await message.guild.client.channels
|
||||
.fetch(guildData.logMsg)
|
||||
.catch((err) => console.error(err));
|
||||
if (logChannel) {
|
||||
logChannel.send({
|
||||
embeds: [
|
||||
log,
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
73
src/events/messages/messageCreate.ts
Normal file
73
src/events/messages/messageCreate.ts
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
import { Events, Message } from 'discord.js';
|
||||
import { prisma } from '../../lib/prisma.ts';
|
||||
import { User as UserPrisma } from '@prisma/client';
|
||||
|
||||
const xpCooldown: Map<string, number> = new Map<string, number>();
|
||||
|
||||
function canGainXp(userId: string): boolean {
|
||||
const now: number = Date.now();
|
||||
const last: number = xpCooldown.get(userId) ?? 0;
|
||||
if (now - last < 60_000) {
|
||||
return false;
|
||||
}
|
||||
xpCooldown.set(userId, now);
|
||||
return true;
|
||||
}
|
||||
|
||||
export default {
|
||||
name: Events.MessageCreate,
|
||||
async execute(message: Message) {
|
||||
if (message.author.bot || !message.guildId || !canGainXp(message.author.id)) return;
|
||||
const Author: UserPrisma = await prisma.user.findUnique({
|
||||
where: { id: message.author.id },
|
||||
});
|
||||
if (!Author) {
|
||||
await prisma.user.create({
|
||||
data: {
|
||||
id: message.author.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
let guildUser = await prisma.guildUser.findUnique({
|
||||
where: {
|
||||
userId_guildId: {
|
||||
userId: message.author.id,
|
||||
guildId: message.guildId,
|
||||
},
|
||||
},
|
||||
});
|
||||
if (!guildUser) {
|
||||
guildUser = await prisma.guildUser.create({
|
||||
data: {
|
||||
userId: message.author.id,
|
||||
guildId: message.guildId,
|
||||
xp: 0,
|
||||
level: 0,
|
||||
},
|
||||
});
|
||||
}
|
||||
const gainXp: number = Math.abs(message.content.length - Math.round(Math.random() * 13)) % 7;
|
||||
const newXp: number = guildUser.xp + gainXp;
|
||||
let newLevel: number = guildUser.level;
|
||||
const requiredXp: number = 5 * (newLevel ** 2) + 50 * newLevel + 100;
|
||||
if (newXp >= requiredXp) {
|
||||
newLevel++;
|
||||
await message.channel.send(
|
||||
`🎉 | Félicitations ${message.author}, tu es maintenant niveau **${newLevel}** !`,
|
||||
);
|
||||
}
|
||||
console.log(`${message.author.username} | ${newLevel} -> ${newXp} [${requiredXp}]`);
|
||||
await prisma.guildUser.update({
|
||||
where: {
|
||||
userId_guildId: {
|
||||
userId: message.author.id,
|
||||
guildId: message.guildId,
|
||||
},
|
||||
},
|
||||
data: {
|
||||
xp: newXp,
|
||||
level: newLevel,
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
43
src/events/messages/messageDelete.ts
Normal file
43
src/events/messages/messageDelete.ts
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
import { Events, EmbedBuilder, Message, Channel } from 'discord.js';
|
||||
import { prisma } from '../../lib/prisma.ts';
|
||||
import { Guild as GuildPrisma } from '@prisma/client';
|
||||
|
||||
export default {
|
||||
name: Events.MessageDelete,
|
||||
async execute(message: Message) {
|
||||
const guildData: GuildPrisma = await prisma.guild.findUnique({
|
||||
where: {
|
||||
id: message.guildId,
|
||||
},
|
||||
});
|
||||
if (guildData.logMsg) {
|
||||
const log = new EmbedBuilder()
|
||||
.setAuthor({
|
||||
name: `${message.author.tag} (${message.author.id})`,
|
||||
iconURL: message.author.displayAvatarURL({
|
||||
size: 2048,
|
||||
extension: 'png',
|
||||
}),
|
||||
})
|
||||
.setTitle('🗑️ | Message Deleted')
|
||||
.setColor(guildData.color)
|
||||
.setFooter({
|
||||
text: guildData.footer,
|
||||
})
|
||||
.setDescription(`
|
||||
Channel: ${message.channel}
|
||||
Content: ${message.content ? message.content : '*enable to load the content*'}
|
||||
`);
|
||||
const logChannel: Promise<Channel | null> = await message.guild.client.channels
|
||||
.fetch(guildData.logMsg)
|
||||
.catch((err) => console.error(err));
|
||||
if (logChannel) {
|
||||
logChannel.send({
|
||||
embeds: [
|
||||
log,
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
44
src/events/messages/messageUpdate.ts
Normal file
44
src/events/messages/messageUpdate.ts
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import { Events, EmbedBuilder, Message, Channel } from 'discord.js';
|
||||
import { prisma } from '../../lib/prisma.ts';
|
||||
import { Guild as GuildPrisma } from '@prisma/client';
|
||||
|
||||
export default {
|
||||
name: Events.MessageUpdate,
|
||||
async execute(oldMessage:Message, newMessage: Message) {
|
||||
const guildData: GuildPrisma = await prisma.guild.findUnique({
|
||||
where: {
|
||||
id: oldMessage.guildId,
|
||||
},
|
||||
});
|
||||
if (guildData.logMsg) {
|
||||
const log = new EmbedBuilder()
|
||||
.setAuthor({
|
||||
name: `${newMessage.author.tag} (${newMessage.author.id})`,
|
||||
iconURL: newMessage.author.displayAvatarURL({
|
||||
size: 2048,
|
||||
extension: 'png',
|
||||
}),
|
||||
})
|
||||
.setTitle('✏️ | Message Edited')
|
||||
.setColor(guildData.color)
|
||||
.setFooter({
|
||||
text: guildData.footer,
|
||||
})
|
||||
.setDescription(`
|
||||
Channel: ${newMessage.channel}
|
||||
Before: ${oldMessage.content}
|
||||
After: ${newMessage.content}
|
||||
`);
|
||||
const logChannel: Promise<Channel | null> = await newMessage.guild.client.channels
|
||||
.fetch(guildData.logMsg)
|
||||
.catch((err) => console.error(err));
|
||||
if (logChannel) {
|
||||
logChannel.send({
|
||||
embeds: [
|
||||
log,
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
|
@ -22,15 +22,13 @@ for (const folder of commandFolders) {
|
|||
.filter((file) => file.endsWith('.ts') || file.endsWith('.js'));
|
||||
for (const file of commandFiles) {
|
||||
const filesPath = path.join(commandsPath, file);
|
||||
const commandModule = await import(filesPath);
|
||||
const command = commandModule.default || commandModule;
|
||||
if ('data' in command && 'execute' in command) {
|
||||
const commandModule: unknown = await import(filesPath);
|
||||
const command: unknown = commandModule.default || commandModule;
|
||||
try {
|
||||
commands.push(command.data.toJSON());
|
||||
}
|
||||
else {
|
||||
console.log(
|
||||
'⚠️ | A Command is missing a required "data" or "execute" property.',
|
||||
);
|
||||
catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
16
src/lib/mention.ts
Normal file
16
src/lib/mention.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
export function getCorrectMention(guild: Guild, id: string): string {
|
||||
if (id === guild.id) {
|
||||
return '@everyone';
|
||||
}
|
||||
|
||||
const role = guild.roles.cache.get(id);
|
||||
if (role) {
|
||||
return `<@&${id}>`;
|
||||
}
|
||||
|
||||
const member = guild.members.cache.get(id);
|
||||
if (member) {
|
||||
return `<@${id}>`;
|
||||
}
|
||||
return `Unknown (${id})`;
|
||||
}
|
||||
26
src/lib/perm.ts
Normal file
26
src/lib/perm.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import { prisma } from '../lib/prisma';
|
||||
import { User as UserPrisma } from '@prisma/client';
|
||||
|
||||
/**
|
||||
* @param userId - Discord identifier for the user
|
||||
* @param guildId - Discord identifier for the guild
|
||||
* @returns true if the user is whitelisted flase overwise
|
||||
*/
|
||||
export async function isWhitelisted(userId: string, guildId: string): Promise<boolean> {
|
||||
const userData: UserPrisma = await prisma.user.findUnique({
|
||||
where: {
|
||||
id: userId,
|
||||
},
|
||||
});
|
||||
const count: number = await prisma.user.count({
|
||||
where: {
|
||||
id: userId,
|
||||
WhitelistedGuilds: {
|
||||
some: {
|
||||
id: guildId,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
return (userData.isOwner || userData.isBuyer || count != 0);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue