const {
Client,
GatewayIntentBits,
Partials,
Collection,
EmbedBuilder,
PermissionsBitField,
AttachmentBuilder,
} = require('discord.js');
const axios = require('axios');
const fs = require('fs');
const path = require('path');
const { createCanvas, loadImage } = require('canvas');
const config = require('../config.json');
require('dotenv').config();
// Initialize Discord client
const client = new Client({
intents: [
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildMessages,
GatewayIntentBits.MessageContent,
GatewayIntentBits.GuildMembers,
GatewayIntentBits.GuildMessageReactions,
GatewayIntentBits.GuildBans,
],
partials: [
Partials.Channel,
Partials.Message,
Partials.User,
Partials.GuildMember,
Partials.Reaction,
],
});
// Initialize collections and properties
client.commands = new Collection();
client.aliases = new Collection();
client.slashCommands = new Collection();
client.buttons = new Collection();
client.prefix = config.prefix;
// Export client for use in other modules
module.exports = client;
// Load command handlers
fs.readdirSync('src/handlers').forEach((handler) => {
require(`./handlers/${handler}`)(client);
});
// Load users to skip
const doNotDetectTheseUsersId = fs
.readFileSync('configs/doNotDetectTheseUsersId.txt', 'utf-8')
.split('\n')
.map((id) => id.trim())
.filter((id) => id !== '');
const virusTotalApiKey = process.env.VIRUS_TOTAL_KEY;
const inviteRegex =
/(?:https?:\/\/)?(?:www\.)?(?:discord\.(gg|com|io|me|li|net)\/(?:invite\/)?[a-zA-Z0-9\-]+|discordapp\.com\/invite\/[a-zA-Z0-9\-]+)/;
const userMessageCounts = {};
const userMessages = {}; // Track all messages from a user
const spamThreshold = config.moderation.spam.threshold; // Number of messages allowed before blocking
const spamTimeFrame = config.moderation.spam.timeFrame; // Time frame for spam detection (5 seconds)
const blockDuration = config.moderation.spam.blockDuration; // Duration to block the user (1 minute)
const spamWarningSent = {}; // Track users who have already received a spam warning
const lastInviteWarning = {}; // Track last invite warning sent time
const inviteWarningCooldown = config.moderation.spam.warningCooldown; // Cooldown for invite warnings (10 seconds)
const lastBotMessageSent = {}; // Track last message sent time for each channel
const botMessageCooldown = config.moderation.spam.botMessageCooldown; // Cooldown for bot messages (15 seconds)
client.once('ready', async () => {
// Fetch the log channel
const logChannel = await client.channels.fetch(
config.moderation.channels.logChannelId,
);
if (logChannel) {
try {
// Check if a webhook already exists
const webhooks = await logChannel.fetchWebhooks();
let webhook = webhooks.find((wh) => wh.name === 'Moderation Logs');
if (!webhook) {
// Create a new webhook if it doesn't exist
webhook = await logChannel.createWebhook({
name: config.moderation.webhook.name,
avatar: config.moderation.webhook.avatar,
});
console.log('Webhook created successfully');
} else {
console.log('Webhook already exists');
}
// Store the webhook for later use
client.moderationWebhook = webhook;
} catch (error) {
console.error('Error handling webhook:', error);
}
} else {
console.error('Log channel not found');
}
});
client.on('messageCreate', async (message) => {
if (message.author.bot) return;
// Check if the user is in the doNotDetectTheseUsersId list
if (doNotDetectTheseUsersId.includes(message.author.id)) {
console.log(`Skipping message from whitelisted user: ${message.author.id}`);
return; // Exit the function early for whitelisted users
}
const userId = message.author.id;
const currentTime = Date.now();
// Initialize user tracking
if (!userMessages[userId]) userMessages[userId] = [];
if (!userMessageCounts[userId]) userMessageCounts[userId] = 0;
// Add current message to the list of messages for the user
userMessages[userId].push({
id: message.id,
timestamp: currentTime,
});
// Remove messages that are outside the time frame
userMessages[userId] = userMessages[userId].filter(
(msg) => currentTime - msg.timestamp <= spamTimeFrame,
);
// Update message count
userMessageCounts[userId] = userMessages[userId].length;
console.log(
`User ${userId} (${message.author.username}) message count: ${userMessageCounts[userId]}`,
);
// Check if user exceeds spam threshold within the time frame
if (userMessageCounts[userId] > spamThreshold) {
const timeDiff = currentTime - (userMessages[userId][0]?.timestamp || 0);
console.log(`Time difference: ${timeDiff}ms`);
if (timeDiff <= spamTimeFrame) {
const member = message.member;
// Check if the bot has permission to manage messages
if (
message.guild.members.me.permissions.has(
PermissionsBitField.Flags.ManageMessages,
)
) {
try {
// Send a single message if not already sent
if (!spamWarningSent[userId]) {
const spamWarningMessage = await message.channel.send(
`${message.author} has been blocked from sending messages due to spamming.`,
);
setTimeout(
() => spamWarningMessage.delete().catch(console.error),
5000,
);
spamWarningSent[userId] = true;
}
if (
member &&
member.roles.highest.position <=
message.guild.members.me.roles.highest.position
) {
if (
message.guild.members.me.permissions.has(
PermissionsBitField.Flags.ModerateMembers,
)
) {
await member.timeout(blockDuration, 'spamming');
} else {
console.error('Bot lacks permissions to timeout members.');
}
}
console.log(`User ${message.author.username} blocked for spamming.`);
} catch (error) {
if (error.code !== 50013) {
console.error('Error managing user:', error);
}
}
// Collect message IDs to delete
const messageIds = userMessages[userId].map((msg) => msg.id);
// Bulk delete messages
try {
await message.channel.bulkDelete(messageIds, true);
userMessages[userId] = []; // Clear tracked messages
userMessageCounts[userId] = 0; // Reset message counts
} catch (error) {
if (error.code === 10008) {
console.warn('Error deleting messages: Unknown Message. Skipping.');
} else {
console.error('Error deleting messages:', error);
}
}
// Clear message counts
userMessageCounts[userId] = 0;
} else {
console.error('Bot lacks permissions to manage messages.');
}
}
}
// Check if the message contains an invite link and handle accordingly
if (inviteRegex.test(message.content)) {
const lastWarningTime = lastInviteWarning[message.author.id] || 0;
const timeSinceLastWarning = currentTime - lastWarningTime;
if (timeSinceLastWarning >= inviteWarningCooldown) {
if (
!message.guild.members.me.permissions.has(
PermissionsBitField.Flags.ManageMessages,
)
) {
console.error('Missing permissions to manage messages.');
return;
}
try {
// Channel specific cooldown check
const channelId = message.channel.id;
const lastMessageTime = lastBotMessageSent[channelId] || 0;
const timeSinceLastMessage = currentTime - lastMessageTime;
if (timeSinceLastMessage >= botMessageCooldown) {
await message.delete();
const warningMessage = await message.channel.send(
`${message.author}, posting invite links is not allowed!`,
);
setTimeout(() => warningMessage.delete().catch(console.error), 5000);
// Update last warning time and last bot message sent time
lastInviteWarning[message.author.id] = currentTime;
lastBotMessageSent[channelId] = currentTime;
} else {
console.log('Bot message suppressed due to cooldown.');
}
} catch (error) {
console.error('Error handling message:', error);
}
}
}
const urlRegex = /(https?:\/\/[^\s]+)/g;
const urls = message.content.match(urlRegex);
if (urls) {
for (const url of urls) {
try {
const encodedUrl = Buffer.from(url)
.toString('base64')
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/, '');
console.log('Original URL:', url);
console.log('Encoded URL:', encodedUrl);
const response = await axios.get(
`https://www.virustotal.com/api/v3/urls/${encodedUrl}`,
{
headers: { 'x-apikey': virusTotalApiKey },
},
);
if (response.data.data.attributes.last_analysis_stats.malicious > 0) {
await message.delete();
await message.channel.send(
`${message.author}, your message contained a malicious link and was deleted.`,
);
if (client.moderationWebhook) {
const embed = new EmbedBuilder()
.setAuthor({
name: message.author.username,
iconURL: message.author.displayAvatarURL(),
})
.setTitle('Malicious Link Detected!')
.setColor('#FF0000')
.addFields(
{
name: 'User',
value: `${message.author.tag} (${message.author.id})`,
inline: true,
},
{ name: 'Message', value: `\`\`\`${message.content}\`\`\`` },
{ name: 'Link', value: `\`\`\`${url}\`\`\`` },
{ name: 'Timestamp', value: new Date().toISOString() },
)
.setFooter({
text: client.user.tag,
iconURL: client.user.displayAvatarURL(),
});
await client.moderationWebhook.send({ embeds: [embed] });
}
break;
}
} catch (error) {
if (
error.response &&
error.response.data.error.code === 'NotFoundError'
) {
console.log('URL not found in VirusTotal database:', url);
} else {
console.error(
'Error checking URL with VirusTotal API:',
error.response ? error.response.data : error.message,
);
}
}
}
}
});
const sharp = require('sharp');
client.on('guildMemberAdd', async (member) => {
try {
const guild = member.guild;
// Welcome message in a specified channel
const welcomeChannel = client.channels.cache.get(
config.moderation.channels.welcomeChannelId,
);
if (!welcomeChannel) {
throw new Error('Welcome channel not found.');
}
// Load the background image for the welcome message
const imagePath = path.resolve(__dirname, '../images/red.png');
console.log('Loading image from path:', imagePath);
let background;
try {
background = await loadImage(imagePath);
console.log('Image loaded successfully');
} catch (error) {
console.error('Error loading image:', error.message);
return; // Exit the function if the image fails to load
}
const canvas = createCanvas(1280, 720);
const ctx = canvas.getContext('2d');
ctx.drawImage(background, 0, 0, canvas.width, canvas.height);
// Fetch the user's avatar in PNG format
const defaultAvatarUrl = 'https://cdn.discordapp.com/embed/avatars/0.png'; // Default Discord avatar
const avatarUrl =
member.user.displayAvatarURL({ format: 'png', size: 2048 }) ||
defaultAvatarUrl;
// Force PNG format by appending `.png` to the URL
const pngAvatarUrl = avatarUrl.replace(/\.webp$/, '.png');
console.log('Avatar URL:', pngAvatarUrl); // Log the modified avatar URL
if (!pngAvatarUrl) {
throw new Error('Failed to fetch user avatar URL.');
}
let avatarBuffer;
try {
const avatarResponse = await axios.get(pngAvatarUrl, {
responseType: 'arraybuffer',
});
avatarBuffer = Buffer.from(avatarResponse.data);
// Log the avatar buffer size
console.log('Avatar buffer size:', avatarBuffer.length);
// Verify the sharp instance
if (!sharp) {
throw new Error('Sharp is not initialized correctly.');
}
// Use sharp to process the avatar
const processedAvatar = await sharp(avatarBuffer)
.resize(345, 345) // Resize to fit the circle
.toFormat('png') // Ensure output is in PNG format
.toBuffer();
// Log the processed avatar buffer size
console.log('Processed avatar buffer size:', processedAvatar.length);
// Load the processed avatar into canvas
const avatar = await loadImage(processedAvatar);
// Draw the avatar with stroke
const circleCenterX = 640; // X center of the circle
const circleCenterY = 485; // Y center of the circle
const circleRadius = 150; // Radius of the circle
const avatarSize = 345; // Size of the avatar
// Draw the stroke
ctx.save();
ctx.lineWidth = 10; // Thickness of the stroke
ctx.strokeStyle = config.colors.red; // Color of the stroke
ctx.beginPath();
ctx.arc(
circleCenterX,
circleCenterY,
circleRadius + 5,
0,
Math.PI * 2,
false,
);
ctx.closePath();
ctx.stroke();
ctx.restore();
ctx.save();
// Draw the circular clipping path for the avatar
ctx.beginPath();
ctx.arc(circleCenterX, circleCenterY, circleRadius, 0, Math.PI * 2, true);
ctx.closePath();
ctx.clip();
// Center the avatar in the circle
const avatarX = circleCenterX - avatarSize / 2;
const avatarY = circleCenterY - avatarSize / 2;
// Draw the avatar
ctx.drawImage(avatar, avatarX, avatarY, avatarSize, avatarSize);
ctx.restore();
// Add text to the image
ctx.font = 'bold 76px Arial'; // Use Arial instead of Mont
ctx.fillStyle = 'red';
ctx.textAlign = 'center';
ctx.shadowColor = config.colors.red;
ctx.shadowBlur = 8;
ctx.fillText(
`Welcome ${member.user.username || 'Guest'}`,
canvas.width / 2,
160,
);
ctx.font = 'bold 58px Arial'; // Use Arial instead of Mont
ctx.fillText(`To ${guild.name || 'Server'}`, canvas.width / 2, 240);
ctx.font = 'bold 38px Arial'; // Use Arial instead of Mont
ctx.fillText(
`You are member number ${guild.memberCount}`,
canvas.width / 2,
300,
);
// Convert canvas to buffer
const buffer = canvas.toBuffer('image/png');
if (!buffer || buffer.length === 0) {
throw new Error('Failed to create buffer from canvas.');
}
// Log the buffer size
console.log('Canvas buffer size:', buffer.length);
// Save the buffer to a temporary file
const tempFilePath = path.resolve(__dirname, `welcome-${member.id}.png`);
fs.writeFileSync(tempFilePath, buffer);
// Create an attachment using the temporary file
const attachment = new AttachmentBuilder(tempFilePath);
// Log the attachment details
console.log('Attachment created:', attachment);
// Send welcome message with the image
await welcomeChannel.send({
content: `** :wave: Hello ${member}, Welcome To ${guild.name}! **`,
files: [attachment],
});
// Delete the temporary file after sending
fs.unlinkSync(tempFilePath);
// Optionally remove the new role (e.g., if it's a verification role)
const newRoleId = '1260326684763099289'; // Update to the role you want to remove
const roleToRemove = guild.roles.cache.get(newRoleId);
if (roleToRemove) {
await member.roles.remove(roleToRemove);
}
} catch (error) {
console.error('Error processing avatar:', error);
return; // Exit if the avatar fails to load
}
} catch (error) {
console.error('Error handling guildMemberAdd event:', error);
}
});
client.login(process.env.TOKEN);