Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Quarantine system #6

Merged
merged 13 commits into from
Apr 7, 2024
33 changes: 32 additions & 1 deletion server/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ permissions:
canEditServersRoles:
- '1206354879321210900'
- '1206355082883371049'
canCreateQuarantinesRoles:
- '1206032198562742312'
- '1206354879321210900'
profilesMaxSocialsLength: 8
packagesMaxEmojisLength: 9
packagesMinEmojisLength: 4
Expand Down Expand Up @@ -80,4 +83,32 @@ excludeCollectionsInBackup:
- 'sessions'
- 'votereminders'
- 'voteremindermetadatas'
- 'votetimeouts'
- 'votetimeouts'
quarantineTypes:
- 'USER_ID'
- 'GUILD_ID'
quarantineRestrictions:
'PROFILES_CREATE':
available_to:
- 'USER_ID'
'PROFILES_LIKE':
available_to:
- 'USER_ID'
'EMOJIS_CREATE':
available_to:
- 'USER_ID'
'EMOJIS_QUICKLY_UPLOAD':
available_to:
- 'USER_ID'
'SERVERS_CREATE':
available_to:
- 'USER_ID'
- 'GUILD_ID'
'SERVERS_CREATE_REVIEW':
available_to:
- 'USER_ID'
'SERVERS_VOTE':
available_to:
- 'USER_ID'
- 'GUILD_ID'
quarantineLogsChannelId: '1226257480112406599'
1 change: 1 addition & 0 deletions server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
"module-alias": "^2.2.3",
"moment": "^2.30.1",
"mongoose": "^8.0.0",
"ms": "^2.1.3",
"multer": "1.4.5-lts.1",
"music-metadata": "^7.14.0",
"passport": "^0.7.0",
Expand Down
3 changes: 3 additions & 0 deletions server/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

235 changes: 235 additions & 0 deletions server/src/bot/commands/admin.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ const PremiumCode = require('@/src/schemas/PremiumCode');
const Premium = require('@/src/schemas/Premium');
const crypto = require('node:crypto');
const Review = require('@/schemas/Server/Review');
const Quarantine = require('@/schemas/Quarantine');
const ms = require('ms');

const { S3Client, DeleteObjectCommand } = require('@aws-sdk/client-s3');
const S3 = new S3Client({
Expand Down Expand Up @@ -51,6 +53,20 @@ module.exports = {
.addSubcommand(subcommand => subcommand.setName('review-delete').setDescription('Deletes a review.')
.addStringOption(option => option.setName('review').setDescription('Select the review to delete.').setRequired(true).setAutocomplete(true))))

.addSubcommandGroup(group => group.setName('quarantine').setDescription('quarantine')
.addSubcommand(subcommand => subcommand.setName('create').setDescription('Creates a new quarantine entry.')
.addStringOption(option => option.setName('type').setDescription('The type of the quarantine entry.').setRequired(true).addChoices(...config.quarantineTypes.map(type => ({ name: type, value: type }))))
.addStringOption(option => option.setName('value').setDescription('The value of the quarantine entry. (User ID, Server ID, etc.)').setRequired(true))
.addStringOption(option => option.setName('restriction').setDescription('The restriction of the quarantine entry.').setRequired(true).addChoices(...Object.keys(config.quarantineRestrictions).map(restriction => ({ name: restriction, value: restriction }))))
.addStringOption(option => option.setName('reason').setDescription('The reason for the quarantine entry.').setRequired(true))
.addStringOption(option => option.setName('time').setDescription('Expiration time for the quarantine entry. (Optional, 20m, 6h, 3d, 1w, 1m, 1y)')))
.addSubcommand(subcommand => subcommand.setName('remove').setDescription('Removes a quarantine entry.')
.addStringOption(option => option.setName('entry').setDescription('Select the quarantine to remove.').setRequired(true).setAutocomplete(true)))
.addSubcommand(subcommand => subcommand.setName('list').setDescription('Lists all quarantine entries.'))
.addSubcommand(subcommand => subcommand.setName('find').setDescription('Finds a quarantine entry.')
.addStringOption(option => option.setName('type').setDescription('The type of the quarantine entry.').setRequired(true).addChoices(...config.quarantineTypes.map(type => ({ name: type, value: type }))))
.addStringOption(option => option.setName('value').setDescription('The value of the quarantine entry. (User ID, Server ID, etc.)').setRequired(true))))

.toJSON(),
execute: async interaction => {
const subcommand = interaction.options.getSubcommand();
Expand Down Expand Up @@ -245,6 +261,216 @@ module.exports = {
return interaction.followUp({ content: 'Review deleted.' });
}
}

if (group === 'quarantine') {
if (config.permissions.canCreateQuarantinesRoles.some(roleId => !interaction.member.roles.cache.has(roleId))) return interaction.reply({ content: 'You don\'t have permission to use this command.' });

if (subcommand === 'create') {
await interaction.deferReply();

const type = interaction.options.getString('type');
const value = interaction.options.getString('value');
const restriction = interaction.options.getString('restriction');
const reason = interaction.options.getString('reason');
const time = interaction.options.getString('time');

const existingQuarantine = await Quarantine.findOne({ type, restriction, [type === 'USER_ID' ? 'user.id' : 'guild.id']: value });
if (existingQuarantine) return interaction.followUp({ content: `There is already a quarantine entry with the same values. ID: ${existingQuarantine._id}` });

const quarantineTime = time ? ms(time) : null;
if (time && typeof quarantineTime !== 'number') return interaction.followUp({ content: 'Invalid time.' });
if (quarantineTime && quarantineTime > 31556952000) return interaction.followUp({ content: 'The maximum quarantine time is 1 year.' });

const quarantine = new Quarantine({
type,
restriction,
reason,
created_by: interaction.user.id,
expire_at: quarantineTime ? new Date(Date.now() + quarantineTime) : null
});

if (type === 'USER_ID') quarantine.user = { id: value };
if (type === 'GUILD_ID') quarantine.guild = { id: value };

const validationErrors = quarantine.validateSync();
if (validationErrors) return interaction.followUp({ content: 'There was an error creating the quarantine entry. Most likely the provided values are invalid.' });

await quarantine.save();

const embeds = [
new Discord.EmbedBuilder()
.setAuthor({ name: `Quarantine #${quarantine._id} Created` })
.setColor(Discord.Colors.Purple)
.setTitle('New Quarantine Entry')
.setFields([
{
name: 'Entry Target',
value: `${value} (${type})`,
inline: true
},
{
name: 'Reason',
value: reason,
inline: true
},
{
name: 'Created By',
value: `<@${interaction.user.id}>`,
inline: true
},
{
name: 'Restriction',
value: restriction,
inline: true
}
])
.setFooter({ text: `Expires at: ${quarantineTime ? new Date(Date.now() + quarantineTime).toLocaleString() : 'Never'}` })
.setTimestamp(quarantineTime ? Date.now() + quarantineTime : null)
];

client.channels.cache.get(config.quarantineLogsChannelId).send({ embeds });

return interaction.followUp({ content: `Quarantine created. ID: ${quarantine._id}` });
}

if (subcommand === 'remove') {
await interaction.deferReply();

const entry = interaction.options.getString('entry');
const quarantine = await Quarantine.findOne({ _id: entry });
if (!quarantine) return interaction.followUp({ content: 'Quarantine not found.' });

await quarantine.deleteOne();

const embeds = [
new Discord.EmbedBuilder()
.setAuthor({ name: `Quarantine #${quarantine._id} Removed` })
.setColor(Discord.Colors.Purple)
.setTitle('Quarantine Entry Removed')
.setFields([
{
name: 'Entry Target',
value: `${quarantine.type === 'USER_ID' ? quarantine.user.id : quarantine.guild.id} (${quarantine.type})`,
inline: true
},
{
name: 'Reason',
value: quarantine.reason,
inline: true
},
{
name: 'Created By',
value: `<@${quarantine.created_by}>`,
inline: true
},
{
name: 'Restriction',
value: quarantine.restriction,
inline: true
}
])
.setFooter({ text: `${interaction.user.username} | Would expire at: ${quarantine.expire_at ? new Date(quarantine.expire_at).toLocaleString() : 'Never'}`, iconURL: interaction.user.displayAvatarURL() })
];

client.channels.cache.get(config.quarantineLogsChannelId).send({ embeds });

return interaction.followUp({ content: 'Quarantine removed.' });
}

if (subcommand === 'list') {
await interaction.deferReply();

const quarantines = await Quarantine.find();
if (!quarantines.length) return interaction.followUp({ content: 'There are no quarantine entries.' });

const perPage = 4;
const pages = Math.ceil(quarantines.length / perPage);
const embeds = [];
let currentPage = 1;

for (let i = 0; i < pages; i++) {
const filteredQuarantines = quarantines.slice(i * perPage, (i + 1) * perPage);
const formattedQuarantinesText = filteredQuarantines.map(quarantine => `- Quarantine #${quarantine._id}
- **Type:** ${quarantine.type}
- **Value:** ${quarantine.type === 'USER_ID' ? quarantine.user.id : quarantine.guild.id}
- **Restriction:** ${quarantine.restriction}
- **Reason:** ${quarantine.reason}
- **Created by:** <@${quarantine.created_by}>
- **Expires at:** ${quarantine.expire_at ? new Date(quarantine.expire_at).toLocaleString() : 'Never'}`).join('\n\n');

const embed = new Discord.EmbedBuilder()
.setAuthor({ name: `Quarantine List | Page ${currentPage} of ${pages}`, iconURL: interaction.guild.iconURL() })
.setColor('Random')
.setFooter({ text: interaction.user.username, iconURL: interaction.user.displayAvatarURL() })
.setTimestamp()
.setDescription(`Total Quarantines: ${quarantines.length}

${formattedQuarantinesText}`);

embeds.push(embed);
}

const components = [
new Discord.ActionRowBuilder()
.addComponents(
new Discord.ButtonBuilder()
.setCustomId('previous')
.setLabel('Previous Page')
.setStyle(Discord.ButtonStyle.Secondary),
new Discord.ButtonBuilder()
.setCustomId('next')
.setLabel('Next Page')
.setStyle(Discord.ButtonStyle.Secondary)
)
];

const message = await interaction.followUp({ embeds: [embeds[0]], components, fetchReply: true });
const collector = message.createMessageComponentCollector({ componentType: Discord.ComponentType.Button, time: 300000 });

collector.on('collect', async buttonInteraction => {
if (buttonInteraction.user.id !== interaction.user.id) return buttonInteraction.reply({ content: 'You are not allowed to interact with this button.', ephemeral: true });

if (buttonInteraction.customId === 'previous') {
if (currentPage === 1) return buttonInteraction.reply({ content: 'You are already on the first page.', ephemeral: true });

currentPage--;
await buttonInteraction.update({ embeds: [embeds[currentPage - 1]] });
}

if (buttonInteraction.customId === 'next') {
if (currentPage === pages) return buttonInteraction.reply({ content: 'You are already on the last page.', ephemeral: true });

currentPage++;
await buttonInteraction.update({ embeds: [embeds[currentPage - 1]] });
}
});

collector.on('end', () => message.edit({ content: 'This message is now inactive.', components: [] }));
}

if (subcommand === 'find') {
await interaction.deferReply();

const type = interaction.options.getString('type');
const value = interaction.options.getString('value');
const quarantine = await Quarantine.findOne({ type, [type === 'USER_ID' ? 'user.id' : 'guild.id']: value });
if (!quarantine) return interaction.followUp({ content: 'Quarantine not found.' });

const embed = new Discord.EmbedBuilder()
.setAuthor({ name: `Quarantine #${quarantine._id}`, iconURL: interaction.guild.iconURL() })
.setColor('Random')
.setFooter({ text: interaction.user.username, iconURL: interaction.user.displayAvatarURL() })
.setTimestamp()
.setDescription(`- **Type:** ${quarantine.type}
- **Value:** ${quarantine.type === 'USER_ID' ? quarantine.user.id : quarantine.guild.id}
- **Restriction:** ${quarantine.restriction}
- **Reason:** ${quarantine.reason}
- **Created by:** <@${quarantine.created_by}>
- **Expires at:** ${quarantine.expire_at ? new Date(quarantine.expire_at).toLocaleString() : 'Never'}`);

return interaction.followUp({ embeds: [embed] });
}
}
},
autocomplete: async interaction => {
const subcommand = interaction.options.getSubcommand();
Expand Down Expand Up @@ -304,5 +530,14 @@ module.exports = {
return interaction.customRespond(reviews.map(review => ({ name: `Review to ${client.guilds.cache.get(review.server.id).name} | User: ${review.user.id}`, value: review._id })));
}
}

if (group === 'quarantine') {
if (config.permissions.canCreateQuarantinesRoles.some(roleId => !interaction.member.roles.cache.has(roleId))) return;

if (subcommand === 'remove') {
const quarantines = await Quarantine.find();
return interaction.customRespond(quarantines.map(quarantine => ({ name: String(quarantine._id), value: quarantine._id })));
}
}
}
};
4 changes: 4 additions & 0 deletions server/src/bot/commands/emoji.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
const Discord = require('discord.js');
const Emoji = require('@/schemas/Emoji');
const getEmojiURL = require('@/utils/emojis/getEmojiURL');
const findQuarantineEntry = require('@/utils/findQuarantineEntry');

module.exports = {
data: new Discord.SlashCommandBuilder()
Expand All @@ -14,6 +15,9 @@ module.exports = {

await interaction.deferReply();

const userQuarantined = await findQuarantineEntry.single('USER_ID', interaction.user.id, 'EMOJIS_QUICKLY_UPLOAD').catch(() => false);
if (userQuarantined) return interaction.followUp({ content: 'You are not allowed to upload emojis.' });

const id = interaction.options.getString('emoji');
const emoji = await Emoji.findOne({ id });
if (!emoji) return interaction.followUp({ content: 'Emoji not found.' });
Expand Down
7 changes: 7 additions & 0 deletions server/src/bot/commands/vote.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const randomizeArray = require('@/src/utils/randomizeArray');
const incrementVote = require('@/src/utils/servers/incrementVote');
const VoteTimeout = require('@/schemas/Server/Vote/Timeout');
const VoteReminder = require('@/schemas/Server/Vote/Reminder');
const findQuarantineEntry = require('@/utils/findQuarantineEntry');

const emojis = ['🌟', '🍕', '🎉', '🚀', '🌈', '🎵', '🏝️', '📚', '🎭', '⚽', '🎲', '🍔', '🚲', '🖥️', '🎨', '🏆', '🔥', '💡', '🛸', '🐶', '🐱', '🐼', '🦁', '🐯', '🐵', '🐙', '🐢', '🐬', '🐳', '🦄', '🐝', '🐞', '🦋', '🐦', '🐧', '🐘', '🦏', '🦒', '🦓'];

Expand All @@ -16,6 +17,12 @@ module.exports = {
execute: async interaction => {
await interaction.deferReply({ ephemeral: true });

const userOrGuildQuarantined = await findQuarantineEntry.multiple([
{ type: 'USER_ID', value: interaction.user.id, restriction: 'SERVERS_VOTE' },
{ type: 'GUILD_ID', value: interaction.guild.id, restriction: 'SERVERS_VOTE' }
]).catch(() => false);
if (userOrGuildQuarantined) return interaction.followUp({ content: 'You are not allowed to vote for servers or this server is not allowed to receive votes.' });

const timeout = await VoteTimeout.findOne({ 'user.id': interaction.user.id, 'guild.id': interaction.guild.id });
if (timeout) return interaction.followUp(`You can vote again in ${Math.floor((timeout.createdAt.getTime() + 86400000 - Date.now()) / 3600000)} hours, ${Math.floor((timeout.createdAt.getTime() + 86400000 - Date.now()) / 60000) % 60} minutes.`);

Expand Down
4 changes: 4 additions & 0 deletions server/src/routes/emojis/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const Emoji = require('@/src/schemas/Emoji');
const EmojiPack = require('@/src/schemas/Emoji/Pack');
const crypto = require('node:crypto');
const Discord = require('discord.js');
const findQuarantineEntry = require('@/utils/findQuarantineEntry');

const multer = require('multer');
const upload = multer({
Expand Down Expand Up @@ -50,6 +51,9 @@ module.exports = {
const errors = validationResult(request);
if (!errors.isEmpty()) return response.sendError(errors.array()[0].msg, 400);

const userQuarantined = await findQuarantineEntry.single('USER_ID', request.user.id, 'EMOJIS_CREATE').catch(() => false);
if (userQuarantined) return response.sendError('You are not allowed to create emojis.', 403);

const userEmojiInQueue = await Emoji.findOne({ 'user.id': request.user.id, approved: false });
if (userEmojiInQueue) return response.sendError(`You are already waiting for approval for emoji ${userEmojiInQueue.name}! Please wait for it to be processed first.`);

Expand Down
Loading
Loading