Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 76 additions & 42 deletions src/base/classes/command.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import {
ChatInputCommandInteraction,
Colors,
ContainerBuilder,
MessageFlags,
PermissionFlagsBits,
SlashCommandBuilder,
Expand Down Expand Up @@ -32,7 +34,7 @@ export class Command {
requiredPermissions: (keyof typeof PermissionFlagsBits)[];
cooldown: number;
masterLock: boolean
private _cooldowns: Map<string, number>;
private readonly _cooldowns: Map<string, number>;

constructor({ info, execute, guildOnly = false, ownerOnly = false, requiredPermissions = [], cooldown = 0, masterLock = false }: CommandOptions) {
this.data = info;
Expand All @@ -50,50 +52,12 @@ export class Command {
}

async run(interaction: ChatInputCommandInteraction, ownerId?: string): Promise<void> {
if (this.guildOnly && !interaction.guild) {
await interaction.reply({ content: 'This command is limited to servers', flags: MessageFlags.Ephemeral });
return;
}

if (this.ownerOnly && interaction.user.id !== ownerId) {
await interaction.reply({ content: 'Only the owner can run this command', flags: MessageFlags.Ephemeral });
if (!await this.validateCommand(interaction, ownerId)) {
return;
}

if (this.masterLock && interaction.guild) {
if (interaction.guild.id != process.env.MASTER_GUILD) {
await interaction.reply('This command cannot be ran in this server.')
}
}

if (this.requiredPermissions.length && interaction.guild) {
const missing = this.requiredPermissions.filter(
p => !interaction.memberPermissions?.has(PermissionFlagsBits[p])
);
if (missing.length) {
await interaction.reply({
content: `You're lacking of the necessary permissions: \`${missing.join(', ')}\``,
flags: MessageFlags.Ephemeral,
});
return;
}
}

if (this.cooldown > 0) {
const now = Date.now();
const expiry = this._cooldowns.get(interaction.user.id);
if (expiry && now < expiry) {
const unixExpiry = Math.floor(expiry / 1000);
await interaction.reply({
content: `You can use \`/${this.name}\` again, please wait <t:${unixExpiry}:R>.`,
flags: MessageFlags.Ephemeral,
});
return;
}
this._cooldowns.set(interaction.user.id, now + this.cooldown * 1000);
setTimeout(() => this._cooldowns.delete(interaction.user.id), this.cooldown * 1000);
}

await this.applyCooldown(interaction);

try {
await this.execute(interaction);
} catch (err) {
Expand All @@ -106,4 +70,74 @@ export class Command {
}
}
}

private async validateCommand(interaction: ChatInputCommandInteraction, ownerId?: string): Promise<boolean> {
if (this.guildOnly && !interaction.guild) {
await interaction.reply({ content: 'This command is limited to servers', flags: MessageFlags.Ephemeral });
return false;
}

if (this.ownerOnly && interaction.user.id !== ownerId) {
const container = new ContainerBuilder().setAccentColor(Colors.Red)
.addTextDisplayComponents((txt) => txt.setContent([
"## Owner-locked command",
"This command can only be used by the owner of this server."
].join("\n")))
await interaction.reply({ components: [container], flags: [MessageFlags.Ephemeral, MessageFlags.IsComponentsV2]})
return false;
}

if (this.masterLock && interaction.guild?.id !== process.env.MASTER_GUILD) {
await interaction.reply('This command cannot be ran in this server.');
return false;
}

if (!await this.validatePermissions(interaction)) {
return false;
}

return true;
}

private async validatePermissions(interaction: ChatInputCommandInteraction): Promise<boolean> {
if (!this.requiredPermissions.length || !interaction.guild) {
return true;
}

const missing = this.requiredPermissions.filter(
p => !interaction.memberPermissions?.has(PermissionFlagsBits[p])
);

if (missing.length) {
await interaction.reply({
content: `You're lacking of the necessary permissions: \`${missing.join(', ')}\``,
flags: MessageFlags.Ephemeral,
});
return false;
}

return true;
}

private async applyCooldown(interaction: ChatInputCommandInteraction): Promise<void> {
if (this.cooldown <= 0) {
return;
}

const now = Date.now();
const expiry = this._cooldowns.get(interaction.user.id);

if (expiry && now < expiry) {
const unixExpiry = Math.floor(expiry / 1000);
await interaction.reply({
content: `You can use \`/${this.name}\` again, please wait <t:${unixExpiry}:R>.`,
flags: MessageFlags.Ephemeral,
});
return;
}

this._cooldowns.set(interaction.user.id, now + this.cooldown * 1000);
setTimeout(() => this._cooldowns.delete(interaction.user.id), this.cooldown * 1000);
await this.execute(interaction);
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
import {
SlashCommandBuilder,
ChatInputCommandInteraction,
EmbedBuilder,
ActionRowBuilder,
ButtonBuilder,
ButtonStyle,
} from "discord.js";
import { Command } from "../base/classes/command.js";

Expand Down
1 change: 0 additions & 1 deletion src/commands/info.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import {
SlashCommandBuilder,
ChatInputCommandInteraction,
EmbedBuilder,
ButtonBuilder,
ButtonStyle,
Expand Down
1 change: 0 additions & 1 deletion src/commands/role.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import {
SlashCommandBuilder,
ChatInputCommandInteraction,
ColorResolvable,
ContainerBuilder,
TextDisplayBuilder,
Expand Down
Empty file added src/commands/settings.ts
Empty file.
10 changes: 9 additions & 1 deletion src/commands/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,12 @@ import {
ChannelType,
LabelBuilder,
MessageFlags,
ContainerBuilder,
Colors,
} from "discord.js";
import { Command } from "../base/classes/command.js";
import { prisma } from "../libs/database.js";
import { SystemColors } from "../libs/colors.js";

export default new Command({
info: new SlashCommandBuilder()
Expand All @@ -22,7 +25,12 @@ export default new Command({
});

if (serversetup) {
await interaction.reply({ content: "This server has been setted up.", flags: [MessageFlags.Ephemeral]})
const container = new ContainerBuilder().setAccentColor(SystemColors.main)
.addTextDisplayComponents((txt) => txt.setContent([
"## This server is already set up",
"This server was already configured, if you want to change a setting please run the `/settings` command."
].join("\n")))
await interaction.reply({ components: [container], flags: [MessageFlags.Ephemeral, MessageFlags.IsComponentsV2]})
return
}

Expand Down
2 changes: 1 addition & 1 deletion src/events/clientReady.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ActivityType, Client, Events } from "discord.js";
import { Client, Events } from "discord.js";
import { logger } from "../libs/logger.js";

export default {
Expand Down
93 changes: 58 additions & 35 deletions src/events/guildMemberUpdate.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import "../libs/loadVariables.js"
import { Client, EmbedBuilder, Events, GuildMember, TextChannel } from "discord.js";
import "../libs/loadVariables.js";
import {
Client,
EmbedBuilder,
Events,
GuildMember,
TextChannel,
} from "discord.js";
import {
registerBoost,
removeBoost,
Expand All @@ -15,12 +21,19 @@ import { prisma } from "../libs/database.js";

export default {
name: Events.GuildMemberUpdate,
async execute(_client: Client, oldMember: GuildMember, newMember: GuildMember) {
async execute(
_client: Client,
oldMember: GuildMember,
newMember: GuildMember,
) {
try {
if (oldMember.partial) oldMember = await oldMember.fetch();
if (newMember.partial) newMember = await newMember.fetch();
} catch (error) {
logger.error(`Failed to fetch partials for user ${oldMember.user.username}`);
const username =
oldMember?.user?.username ?? oldMember?.id ?? "Unknown user";
logger.error(`Failed to fetch partials for user ${username}`);
return;
}

const wasBoostingBefore = oldMember.premiumSince !== null;
Expand All @@ -43,7 +56,9 @@ async function onBoostStart(member: GuildMember): Promise<void> {

const settings = await getGuildSettings(guild.id);
if (!settings) {
logger.error(`No guild settings found for guild ${guild.id} — run /setup first.`);
logger.error(
`No guild settings found for guild ${guild.id} — run /setup first.`,
);
return;
}

Expand All @@ -58,43 +73,45 @@ async function onBoostStart(member: GuildMember): Promise<void> {
await clearPendingCustomRoleDeletion(record.id);
await assignLevelRoles(member, record.boostCounts ?? 1);

const greetChannel = guild.channels.cache.get(settings.greetChannelId) as TextChannel | undefined;
const greetChannel = guild.channels.cache.get(settings.greetChannelId) as
| TextChannel
| undefined;
if (greetChannel) {
const embed = new EmbedBuilder()
.setColor(0xf47fff)
.setTitle("New Server Boost! 🎉")
.setDescription(`${member} has boosted the server!`)
.addFields(
{
name: "Total Boosts",
value: String(record.boostCounts ?? 1),
inline: true
},
{ name: "Member",
value: member.user.tag,
inline: true
{
name: "Total Boosts",
value: String(record.boostCounts ?? 1),
inline: true,
},
{ name: "Member", value: member.user.tag, inline: true },
)
.setThumbnail(member.displayAvatarURL({ size: 512 }))
.setFooter({ text: "Thank youuu!"})
.setTimestamp();
await greetChannel.send({ embeds: [embed] });
}

const logChannel = guild.channels.cache.get(settings.logChannelId) as TextChannel | undefined;
const logChannel = guild.channels.cache.get(settings.logChannelId) as
| TextChannel
| undefined;
if (logChannel) {
const logEmbed = new EmbedBuilder()
.setColor(0x57f287)
.setTitle("Boost Started")
.addFields(
{
name: "User",
value: `${member.user.tag} (${member.id})`,
inline: false
{
name: "User",
value: `${member.user.tag} (${member.id})`,
inline: false,
},
{
name: "Total Boost Count",
value: String(record.boostCounts ?? 1),
inline: true
{
name: "Total Boost Count",
value: String(record.boostCounts ?? 1),
inline: true,
},
)
.setThumbnail(member.displayAvatarURL({ size: 512 }))
Expand All @@ -103,7 +120,9 @@ async function onBoostStart(member: GuildMember): Promise<void> {
}

try {
await member.send(`Thank you for boosting the server! You now have access to booster perks.`);
await member.send(
`Thank you for boosting the server! You now have access to booster perks.`,
);
} catch {}
}

Expand All @@ -112,7 +131,9 @@ async function onBoostEnd(member: GuildMember): Promise<void> {

const settings = await getGuildSettings(guild.id);
if (!settings) {
logger.error(`No guild settings found for guild ${guild.id} — run /setup first.`);
logger.error(
`No guild settings found for guild ${guild.id} — run /setup first.`,
);
return;
}

Expand All @@ -122,25 +143,27 @@ async function onBoostEnd(member: GuildMember): Promise<void> {
await removeAllLevelRoles(member);
await scheduleCustomRoleDeletionAfterGrace(member.id, guild.id);

const logChannel = guild.channels.cache.get(settings.logChannelId) as TextChannel | undefined;
const logChannel = guild.channels.cache.get(settings.logChannelId) as
| TextChannel
| undefined;
if (logChannel) {
const logEmbed = new EmbedBuilder()
.setColor(0xed4245)
.setTitle("Boost Ended")
.addFields(
{
name: "User",
value: `${member.user.tag} (${member.id})`,
inline: false
{
name: "User",
value: `${member.user.tag} (${member.id})`,
inline: false,
},
{
name: "Historical Boost Count",
value: String(result.boostCounts ?? 0),
inline: true
{
name: "Historical Boost Count",
value: String(result.boostCounts ?? 0),
inline: true,
},
)
.setThumbnail(member.displayAvatarURL({ size: 512 }))
.setTimestamp();
await logChannel.send({ embeds: [logEmbed] });
}
}
}
1 change: 0 additions & 1 deletion src/events/roleCleanup.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { Client, Events } from "discord.js";
import { logger } from "../libs/logger.js";
import { processDueCustomRoleDeletions } from "../services/customRoleCleanup.js";

const CUSTOM_ROLE_CLEANUP_MS = 60 * 60 * 1000;
Expand Down
3 changes: 3 additions & 0 deletions src/libs/colors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export enum SystemColors {
main = 0xFF48B6
}
Loading
Loading