🚀 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

@ -9,9 +9,27 @@
outputs = { self, nixpkgs, flake-utils }: outputs = { self, nixpkgs, flake-utils }:
flake-utils.lib.eachDefaultSystem (system: flake-utils.lib.eachDefaultSystem (system:
let let
pkgs = import nixpkgs { pkgs = import nixpkgs { inherit system; };
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 { in {
devShells.default = pkgs.mkShell { devShells.default = pkgs.mkShell {
name = "discord-bot-bun-ts"; name = "discord-bot-bun-ts";
@ -22,6 +40,7 @@
wget wget
nodejs_latest nodejs_latest
mariadb mariadb
tmux-setup
]; ];
shellHook = '' shellHook = ''
export NIX_SHOW_STATS=0 export NIX_SHOW_STATS=0

View file

@ -15,22 +15,22 @@
}, },
"devDependencies": { "devDependencies": {
"@types/bun": "latest", "@types/bun": "latest",
"eslint": "^9.33.0", "eslint": "^9.36.0",
"eslint-config-prettier": "^10.1.8", "eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.5.4", "eslint-plugin-prettier": "^5.5.4",
"husky": "^9.1.7", "husky": "^9.1.7",
"lint-staged": "^16.1.5", "lint-staged": "^16.2.3",
"prettier": "^3.6.2", "prettier": "^3.6.2",
"prisma": "^6.14.0", "prisma": "^6.16.2",
"typescript-eslint": "^8.39.1" "typescript-eslint": "^8.45.0"
}, },
"peerDependencies": { "peerDependencies": {
"typescript": "^5.8.3" "typescript": "^5.8.3"
}, },
"dependencies": { "dependencies": {
"@prisma/client": "^6.14.0", "@prisma/client": "^6.16.2",
"discord.js": "^14.21.0", "discord.js": "^14.22.1",
"dotenv": "^17.2.1", "dotenv": "^17.2.2",
"mariadb": "^3.4.5" "mariadb": "^3.4.5"
} }
} }

View file

@ -46,6 +46,14 @@ model Guild {
logMsg String? logMsg String?
logServer 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") footer String @default("© EniumTeam ~ 2025")
color String @default("#000000") color String @default("#000000")

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

View file

@ -10,8 +10,8 @@ import {
StringSelectMenuOptionBuilder, StringSelectMenuOptionBuilder,
SlashCommandBuilder, SlashCommandBuilder,
MessageFlags, MessageFlags,
SlashCommandBuilder,
EmbedBuilder, EmbedBuilder,
CommandInteraction,
} from 'discord.js'; } from 'discord.js';
import emoji from '../../../assets/emoji.json' assert { type: 'json' }; import emoji from '../../../assets/emoji.json' assert { type: 'json' };
@ -72,11 +72,6 @@ export default {
} }
catch (err) { catch (err) {
throw `\t⚠ | Cannot get the database connection!\n\t\t(${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'); const choice: string = interaction.options.getString('action');
switch (choice) { switch (choice) {
@ -150,7 +145,7 @@ export default {
), ),
); );
const roleSelection = const roleSelection: ActionRowBuilder<StringSelectMenuBuilder> =
new ActionRowBuilder<StringSelectMenuBuilder>().addComponents(menu); new ActionRowBuilder<StringSelectMenuBuilder>().addComponents(menu);
const permSelector: EmbedBuilder = new EmbedBuilder() const permSelector: EmbedBuilder = new EmbedBuilder()
@ -170,6 +165,15 @@ export default {
time: 60_000, time: 60_000,
max: 25, 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( collector.on(
'collect', 'collect',
async (selectInteraction: StringSelectMenuInteraction) => { 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, PresenceUpdateStatus,
MessageFlags, MessageFlags,
SlashCommandBuilder, SlashCommandBuilder,
CommandInteraction,
} from 'discord.js'; } from 'discord.js';
import emoji from '../../../assets/emoji.json' assert { type: 'json' }; import emoji from '../../../assets/emoji.json' assert { type: 'json' };
import { User as UserPrisma } from '@prisma/client';
export default { export default {
data: new SlashCommandBuilder() data: new SlashCommandBuilder()
@ -108,7 +110,7 @@ export default {
), ),
), ),
async execute(interaction: CommandInteraction) { async execute(interaction: CommandInteraction) {
let userData: User; let userData: UserPrisma | null;
try { try {
userData = await prisma.user.findUnique({ userData = await prisma.user.findUnique({
where: { 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 { prisma } from '../../lib/prisma.ts';
import emoji from '../../../assets/emoji.json' assert { type: 'json' }; 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 { export default {
data: new SlashCommandBuilder() data: new SlashCommandBuilder()
@ -33,7 +35,7 @@ export default {
), ),
async execute(interaction: CommandInteraction) { async execute(interaction: CommandInteraction) {
const subcommand = interaction.options.getSubcommand(); const subcommand = interaction.options.getSubcommand();
let userData: User; let userData: UserPrisma;
try { try {
userData = await prisma.user.findUnique({ userData = await prisma.user.findUnique({
where: { where: {
@ -51,7 +53,7 @@ export default {
}); });
return; return;
} }
let guildData: Guild; let guildData: GuildPrisma;
try { try {
guildData = await prisma.guild.findUnique({ guildData = await prisma.guild.findUnique({
where: { where: {

View 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}`);
}
},
};

View 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}`);
}
},
};

View 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}`);
}
},
};

View file

@ -1,9 +1,39 @@
import { Events } from 'discord.js'; import { Events, EmbedBuilder, Guild } from 'discord.js';
import { prisma } from '../../lib/prisma.ts'; 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 { export default {
name: Events.GuildCreate, name: Events.GuildCreate,
async execute(guild) { async execute(guild: Guild) {
try {
await prisma.guild.upsert({ await prisma.guild.upsert({
where: { where: {
id: guild.id, id: guild.id,
@ -39,10 +69,55 @@ export default {
guildId: guild.id, 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( console.log(
`✅ | Guild ${guild.name} synchronisée avec ${members.size} membres.`, `✅ | Guild ${guild.name} synchronisée avec ${guild.memberCount} membres.`,
); );
}, },
}; };

View 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;
}
}),
);
},
};

View 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,
],
});
}
},
};

View file

@ -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'; import { prisma } from '../../lib/prisma.ts';
export default { export default {
name: Events.ClientReady, name: Events.ClientReady,
once: true, once: true,
async execute(client) { async execute(client: User) {
try { try {
const botData: Bot = await prisma.bot.findUnique({ const botData: Bot = await prisma.bot.findUnique({
where: { where: {
id: 1, id: 1,
}, },
include: {
buyers: {
select: {
id: true,
},
},
},
}); });
const newStatus: string = botData.status; const newStatus: string = botData.status;
const tmpType: string = botData.type; const tmpType: string = botData.type;
@ -30,6 +42,8 @@ export default {
case 'comptet': case 'comptet':
newType = ActivityType.Competing; newType = ActivityType.Competing;
break; break;
default:
return;
} }
const tmpPresence: string = botData.presence; const tmpPresence: string = botData.presence;
let newPresence: PresenceUpdateStatus; 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) { catch (err) {
console.error( console.error(

View 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,
],
});
}
},
};

View file

@ -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' }; import emoji from '../../../assets/emoji.json' assert { type: 'json' };
export default { export default {
name: Events.InteractionCreate, name: Events.InteractionCreate,
async execute(interaction) { async execute(interaction: Interaction) {
if (!interaction.isChatInputCommand()) return; if (!interaction.isChatInputCommand()) return;
const command = interaction.client.commands.get(interaction.commandName); const command: CommandInteraction = interaction.client.commands.get(interaction.commandName);
if (!command) { if (!command) {
console.error(`⚠️ | Can't execute ${interaction.commandName}`); console.error(`⚠️ | Can't execute ${interaction.commandName}`);
await interaction.reply({ await interaction.reply({

View 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,
],
});
}
}
},
};

View 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,
},
});
},
};

View 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,
],
});
}
}
},
};

View 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,
],
});
}
}
},
};

View file

@ -22,15 +22,13 @@ for (const folder of commandFolders) {
.filter((file) => file.endsWith('.ts') || file.endsWith('.js')); .filter((file) => file.endsWith('.ts') || file.endsWith('.js'));
for (const file of commandFiles) { for (const file of commandFiles) {
const filesPath = path.join(commandsPath, file); const filesPath = path.join(commandsPath, file);
const commandModule = await import(filesPath); const commandModule: unknown = await import(filesPath);
const command = commandModule.default || commandModule; const command: unknown = commandModule.default || commandModule;
if ('data' in command && 'execute' in command) { try {
commands.push(command.data.toJSON()); commands.push(command.data.toJSON());
} }
else { catch (err) {
console.log( console.error(err);
'⚠️ | A Command is missing a required "data" or "execute" property.',
);
} }
} }
} }

16
src/lib/mention.ts Normal file
View 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
View 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);
}