Add new commands and improve existing ones for better user experience

Introduces new commands like `/automod`, `/giveaway`, `/rolepanel`, and `/schedule`. Enhances existing commands such as `/announce`, `/help`, `/leaderboard`, `/profile`, and `/serverinfo` with new features and improved embed designs. Updates welcome and goodbye listeners with rich embeds. Fixes a critical issue in the `/rolepanel` command regarding channel fetching. Adds interaction handling for role buttons and giveaway entries.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: aed2e46d-25bb-4b73-81a1-bb9e8437c261
Replit-Commit-Checkpoint-Type: intermediate_checkpoint
Replit-Commit-Event-Id: eefee140-1301-4b6f-9439-2b0b883aa40a
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/3bdfff67-975a-46ad-9845-fbb6b4a4c4b5/aed2e46d-25bb-4b73-81a1-bb9e8437c261/qAaysIh
Replit-Helium-Checkpoint-Created: true
This commit is contained in:
sirpiglr 2025-12-08 07:24:49 +00:00
parent cbfc697853
commit 6f5c37959f
15 changed files with 2590 additions and 122 deletions

View file

@ -23,7 +23,7 @@ localPort = 8080
externalPort = 8080
[[ports]]
localPort = 46469
localPort = 45017
externalPort = 3000
[workflows]

View file

@ -867,6 +867,142 @@ client.on("interactionCreate", async (interaction) => {
}
}
}
if (action === 'role') {
const roleId = params.join('_');
try {
const role = interaction.guild.roles.cache.get(roleId);
if (!role) {
return interaction.reply({ content: 'This role no longer exists.', ephemeral: true });
}
const member = interaction.member;
if (member.roles.cache.has(roleId)) {
await member.roles.remove(roleId);
await interaction.reply({ content: `Removed ${role} from you!`, ephemeral: true });
} else {
await member.roles.add(roleId);
await interaction.reply({ content: `Added ${role} to you!`, ephemeral: true });
}
} catch (err) {
console.error('Role button error:', err);
await interaction.reply({ content: 'Failed to toggle role. Check bot permissions.', ephemeral: true }).catch(() => {});
}
}
if (action === 'giveaway') {
const giveawayAction = params[0];
if (giveawayAction === 'enter') {
try {
const messageId = interaction.message.id;
let giveawayData = client.giveaways?.get(messageId);
let entries = giveawayData?.entries || [];
if (supabase) {
const { data } = await supabase
.from('giveaways')
.select('*')
.eq('message_id', messageId)
.single();
if (data) {
entries = data.entries || [];
giveawayData = data;
}
}
if (!giveawayData) {
return interaction.reply({ content: 'This giveaway is no longer active.', ephemeral: true });
}
if (giveawayData.required_role) {
const hasRole = interaction.member.roles.cache.has(giveawayData.required_role);
if (!hasRole) {
return interaction.reply({ content: `You need the <@&${giveawayData.required_role}> role to enter!`, ephemeral: true });
}
}
if (entries.includes(interaction.user.id)) {
return interaction.reply({ content: 'You have already entered this giveaway!', ephemeral: true });
}
entries.push(interaction.user.id);
if (client.giveaways?.has(messageId)) {
client.giveaways.get(messageId).entries = entries;
}
if (supabase) {
await supabase
.from('giveaways')
.update({ entries: entries })
.eq('message_id', messageId);
}
const embed = EmbedBuilder.from(interaction.message.embeds[0]);
const entriesField = embed.data.fields?.find(f => f.name.includes('Entries'));
if (entriesField) {
entriesField.value = `${entries.length}`;
}
await interaction.message.edit({ embeds: [embed] });
await interaction.reply({ content: `You have entered the giveaway! Total entries: ${entries.length}`, ephemeral: true });
} catch (err) {
console.error('Giveaway entry error:', err);
await interaction.reply({ content: 'Failed to enter giveaway.', ephemeral: true }).catch(() => {});
}
}
}
}
if (interaction.isModalSubmit()) {
if (interaction.customId.startsWith('embed_modal_')) {
try {
const parts = interaction.customId.split('_');
const channelId = parts[2];
const color = parseInt(parts[3], 16);
const title = interaction.fields.getTextInputValue('embed_title');
const description = interaction.fields.getTextInputValue('embed_description');
const imageUrl = interaction.fields.getTextInputValue('embed_image') || null;
const thumbnailUrl = interaction.fields.getTextInputValue('embed_thumbnail') || null;
const footerText = interaction.fields.getTextInputValue('embed_footer') || null;
const embed = new EmbedBuilder()
.setColor(color)
.setTitle(title)
.setDescription(description)
.setTimestamp();
if (imageUrl) embed.setImage(imageUrl);
if (thumbnailUrl) embed.setThumbnail(thumbnailUrl);
if (footerText) embed.setFooter({ text: footerText });
const channel = await client.channels.fetch(channelId);
await channel.send({ embeds: [embed] });
await interaction.reply({ content: `Embed sent to <#${channelId}>!`, ephemeral: true });
} catch (err) {
console.error('Embed modal error:', err);
await interaction.reply({ content: 'Failed to send embed. Check permissions and URLs.', ephemeral: true }).catch(() => {});
}
}
}
if (interaction.isStringSelectMenu()) {
if (interaction.customId === 'help_category') {
try {
const category = interaction.values[0];
const { getCategoryEmbed } = require('./commands/help');
const embed = getCategoryEmbed(category);
await interaction.update({ embeds: [embed] });
} catch (err) {
console.error('Help select error:', err);
}
}
}
});

View file

@ -18,17 +18,23 @@ module.exports = {
.setMaxLength(2000)
)
.addStringOption(option =>
option.setName('color')
.setDescription('Embed color')
option.setName('type')
.setDescription('Announcement type (changes color and style)')
.setRequired(false)
.addChoices(
{ name: 'Purple (Default)', value: '7c3aed' },
{ name: 'Green (Success)', value: '00ff00' },
{ name: 'Red (Alert)', value: 'ff0000' },
{ name: 'Blue (Info)', value: '3b82f6' },
{ name: 'Yellow (Warning)', value: 'fbbf24' }
{ name: '📢 General (Purple)', value: 'general' },
{ name: '🆕 Update (Green)', value: 'update' },
{ name: '🎉 Event (Orange)', value: 'event' },
{ name: '⚠️ Important (Red)', value: 'important' },
{ name: '⭐ Highlight (Gold)', value: 'highlight' },
{ name: '💡 Info (Blue)', value: 'info' }
)
)
.addStringOption(option =>
option.setName('image')
.setDescription('Image URL to include')
.setRequired(false)
)
.addBooleanOption(option =>
option.setName('ping')
.setDescription('Ping @everyone with this announcement')
@ -40,16 +46,39 @@ module.exports = {
const title = interaction.options.getString('title');
const message = interaction.options.getString('message');
const color = parseInt(interaction.options.getString('color') || '7c3aed', 16);
const type = interaction.options.getString('type') || 'general';
const imageUrl = interaction.options.getString('image');
const ping = interaction.options.getBoolean('ping') || false;
const typeConfig = {
general: { color: 0x7c3aed, emoji: '📢', label: 'Announcement' },
update: { color: 0x22c55e, emoji: '🆕', label: 'Update' },
event: { color: 0xf97316, emoji: '🎉', label: 'Event' },
important: { color: 0xef4444, emoji: '⚠️', label: 'Important' },
highlight: { color: 0xfbbf24, emoji: '⭐', label: 'Highlight' },
info: { color: 0x3b82f6, emoji: '💡', label: 'Info' }
};
const config = typeConfig[type];
const embed = new EmbedBuilder()
.setColor(color)
.setColor(config.color)
.setAuthor({
name: `${config.emoji} ${config.label}`,
iconURL: interaction.guild.iconURL({ size: 64 })
})
.setTitle(title)
.setDescription(message)
.setFooter({ text: `Announced by ${interaction.user.tag}` })
.setFooter({
text: `Announced by ${interaction.user.tag}${interaction.guild.name}`,
iconURL: interaction.user.displayAvatarURL({ size: 32 })
})
.setTimestamp();
if (imageUrl) {
embed.setImage(imageUrl);
}
const results = [];
const REALM_GUILDS = client.REALM_GUILDS;
@ -58,7 +87,7 @@ module.exports = {
const guild = client.guilds.cache.get(guildId);
if (!guild) {
results.push({ realm, status: 'offline' });
results.push({ realm, status: 'offline', emoji: '⚫' });
continue;
}
@ -71,13 +100,13 @@ module.exports = {
if (announcementChannel && announcementChannel.isTextBased()) {
const content = ping ? '@everyone' : null;
await announcementChannel.send({ content, embeds: [embed] });
results.push({ realm, status: 'sent', channel: announcementChannel.name });
results.push({ realm, status: 'sent', channel: announcementChannel.name, emoji: '✅' });
} else {
results.push({ realm, status: 'no_channel' });
results.push({ realm, status: 'no_channel', emoji: '⚠️' });
}
} catch (error) {
console.error(`Announce error for ${realm}:`, error);
results.push({ realm, status: 'error', error: error.message });
results.push({ realm, status: 'error', error: error.message, emoji: '❌' });
}
}
@ -88,24 +117,39 @@ module.exports = {
user_id: interaction.user.id,
username: interaction.user.tag,
guild_id: interaction.guildId,
details: { title, message, results },
details: { title, message, type, results },
});
} catch (e) {
console.warn('Failed to log announcement:', e.message);
}
}
const resultText = results.map(r => {
const emoji = r.status === 'sent' ? '✅' : r.status === 'offline' ? '⚫' : '❌';
return `${emoji} **${r.realm}**: ${r.status}${r.channel ? ` (#${r.channel})` : ''}`;
}).join('\n');
const successCount = results.filter(r => r.status === 'sent').length;
const failCount = results.filter(r => r.status !== 'sent' && r.status !== 'offline').length;
const resultEmbed = new EmbedBuilder()
.setColor(0x7c3aed)
.setTitle('Announcement Results')
.setDescription(resultText || 'No realms configured')
.setColor(successCount > 0 ? 0x22c55e : 0xef4444)
.setTitle(`${config.emoji} Announcement Results`)
.setDescription(
results.length > 0
? results.map(r =>
`${r.emoji} **${capitalizeFirst(r.realm)}**: ${r.status}${r.channel ? ` (#${r.channel})` : ''}`
).join('\n')
: 'No realms configured'
)
.addFields({
name: '📊 Summary',
value: `✅ Sent: ${successCount} | ⚠️ Failed: ${failCount} | ⚫ Offline: ${results.filter(r => r.status === 'offline').length}`,
inline: false
})
.setFooter({ text: `Type: ${config.label}` })
.setTimestamp();
await interaction.editReply({ embeds: [resultEmbed] });
},
};
function capitalizeFirst(str) {
if (!str) return str;
return str.charAt(0).toUpperCase() + str.slice(1);
}

View file

@ -0,0 +1,424 @@
const {
SlashCommandBuilder,
EmbedBuilder,
PermissionFlagsBits
} = require('discord.js');
module.exports = {
data: new SlashCommandBuilder()
.setName('automod')
.setDescription('Configure auto-moderation settings')
.setDefaultMemberPermissions(PermissionFlagsBits.ManageGuild)
.addSubcommand(sub =>
sub.setName('status')
.setDescription('View current auto-mod settings')
)
.addSubcommand(sub =>
sub.setName('links')
.setDescription('Toggle link filtering')
.addBooleanOption(option =>
option.setName('enabled')
.setDescription('Enable or disable link filtering')
.setRequired(true)
)
.addStringOption(option =>
option.setName('action')
.setDescription('Action to take')
.setRequired(false)
.addChoices(
{ name: 'Delete only', value: 'delete' },
{ name: 'Delete + Warn', value: 'warn' },
{ name: 'Delete + Timeout (5min)', value: 'timeout' }
)
)
)
.addSubcommand(sub =>
sub.setName('spam')
.setDescription('Toggle spam detection')
.addBooleanOption(option =>
option.setName('enabled')
.setDescription('Enable or disable spam detection')
.setRequired(true)
)
.addIntegerOption(option =>
option.setName('threshold')
.setDescription('Messages per 5 seconds to trigger')
.setRequired(false)
.setMinValue(3)
.setMaxValue(20)
)
)
.addSubcommand(sub =>
sub.setName('badwords')
.setDescription('Manage banned words')
.addStringOption(option =>
option.setName('action')
.setDescription('Add or remove words')
.setRequired(true)
.addChoices(
{ name: 'Add word', value: 'add' },
{ name: 'Remove word', value: 'remove' },
{ name: 'List words', value: 'list' },
{ name: 'Clear all', value: 'clear' }
)
)
.addStringOption(option =>
option.setName('word')
.setDescription('Word to add/remove')
.setRequired(false)
)
)
.addSubcommand(sub =>
sub.setName('invites')
.setDescription('Toggle Discord invite filtering')
.addBooleanOption(option =>
option.setName('enabled')
.setDescription('Enable or disable invite filtering')
.setRequired(true)
)
)
.addSubcommand(sub =>
sub.setName('mentions')
.setDescription('Toggle mass mention detection')
.addBooleanOption(option =>
option.setName('enabled')
.setDescription('Enable or disable mention spam detection')
.setRequired(true)
)
.addIntegerOption(option =>
option.setName('limit')
.setDescription('Maximum mentions per message')
.setRequired(false)
.setMinValue(3)
.setMaxValue(50)
)
)
.addSubcommand(sub =>
sub.setName('exempt')
.setDescription('Exempt a role from auto-mod')
.addRoleOption(option =>
option.setName('role')
.setDescription('Role to exempt')
.setRequired(true)
)
.addBooleanOption(option =>
option.setName('exempt')
.setDescription('Exempt or un-exempt')
.setRequired(true)
)
),
async execute(interaction, supabase, client) {
const subcommand = interaction.options.getSubcommand();
switch (subcommand) {
case 'status':
await handleStatus(interaction, supabase);
break;
case 'links':
await handleLinks(interaction, supabase, client);
break;
case 'spam':
await handleSpam(interaction, supabase, client);
break;
case 'badwords':
await handleBadwords(interaction, supabase, client);
break;
case 'invites':
await handleInvites(interaction, supabase, client);
break;
case 'mentions':
await handleMentions(interaction, supabase, client);
break;
case 'exempt':
await handleExempt(interaction, supabase, client);
break;
}
},
};
async function getAutomodConfig(guildId, supabase) {
if (!supabase) return getDefaultConfig();
try {
const { data, error } = await supabase
.from('automod_config')
.select('*')
.eq('guild_id', guildId)
.single();
if (error || !data) return getDefaultConfig();
return data;
} catch {
return getDefaultConfig();
}
}
function getDefaultConfig() {
return {
links_enabled: false,
links_action: 'delete',
spam_enabled: false,
spam_threshold: 5,
badwords: [],
invites_enabled: false,
mentions_enabled: false,
mentions_limit: 5,
exempt_roles: []
};
}
async function saveAutomodConfig(guildId, config, supabase) {
if (!supabase) return;
try {
await supabase.from('automod_config').upsert({
guild_id: guildId,
...config,
updated_at: new Date().toISOString()
});
} catch (error) {
console.error('Save automod config error:', error);
}
}
async function handleStatus(interaction, supabase) {
await interaction.deferReply({ ephemeral: true });
const config = await getAutomodConfig(interaction.guildId, supabase);
const embed = new EmbedBuilder()
.setColor(0x7c3aed)
.setTitle('🛡️ Auto-Moderation Settings')
.addFields(
{
name: '🔗 Link Filter',
value: config.links_enabled ? `✅ Enabled (${config.links_action})` : '❌ Disabled',
inline: true
},
{
name: '📨 Spam Detection',
value: config.spam_enabled ? `✅ Enabled (${config.spam_threshold} msg/5s)` : '❌ Disabled',
inline: true
},
{
name: '🚫 Bad Words',
value: config.badwords?.length > 0 ? `${config.badwords.length} words` : '❌ None set',
inline: true
},
{
name: '📩 Invite Filter',
value: config.invites_enabled ? '✅ Enabled' : '❌ Disabled',
inline: true
},
{
name: '📢 Mass Mentions',
value: config.mentions_enabled ? `✅ Enabled (max ${config.mentions_limit})` : '❌ Disabled',
inline: true
},
{
name: '🎭 Exempt Roles',
value: config.exempt_roles?.length > 0
? config.exempt_roles.map(r => `<@&${r}>`).join(', ')
: 'None',
inline: true
}
)
.setFooter({ text: 'Use /automod [setting] to configure' })
.setTimestamp();
await interaction.editReply({ embeds: [embed] });
}
async function handleLinks(interaction, supabase, client) {
await interaction.deferReply({ ephemeral: true });
const enabled = interaction.options.getBoolean('enabled');
const action = interaction.options.getString('action') || 'delete';
const config = await getAutomodConfig(interaction.guildId, supabase);
config.links_enabled = enabled;
config.links_action = action;
await saveAutomodConfig(interaction.guildId, config, supabase);
client.automodConfig = client.automodConfig || new Map();
client.automodConfig.set(interaction.guildId, config);
const embed = new EmbedBuilder()
.setColor(enabled ? 0x22c55e : 0xef4444)
.setTitle(enabled ? '✅ Link Filter Enabled' : '❌ Link Filter Disabled')
.setDescription(enabled ? `Links will be ${action === 'delete' ? 'deleted' : action === 'warn' ? 'deleted and user warned' : 'deleted and user timed out'}` : 'Links will no longer be filtered')
.setTimestamp();
await interaction.editReply({ embeds: [embed] });
}
async function handleSpam(interaction, supabase, client) {
await interaction.deferReply({ ephemeral: true });
const enabled = interaction.options.getBoolean('enabled');
const threshold = interaction.options.getInteger('threshold') || 5;
const config = await getAutomodConfig(interaction.guildId, supabase);
config.spam_enabled = enabled;
config.spam_threshold = threshold;
await saveAutomodConfig(interaction.guildId, config, supabase);
client.automodConfig = client.automodConfig || new Map();
client.automodConfig.set(interaction.guildId, config);
const embed = new EmbedBuilder()
.setColor(enabled ? 0x22c55e : 0xef4444)
.setTitle(enabled ? '✅ Spam Detection Enabled' : '❌ Spam Detection Disabled')
.setDescription(enabled ? `Spam threshold set to ${threshold} messages per 5 seconds` : 'Spam will no longer be detected')
.setTimestamp();
await interaction.editReply({ embeds: [embed] });
}
async function handleBadwords(interaction, supabase, client) {
await interaction.deferReply({ ephemeral: true });
const action = interaction.options.getString('action');
const word = interaction.options.getString('word')?.toLowerCase();
const config = await getAutomodConfig(interaction.guildId, supabase);
config.badwords = config.badwords || [];
let embed;
switch (action) {
case 'add':
if (!word) {
return interaction.editReply({ content: 'Please provide a word to add.' });
}
if (!config.badwords.includes(word)) {
config.badwords.push(word);
}
embed = new EmbedBuilder()
.setColor(0x22c55e)
.setTitle('✅ Word Added')
.setDescription(`Added "${word}" to the filter list.`)
.setTimestamp();
break;
case 'remove':
if (!word) {
return interaction.editReply({ content: 'Please provide a word to remove.' });
}
config.badwords = config.badwords.filter(w => w !== word);
embed = new EmbedBuilder()
.setColor(0x22c55e)
.setTitle('✅ Word Removed')
.setDescription(`Removed "${word}" from the filter list.`)
.setTimestamp();
break;
case 'list':
embed = new EmbedBuilder()
.setColor(0x7c3aed)
.setTitle('🚫 Banned Words')
.setDescription(config.badwords.length > 0
? config.badwords.map(w => `\`${w}\``).join(', ')
: 'No words in the filter list.')
.setTimestamp();
break;
case 'clear':
config.badwords = [];
embed = new EmbedBuilder()
.setColor(0x22c55e)
.setTitle('✅ List Cleared')
.setDescription('All banned words have been removed.')
.setTimestamp();
break;
}
await saveAutomodConfig(interaction.guildId, config, supabase);
client.automodConfig = client.automodConfig || new Map();
client.automodConfig.set(interaction.guildId, config);
await interaction.editReply({ embeds: [embed] });
}
async function handleInvites(interaction, supabase, client) {
await interaction.deferReply({ ephemeral: true });
const enabled = interaction.options.getBoolean('enabled');
const config = await getAutomodConfig(interaction.guildId, supabase);
config.invites_enabled = enabled;
await saveAutomodConfig(interaction.guildId, config, supabase);
client.automodConfig = client.automodConfig || new Map();
client.automodConfig.set(interaction.guildId, config);
const embed = new EmbedBuilder()
.setColor(enabled ? 0x22c55e : 0xef4444)
.setTitle(enabled ? '✅ Invite Filter Enabled' : '❌ Invite Filter Disabled')
.setDescription(enabled ? 'Discord invites will be automatically deleted' : 'Discord invites will no longer be filtered')
.setTimestamp();
await interaction.editReply({ embeds: [embed] });
}
async function handleMentions(interaction, supabase, client) {
await interaction.deferReply({ ephemeral: true });
const enabled = interaction.options.getBoolean('enabled');
const limit = interaction.options.getInteger('limit') || 5;
const config = await getAutomodConfig(interaction.guildId, supabase);
config.mentions_enabled = enabled;
config.mentions_limit = limit;
await saveAutomodConfig(interaction.guildId, config, supabase);
client.automodConfig = client.automodConfig || new Map();
client.automodConfig.set(interaction.guildId, config);
const embed = new EmbedBuilder()
.setColor(enabled ? 0x22c55e : 0xef4444)
.setTitle(enabled ? '✅ Mass Mention Detection Enabled' : '❌ Mass Mention Detection Disabled')
.setDescription(enabled ? `Messages with more than ${limit} mentions will be deleted` : 'Mass mentions will no longer be filtered')
.setTimestamp();
await interaction.editReply({ embeds: [embed] });
}
async function handleExempt(interaction, supabase, client) {
await interaction.deferReply({ ephemeral: true });
const role = interaction.options.getRole('role');
const exempt = interaction.options.getBoolean('exempt');
const config = await getAutomodConfig(interaction.guildId, supabase);
config.exempt_roles = config.exempt_roles || [];
if (exempt) {
if (!config.exempt_roles.includes(role.id)) {
config.exempt_roles.push(role.id);
}
} else {
config.exempt_roles = config.exempt_roles.filter(r => r !== role.id);
}
await saveAutomodConfig(interaction.guildId, config, supabase);
client.automodConfig = client.automodConfig || new Map();
client.automodConfig.set(interaction.guildId, config);
const embed = new EmbedBuilder()
.setColor(0x22c55e)
.setTitle(exempt ? '✅ Role Exempted' : '✅ Role Un-exempted')
.setDescription(exempt
? `${role} is now exempt from auto-moderation`
: `${role} is no longer exempt from auto-moderation`)
.setTimestamp();
await interaction.editReply({ embeds: [embed] });
}

View file

@ -0,0 +1,97 @@
const {
SlashCommandBuilder,
EmbedBuilder,
PermissionFlagsBits,
ModalBuilder,
TextInputBuilder,
TextInputStyle,
ActionRowBuilder,
ChannelType
} = require('discord.js');
module.exports = {
data: new SlashCommandBuilder()
.setName('embed')
.setDescription('Create a custom embed message')
.setDefaultMemberPermissions(PermissionFlagsBits.ManageMessages)
.addChannelOption(option =>
option.setName('channel')
.setDescription('Channel to send the embed to')
.setRequired(true)
.addChannelTypes(ChannelType.GuildText, ChannelType.GuildAnnouncement)
)
.addStringOption(option =>
option.setName('color')
.setDescription('Embed color')
.setRequired(false)
.addChoices(
{ name: '🟣 Purple (Default)', value: '7c3aed' },
{ name: '🟢 Green (Success)', value: '22c55e' },
{ name: '🔴 Red (Alert)', value: 'ef4444' },
{ name: '🔵 Blue (Info)', value: '3b82f6' },
{ name: '🟡 Yellow (Warning)', value: 'eab308' },
{ name: '🟠 Orange (Highlight)', value: 'f97316' },
{ name: '⚪ White', value: 'ffffff' },
{ name: '⚫ Black', value: '1f2937' },
{ name: '🩷 Pink', value: 'ec4899' },
{ name: '🩵 Cyan', value: '06b6d4' }
)
),
async execute(interaction, supabase, client) {
const channel = interaction.options.getChannel('channel');
const color = interaction.options.getString('color') || '7c3aed';
const modal = new ModalBuilder()
.setCustomId(`embed_modal_${channel.id}_${color}`)
.setTitle('Create Custom Embed');
const titleInput = new TextInputBuilder()
.setCustomId('embed_title')
.setLabel('Title')
.setStyle(TextInputStyle.Short)
.setPlaceholder('Enter embed title')
.setMaxLength(256)
.setRequired(true);
const descriptionInput = new TextInputBuilder()
.setCustomId('embed_description')
.setLabel('Description')
.setStyle(TextInputStyle.Paragraph)
.setPlaceholder('Enter embed description (supports markdown)')
.setMaxLength(4000)
.setRequired(true);
const imageInput = new TextInputBuilder()
.setCustomId('embed_image')
.setLabel('Image URL (optional)')
.setStyle(TextInputStyle.Short)
.setPlaceholder('https://example.com/image.png')
.setRequired(false);
const thumbnailInput = new TextInputBuilder()
.setCustomId('embed_thumbnail')
.setLabel('Thumbnail URL (optional)')
.setStyle(TextInputStyle.Short)
.setPlaceholder('https://example.com/thumbnail.png')
.setRequired(false);
const footerInput = new TextInputBuilder()
.setCustomId('embed_footer')
.setLabel('Footer text (optional)')
.setStyle(TextInputStyle.Short)
.setPlaceholder('Enter footer text')
.setMaxLength(2048)
.setRequired(false);
const row1 = new ActionRowBuilder().addComponents(titleInput);
const row2 = new ActionRowBuilder().addComponents(descriptionInput);
const row3 = new ActionRowBuilder().addComponents(imageInput);
const row4 = new ActionRowBuilder().addComponents(thumbnailInput);
const row5 = new ActionRowBuilder().addComponents(footerInput);
modal.addComponents(row1, row2, row3, row4, row5);
await interaction.showModal(modal);
},
};

View file

@ -0,0 +1,326 @@
const {
SlashCommandBuilder,
EmbedBuilder,
PermissionFlagsBits,
ActionRowBuilder,
ButtonBuilder,
ButtonStyle
} = require('discord.js');
module.exports = {
data: new SlashCommandBuilder()
.setName('giveaway')
.setDescription('Create and manage giveaways')
.setDefaultMemberPermissions(PermissionFlagsBits.ManageMessages)
.addSubcommand(sub =>
sub.setName('start')
.setDescription('Start a new giveaway')
.addStringOption(option =>
option.setName('prize')
.setDescription('What are you giving away?')
.setRequired(true)
.setMaxLength(256)
)
.addIntegerOption(option =>
option.setName('duration')
.setDescription('Duration in minutes')
.setRequired(true)
.setMinValue(1)
.setMaxValue(10080)
)
.addIntegerOption(option =>
option.setName('winners')
.setDescription('Number of winners')
.setRequired(false)
.setMinValue(1)
.setMaxValue(20)
)
.addRoleOption(option =>
option.setName('required_role')
.setDescription('Role required to enter (optional)')
.setRequired(false)
)
)
.addSubcommand(sub =>
sub.setName('end')
.setDescription('End a giveaway early')
.addStringOption(option =>
option.setName('message_id')
.setDescription('Message ID of the giveaway')
.setRequired(true)
)
)
.addSubcommand(sub =>
sub.setName('reroll')
.setDescription('Reroll a giveaway winner')
.addStringOption(option =>
option.setName('message_id')
.setDescription('Message ID of the giveaway')
.setRequired(true)
)
)
.addSubcommand(sub =>
sub.setName('list')
.setDescription('List active giveaways')
),
async execute(interaction, supabase, client) {
const subcommand = interaction.options.getSubcommand();
if (subcommand === 'start') {
await handleStart(interaction, supabase, client);
} else if (subcommand === 'end') {
await handleEnd(interaction, supabase, client);
} else if (subcommand === 'reroll') {
await handleReroll(interaction, supabase, client);
} else if (subcommand === 'list') {
await handleList(interaction, supabase);
}
},
};
async function handleStart(interaction, supabase, client) {
await interaction.deferReply({ ephemeral: true });
const prize = interaction.options.getString('prize');
const duration = interaction.options.getInteger('duration');
const winnersCount = interaction.options.getInteger('winners') || 1;
const requiredRole = interaction.options.getRole('required_role');
const endTime = Date.now() + (duration * 60 * 1000);
const endTimestamp = Math.floor(endTime / 1000);
const embed = new EmbedBuilder()
.setColor(0xfbbf24)
.setTitle('🎉 GIVEAWAY 🎉')
.setDescription(`**${prize}**\n\nClick the button below to enter!`)
.addFields(
{ name: '⏰ Ends', value: `<t:${endTimestamp}:R>`, inline: true },
{ name: '🏆 Winners', value: `${winnersCount}`, inline: true },
{ name: '👥 Entries', value: '0', inline: true }
)
.setFooter({ text: `Hosted by ${interaction.user.tag}` })
.setTimestamp(new Date(endTime));
if (requiredRole) {
embed.addFields({ name: '🔒 Required Role', value: `${requiredRole}`, inline: false });
}
const button = new ButtonBuilder()
.setCustomId('giveaway_enter')
.setLabel('🎁 Enter Giveaway')
.setStyle(ButtonStyle.Success);
const row = new ActionRowBuilder().addComponents(button);
try {
const message = await interaction.channel.send({ embeds: [embed], components: [row] });
if (supabase) {
await supabase.from('giveaways').insert({
message_id: message.id,
channel_id: interaction.channelId,
guild_id: interaction.guildId,
prize: prize,
winners_count: winnersCount,
required_role: requiredRole?.id || null,
host_id: interaction.user.id,
end_time: new Date(endTime).toISOString(),
entries: [],
status: 'active'
});
}
client.giveaways = client.giveaways || new Map();
client.giveaways.set(message.id, {
channelId: interaction.channelId,
endTime: endTime,
prize: prize,
winnersCount: winnersCount,
requiredRole: requiredRole?.id,
entries: []
});
setTimeout(() => endGiveaway(message.id, client, supabase), duration * 60 * 1000);
const successEmbed = new EmbedBuilder()
.setColor(0x22c55e)
.setTitle('✅ Giveaway Started!')
.setDescription(`Giveaway for **${prize}** has started!\n\nEnds <t:${endTimestamp}:R>`)
.setTimestamp();
await interaction.editReply({ embeds: [successEmbed] });
} catch (error) {
console.error('Giveaway start error:', error);
await interaction.editReply({ content: 'Failed to start giveaway.' });
}
}
async function handleEnd(interaction, supabase, client) {
await interaction.deferReply({ ephemeral: true });
const messageId = interaction.options.getString('message_id');
try {
await endGiveaway(messageId, client, supabase, interaction.channel);
await interaction.editReply({ content: '✅ Giveaway ended!' });
} catch (error) {
console.error('Giveaway end error:', error);
await interaction.editReply({ content: 'Failed to end giveaway. Check the message ID.' });
}
}
async function handleReroll(interaction, supabase, client) {
await interaction.deferReply({ ephemeral: true });
const messageId = interaction.options.getString('message_id');
try {
if (!supabase) {
return interaction.editReply({ content: 'Database not available.' });
}
const { data: giveaway, error } = await supabase
.from('giveaways')
.select('*')
.eq('message_id', messageId)
.single();
if (error || !giveaway) {
return interaction.editReply({ content: 'Giveaway not found.' });
}
const entries = giveaway.entries || [];
if (entries.length === 0) {
return interaction.editReply({ content: 'No entries to reroll.' });
}
const winner = entries[Math.floor(Math.random() * entries.length)];
const channel = await client.channels.fetch(giveaway.channel_id);
await channel.send({
content: `🎉 New winner: <@${winner}>!\nPrize: **${giveaway.prize}**`
});
await interaction.editReply({ content: '✅ Rerolled winner!' });
} catch (error) {
console.error('Giveaway reroll error:', error);
await interaction.editReply({ content: 'Failed to reroll.' });
}
}
async function handleList(interaction, supabase) {
await interaction.deferReply({ ephemeral: true });
if (!supabase) {
return interaction.editReply({ content: 'Database not available.' });
}
try {
const { data: giveaways, error } = await supabase
.from('giveaways')
.select('*')
.eq('guild_id', interaction.guildId)
.eq('status', 'active')
.order('end_time', { ascending: true });
if (error) throw error;
if (!giveaways || giveaways.length === 0) {
return interaction.editReply({ content: 'No active giveaways.' });
}
const embed = new EmbedBuilder()
.setColor(0xfbbf24)
.setTitle('🎉 Active Giveaways')
.setDescription(giveaways.map(g => {
const endTimestamp = Math.floor(new Date(g.end_time).getTime() / 1000);
return `**${g.prize}**\nEnds: <t:${endTimestamp}:R>\nEntries: ${g.entries?.length || 0}\nMessage ID: \`${g.message_id}\``;
}).join('\n\n'))
.setTimestamp();
await interaction.editReply({ embeds: [embed] });
} catch (error) {
console.error('Giveaway list error:', error);
await interaction.editReply({ content: 'Failed to fetch giveaways.' });
}
}
async function endGiveaway(messageId, client, supabase, channel = null) {
try {
let giveawayData = client.giveaways?.get(messageId);
let entries = giveawayData?.entries || [];
let prize = giveawayData?.prize || 'Unknown Prize';
let winnersCount = giveawayData?.winnersCount || 1;
let channelId = giveawayData?.channelId;
if (supabase) {
const { data } = await supabase
.from('giveaways')
.select('*')
.eq('message_id', messageId)
.single();
if (data) {
entries = data.entries || [];
prize = data.prize;
winnersCount = data.winners_count;
channelId = data.channel_id;
await supabase
.from('giveaways')
.update({ status: 'ended' })
.eq('message_id', messageId);
}
}
if (!channelId) return;
const giveawayChannel = channel || await client.channels.fetch(channelId);
const message = await giveawayChannel.messages.fetch(messageId).catch(() => null);
if (!message) return;
const winners = [];
const entriesCopy = [...entries];
for (let i = 0; i < winnersCount && entriesCopy.length > 0; i++) {
const index = Math.floor(Math.random() * entriesCopy.length);
winners.push(entriesCopy.splice(index, 1)[0]);
}
const endedEmbed = EmbedBuilder.from(message.embeds[0])
.setColor(0x6b7280)
.setTitle('🎉 GIVEAWAY ENDED 🎉')
.setFields(
{ name: '🏆 Winner(s)', value: winners.length > 0 ? winners.map(w => `<@${w}>`).join(', ') : 'No valid entries', inline: false },
{ name: '👥 Total Entries', value: `${entries.length}`, inline: true }
);
const disabledButton = new ButtonBuilder()
.setCustomId('giveaway_ended')
.setLabel('Giveaway Ended')
.setStyle(ButtonStyle.Secondary)
.setDisabled(true);
const row = new ActionRowBuilder().addComponents(disabledButton);
await message.edit({ embeds: [endedEmbed], components: [row] });
if (winners.length > 0) {
await giveawayChannel.send({
content: `🎉 Congratulations ${winners.map(w => `<@${w}>`).join(', ')}! You won **${prize}**!`
});
}
client.giveaways?.delete(messageId);
} catch (error) {
console.error('End giveaway error:', error);
}
}
module.exports.endGiveaway = endGiveaway;

View file

@ -1,55 +1,205 @@
const { SlashCommandBuilder, EmbedBuilder } = require("discord.js");
const { SlashCommandBuilder, EmbedBuilder, ActionRowBuilder, StringSelectMenuBuilder } = require("discord.js");
module.exports = {
data: new SlashCommandBuilder()
.setName("help")
.setDescription("View all AeThex bot commands and features"),
.setDescription("View all AeThex bot commands and features")
.addStringOption(option =>
option.setName('category')
.setDescription('Specific category to view')
.setRequired(false)
.addChoices(
{ name: '🔗 Account', value: 'account' },
{ name: '⚔️ Realms', value: 'realms' },
{ name: '📊 Community', value: 'community' },
{ name: '⭐ Leveling', value: 'leveling' },
{ name: '🛡️ Moderation', value: 'moderation' },
{ name: '🔧 Utility', value: 'utility' },
{ name: '⚙️ Admin', value: 'admin' }
)
),
async execute(interaction) {
const embed = new EmbedBuilder()
.setColor(0x7289da)
.setTitle("🤖 AeThex Bot Commands")
.setDescription("Here are all the commands you can use with the AeThex Discord bot.")
const category = interaction.options.getString('category');
if (category) {
const embed = getCategoryEmbed(category);
return interaction.reply({ embeds: [embed], ephemeral: true });
}
const mainEmbed = new EmbedBuilder()
.setColor(0x7c3aed)
.setAuthor({
name: 'AeThex Bot Help',
iconURL: interaction.client.user.displayAvatarURL()
})
.setDescription('Welcome to AeThex Bot! Select a category below to view commands.')
.addFields(
{
name: "🔗 Account Linking",
value: [
"`/verify` - Link your Discord account to AeThex",
"`/unlink` - Disconnect your Discord from AeThex",
"`/profile` - View your linked AeThex profile",
].join("\n"),
name: "🔗 Account",
value: "`/verify` `/unlink` `/profile`",
inline: true,
},
{
name: "⚔️ Realm Management",
value: [
"`/set-realm` - Choose your primary realm (Labs, GameForge, Corp, Foundation, Dev-Link)",
"`/verify-role` - Check your assigned Discord roles",
].join("\n"),
name: "⚔️ Realms",
value: "`/set-realm` `/verify-role` `/refresh-roles`",
inline: true,
},
{
name: "📊 Community",
value: [
"`/stats` - View your AeThex statistics and activity",
"`/leaderboard` - See the top contributors",
"`/post` - Create a post in the AeThex community feed",
].join("\n"),
value: "`/stats` `/leaderboard` `/post` `/poll`",
inline: true,
},
{
name: " Information",
value: "`/help` - Show this help message",
name: "⭐ Leveling",
value: "`/rank` `/daily` `/badges`",
inline: true,
},
{
name: "🛡️ Moderation",
value: "`/warn` `/kick` `/ban` `/timeout` `/modlog`",
inline: true,
},
{
name: "🔧 Utility",
value: "`/userinfo` `/serverinfo` `/avatar`",
inline: true,
},
{
name: "⚙️ Admin",
value: "`/config` `/announce` `/embed` `/rolepanel`",
inline: true,
},
{
name: "🎉 Fun",
value: "`/giveaway` `/schedule`",
inline: true,
},
{
name: "🛡️ Auto-Mod",
value: "`/automod`",
inline: true,
}
)
.addFields({
name: "🔗 Quick Links",
value: [
"[AeThex Platform](https://aethex.dev)",
"[Creator Directory](https://aethex.dev/creators)",
"[Community Feed](https://aethex.dev/community/feed)",
].join(" | "),
value: "[AeThex Platform](https://aethex.dev) • [Creator Directory](https://aethex.dev/creators) • [Community Feed](https://aethex.dev/community/feed)",
inline: false,
})
.setFooter({
text: "Use /help [category] for detailed commands • AeThex Bot",
iconURL: interaction.client.user.displayAvatarURL()
})
.setFooter({ text: "AeThex | Build. Create. Connect." })
.setTimestamp();
await interaction.reply({ embeds: [embed], ephemeral: true });
const selectMenu = new StringSelectMenuBuilder()
.setCustomId('help_category')
.setPlaceholder('Select a category for details...')
.addOptions([
{ label: 'Account', description: 'Link and manage your account', emoji: '🔗', value: 'account' },
{ label: 'Realms', description: 'Realm selection and roles', emoji: '⚔️', value: 'realms' },
{ label: 'Community', description: 'Community features', emoji: '📊', value: 'community' },
{ label: 'Leveling', description: 'XP and leveling system', emoji: '⭐', value: 'leveling' },
{ label: 'Moderation', description: 'Moderation tools', emoji: '🛡️', value: 'moderation' },
{ label: 'Utility', description: 'Utility commands', emoji: '🔧', value: 'utility' },
{ label: 'Admin', description: 'Admin and config', emoji: '⚙️', value: 'admin' },
]);
const row = new ActionRowBuilder().addComponents(selectMenu);
await interaction.reply({ embeds: [mainEmbed], components: [row], ephemeral: true });
},
};
function getCategoryEmbed(category) {
const categories = {
account: {
title: '🔗 Account Commands',
color: 0x3b82f6,
commands: [
{ name: '/verify', desc: 'Link your Discord account to AeThex' },
{ name: '/unlink', desc: 'Disconnect your Discord from AeThex' },
{ name: '/profile [@user]', desc: 'View your or another user\'s AeThex profile' },
]
},
realms: {
title: '⚔️ Realm Commands',
color: 0xf97316,
commands: [
{ name: '/set-realm', desc: 'Choose your primary realm (Labs, GameForge, Corp, Foundation, Dev-Link)' },
{ name: '/verify-role', desc: 'Check your assigned Discord roles' },
{ name: '/refresh-roles', desc: 'Sync your roles based on AeThex profile' },
]
},
community: {
title: '📊 Community Commands',
color: 0x22c55e,
commands: [
{ name: '/stats', desc: 'View your AeThex statistics and activity' },
{ name: '/leaderboard [category]', desc: 'See the top contributors' },
{ name: '/post', desc: 'Create a post in the AeThex community feed' },
{ name: '/poll', desc: 'Create a community poll' },
{ name: '/studio [@user]', desc: 'View AeThex Studio profile' },
{ name: '/foundation [@user]', desc: 'View Foundation contributions' },
]
},
leveling: {
title: '⭐ Leveling Commands',
color: 0xfbbf24,
commands: [
{ name: '/rank [@user]', desc: 'View your level and unified XP' },
{ name: '/daily', desc: 'Claim your daily XP bonus (+50 base + streak)' },
{ name: '/badges', desc: 'View earned badges across platforms' },
]
},
moderation: {
title: '🛡️ Moderation Commands',
color: 0xef4444,
commands: [
{ name: '/warn @user [reason]', desc: 'Warn a user' },
{ name: '/kick @user [reason]', desc: 'Kick a user from the server' },
{ name: '/ban @user [reason]', desc: 'Ban a user from the server' },
{ name: '/timeout @user [minutes] [reason]', desc: 'Timeout a user' },
{ name: '/modlog @user', desc: 'View a user\'s moderation history' },
{ name: '/auditlog', desc: 'View admin action history' },
]
},
utility: {
title: '🔧 Utility Commands',
color: 0x8b5cf6,
commands: [
{ name: '/userinfo [@user]', desc: 'View detailed user information' },
{ name: '/serverinfo', desc: 'View server statistics and info' },
{ name: '/avatar [@user]', desc: 'Get a user\'s avatar' },
{ name: '/status', desc: 'View network status' },
]
},
admin: {
title: '⚙️ Admin Commands',
color: 0x6b7280,
commands: [
{ name: '/config', desc: 'View and edit server configuration' },
{ name: '/announce', desc: 'Send cross-server announcements' },
{ name: '/embed', desc: 'Create custom embed messages' },
{ name: '/rolepanel', desc: 'Create role button panels' },
{ name: '/giveaway', desc: 'Create and manage giveaways' },
{ name: '/schedule', desc: 'Schedule messages for later' },
{ name: '/automod', desc: 'Configure auto-moderation' },
{ name: '/admin', desc: 'Bot administration commands' },
{ name: '/federation', desc: 'Manage cross-server role sync' },
{ name: '/ticket', desc: 'Manage support tickets' },
]
}
};
const cat = categories[category] || categories.account;
return new EmbedBuilder()
.setColor(cat.color)
.setTitle(cat.title)
.setDescription(cat.commands.map(c => `**${c.name}**\n${c.desc}`).join('\n\n'))
.setFooter({ text: 'Use /help to see all categories' })
.setTimestamp();
}
module.exports.getCategoryEmbed = getCategoryEmbed;

View file

@ -12,7 +12,8 @@ module.exports = {
.addChoices(
{ name: "🔥 Most Active (Posts)", value: "posts" },
{ name: "❤️ Most Liked", value: "likes" },
{ name: "🎨 Top Creators", value: "creators" }
{ name: "🎨 Top Creators", value: "creators" },
{ name: "⭐ XP Leaders", value: "xp" }
)
),
@ -23,15 +24,38 @@ module.exports = {
await interaction.deferReply();
try {
const category = interaction.options.getString("category") || "posts";
const category = interaction.options.getString("category") || "xp";
let leaderboardData = [];
let title = "";
let emoji = "";
let color = 0x7c3aed;
if (category === "posts") {
if (category === "xp") {
title = "XP Leaderboard";
emoji = "⭐";
color = 0xfbbf24;
const { data: profiles } = await supabase
.from("user_profiles")
.select("id, username, full_name, avatar_url, xp")
.not("xp", "is", null)
.order("xp", { ascending: false })
.limit(10);
for (const profile of profiles || []) {
const level = Math.floor(Math.sqrt((profile.xp || 0) / 100));
leaderboardData.push({
name: profile.full_name || profile.username || "Anonymous",
value: `Level ${level}${(profile.xp || 0).toLocaleString()} XP`,
username: profile.username,
xp: profile.xp || 0
});
}
} else if (category === "posts") {
title = "Most Active Posters";
emoji = "🔥";
color = 0xef4444;
const { data: posts } = await supabase
.from("community_posts")
@ -65,6 +89,7 @@ module.exports = {
} else if (category === "likes") {
title = "Most Liked Users";
emoji = "❤️";
color = 0xec4899;
const { data: posts } = await supabase
.from("community_posts")
@ -92,7 +117,7 @@ module.exports = {
if (profile) {
leaderboardData.push({
name: profile.full_name || profile.username || "Anonymous",
value: `${count} likes received`,
value: `${count.toLocaleString()} likes`,
username: profile.username,
});
}
@ -100,6 +125,7 @@ module.exports = {
} else if (category === "creators") {
title = "Top Creators";
emoji = "🎨";
color = 0x8b5cf6;
const { data: creators } = await supabase
.from("aethex_creators")
@ -128,22 +154,36 @@ module.exports = {
}
}
const medals = ['🥇', '🥈', '🥉'];
const description = leaderboardData.length > 0
? leaderboardData
.map((user, index) => {
const medal = index < 3 ? medals[index] : `\`${index + 1}.\``;
return `${medal} **${user.name}**\n${user.value}`;
})
.join("\n\n")
: "No data available yet. Be the first to contribute!";
const embed = new EmbedBuilder()
.setColor(0x7289da)
.setColor(color)
.setTitle(`${emoji} ${title}`)
.setDescription(
leaderboardData.length > 0
? leaderboardData
.map(
(user, index) =>
`**${index + 1}.** ${user.name} - ${user.value}`
)
.join("\n")
: "No data available yet. Be the first to contribute!"
)
.setFooter({ text: "AeThex Leaderboard | Updated in real-time" })
.setDescription(description)
.setThumbnail(interaction.guild.iconURL({ size: 128 }))
.setFooter({
text: `${interaction.guild.name} • Updated in real-time`,
iconURL: interaction.guild.iconURL({ size: 32 })
})
.setTimestamp();
if (leaderboardData.length > 0) {
embed.addFields({
name: '📊 Stats',
value: `Showing top ${leaderboardData.length} contributors`,
inline: true
});
}
await interaction.editReply({ embeds: [embed] });
} catch (error) {
console.error("Leaderboard command error:", error);

View file

@ -3,27 +3,37 @@ const { SlashCommandBuilder, EmbedBuilder } = require("discord.js");
module.exports = {
data: new SlashCommandBuilder()
.setName("profile")
.setDescription("View your AeThex profile in Discord"),
.setDescription("View your AeThex profile in Discord")
.addUserOption(option =>
option.setName('user')
.setDescription('User to view profile of')
.setRequired(false)
),
async execute(interaction, supabase) {
if (!supabase) {
return interaction.reply({ content: "This feature requires Supabase to be configured.", ephemeral: true });
}
await interaction.deferReply({ ephemeral: true });
await interaction.deferReply();
const targetUser = interaction.options.getUser('user') || interaction.user;
try {
const { data: link } = await supabase
.from("discord_links")
.select("user_id, primary_arm")
.eq("discord_id", interaction.user.id)
.eq("discord_id", targetUser.id)
.single();
if (!link) {
const embed = new EmbedBuilder()
.setColor(0xff6b6b)
.setTitle("❌ Not Linked")
.setThumbnail(targetUser.displayAvatarURL({ size: 256 }))
.setDescription(
"You must link your Discord account to AeThex first.\nUse `/verify` to get started.",
targetUser.id === interaction.user.id
? "You must link your Discord account to AeThex first.\nUse `/verify` to get started."
: `${targetUser.tag} hasn't linked their Discord account to AeThex yet.`
);
return await interaction.editReply({ embeds: [embed] });
@ -39,7 +49,7 @@ module.exports = {
const embed = new EmbedBuilder()
.setColor(0xff6b6b)
.setTitle("❌ Profile Not Found")
.setDescription("Your AeThex profile could not be found.");
.setDescription("The AeThex profile could not be found.");
return await interaction.editReply({ embeds: [embed] });
}
@ -52,35 +62,77 @@ module.exports = {
devlink: "💻",
};
const armColors = {
labs: 0x22c55e,
gameforge: 0xf97316,
corp: 0x3b82f6,
foundation: 0xec4899,
devlink: 0x8b5cf6,
};
const xp = profile.xp || 0;
const level = Math.floor(Math.sqrt(xp / 100));
const currentLevelXp = level * level * 100;
const nextLevelXp = (level + 1) * (level + 1) * 100;
const progressXp = xp - currentLevelXp;
const neededXp = nextLevelXp - currentLevelXp;
const progressPercent = Math.min(100, Math.floor((progressXp / neededXp) * 100));
const progressBar = createProgressBar(progressPercent);
const badges = profile.badges || [];
const badgeDisplay = badges.length > 0
? badges.map(b => getBadgeEmoji(b)).join(' ')
: 'No badges yet';
const embed = new EmbedBuilder()
.setColor(0x7289da)
.setTitle(`${profile.full_name || "AeThex User"}'s Profile`)
.setThumbnail(
profile.avatar_url || "https://aethex.dev/placeholder.svg",
)
.setColor(armColors[link.primary_arm] || 0x7c3aed)
.setAuthor({
name: `${profile.full_name || profile.username || 'AeThex User'}`,
iconURL: targetUser.displayAvatarURL({ size: 64 })
})
.setThumbnail(profile.avatar_url || targetUser.displayAvatarURL({ size: 256 }))
.setDescription(profile.bio || '*No bio set*')
.addFields(
{
name: "👤 Username",
value: profile.username || "N/A",
value: `\`${profile.username || 'N/A'}\``,
inline: true,
},
{
name: `${armEmojis[link.primary_arm] || "⚔️"} Primary Realm`,
value: link.primary_arm || "Not set",
name: `${armEmojis[link.primary_arm] || "⚔️"} Realm`,
value: capitalizeFirst(link.primary_arm) || "Not set",
inline: true,
},
{
name: "📊 Role",
value: profile.user_type || "community_member",
value: formatRole(profile.user_type),
inline: true,
},
{ name: "📝 Bio", value: profile.bio || "No bio set", inline: false },
{
name: `📈 Level ${level}`,
value: `${progressBar}\n\`${xp.toLocaleString()}\` / \`${nextLevelXp.toLocaleString()}\` XP`,
inline: false,
},
{
name: "🏆 Badges",
value: badgeDisplay,
inline: false,
}
)
.addFields({
name: "🔗 Links",
value: `[Visit Full Profile](https://aethex.dev/creators/${profile.username})`,
value: `[View Full Profile](https://aethex.dev/creators/${profile.username}) • [AeThex Platform](https://aethex.dev)`,
})
.setFooter({ text: "AeThex | Your Web3 Creator Hub" });
.setFooter({
text: `AeThex | ${targetUser.tag}`,
iconURL: 'https://aethex.dev/favicon.ico'
})
.setTimestamp();
if (profile.banner_url) {
embed.setImage(profile.banner_url);
}
await interaction.editReply({ embeds: [embed] });
} catch (error) {
@ -94,3 +146,38 @@ module.exports = {
}
},
};
function createProgressBar(percent) {
const filled = Math.floor(percent / 10);
const empty = 10 - filled;
return `${'▓'.repeat(filled)}${'░'.repeat(empty)} ${percent}%`;
}
function capitalizeFirst(str) {
if (!str) return str;
return str.charAt(0).toUpperCase() + str.slice(1);
}
function formatRole(role) {
if (!role) return 'Member';
return role.split('_').map(capitalizeFirst).join(' ');
}
function getBadgeEmoji(badge) {
const badgeMap = {
'verified': '✅',
'founder': '👑',
'early_adopter': '🌟',
'contributor': '💎',
'creator': '🎨',
'developer': '💻',
'moderator': '🛡️',
'partner': '🤝',
'premium': '💫',
'top_poster': '📝',
'helpful': '❤️',
'bug_hunter': '🐛',
'event_winner': '🏆',
};
return badgeMap[badge] || `[${badge}]`;
}

View file

@ -0,0 +1,394 @@
const {
SlashCommandBuilder,
EmbedBuilder,
PermissionFlagsBits,
ActionRowBuilder,
ButtonBuilder,
ButtonStyle,
ChannelType
} = require('discord.js');
module.exports = {
data: new SlashCommandBuilder()
.setName('rolepanel')
.setDescription('Create and manage role button panels')
.setDefaultMemberPermissions(PermissionFlagsBits.ManageRoles)
.addSubcommand(sub =>
sub.setName('create')
.setDescription('Create a new role panel')
.addChannelOption(option =>
option.setName('channel')
.setDescription('Channel to send the panel to')
.setRequired(true)
.addChannelTypes(ChannelType.GuildText)
)
.addStringOption(option =>
option.setName('title')
.setDescription('Panel title')
.setRequired(true)
.setMaxLength(256)
)
.addStringOption(option =>
option.setName('description')
.setDescription('Panel description')
.setRequired(false)
.setMaxLength(2000)
)
.addStringOption(option =>
option.setName('color')
.setDescription('Embed color')
.setRequired(false)
.addChoices(
{ name: '🟣 Purple', value: '7c3aed' },
{ name: '🟢 Green', value: '22c55e' },
{ name: '🔵 Blue', value: '3b82f6' },
{ name: '🟡 Yellow', value: 'eab308' },
{ name: '🟠 Orange', value: 'f97316' }
)
)
)
.addSubcommand(sub =>
sub.setName('addrole')
.setDescription('Add a role button to a panel')
.addStringOption(option =>
option.setName('message_id')
.setDescription('Message ID of the role panel')
.setRequired(true)
)
.addRoleOption(option =>
option.setName('role')
.setDescription('Role to add')
.setRequired(true)
)
.addStringOption(option =>
option.setName('label')
.setDescription('Button label (optional, uses role name if not set)')
.setRequired(false)
.setMaxLength(80)
)
.addStringOption(option =>
option.setName('emoji')
.setDescription('Button emoji (optional)')
.setRequired(false)
)
.addStringOption(option =>
option.setName('style')
.setDescription('Button style')
.setRequired(false)
.addChoices(
{ name: 'Blue (Primary)', value: 'primary' },
{ name: 'Gray (Secondary)', value: 'secondary' },
{ name: 'Green (Success)', value: 'success' },
{ name: 'Red (Danger)', value: 'danger' }
)
)
)
.addSubcommand(sub =>
sub.setName('removerole')
.setDescription('Remove a role button from a panel')
.addStringOption(option =>
option.setName('message_id')
.setDescription('Message ID of the role panel')
.setRequired(true)
)
.addRoleOption(option =>
option.setName('role')
.setDescription('Role to remove')
.setRequired(true)
)
)
.addSubcommand(sub =>
sub.setName('list')
.setDescription('List all role panels in this server')
),
async execute(interaction, supabase, client) {
const subcommand = interaction.options.getSubcommand();
if (subcommand === 'create') {
await handleCreate(interaction, supabase);
} else if (subcommand === 'addrole') {
await handleAddRole(interaction, supabase, client);
} else if (subcommand === 'removerole') {
await handleRemoveRole(interaction, supabase, client);
} else if (subcommand === 'list') {
await handleList(interaction, supabase);
}
},
};
async function handleCreate(interaction, supabase) {
await interaction.deferReply({ ephemeral: true });
const channel = interaction.options.getChannel('channel');
const title = interaction.options.getString('title');
const description = interaction.options.getString('description') || 'Click a button below to get or remove a role!';
const color = parseInt(interaction.options.getString('color') || '7c3aed', 16);
const embed = new EmbedBuilder()
.setColor(color)
.setTitle(`🎭 ${title}`)
.setDescription(description)
.setFooter({ text: 'Click a button to toggle the role' })
.setTimestamp();
try {
const message = await channel.send({ embeds: [embed] });
if (supabase) {
await supabase.from('role_panels').insert({
message_id: message.id,
channel_id: channel.id,
guild_id: interaction.guildId,
title: title,
description: description,
color: color.toString(16),
roles: [],
created_by: interaction.user.id
});
}
const successEmbed = new EmbedBuilder()
.setColor(0x22c55e)
.setTitle('✅ Role Panel Created')
.setDescription(`Panel created in ${channel}!\n\nUse \`/rolepanel addrole\` with message ID:\n\`${message.id}\``)
.setTimestamp();
await interaction.editReply({ embeds: [successEmbed] });
} catch (error) {
console.error('Role panel create error:', error);
await interaction.editReply({ content: 'Failed to create role panel. Check bot permissions.', ephemeral: true });
}
}
async function handleAddRole(interaction, supabase, client) {
await interaction.deferReply({ ephemeral: true });
const messageId = interaction.options.getString('message_id');
const role = interaction.options.getRole('role');
const label = interaction.options.getString('label') || role.name;
const emoji = interaction.options.getString('emoji');
const styleStr = interaction.options.getString('style') || 'primary';
const styleMap = {
'primary': ButtonStyle.Primary,
'secondary': ButtonStyle.Secondary,
'success': ButtonStyle.Success,
'danger': ButtonStyle.Danger
};
const style = styleMap[styleStr];
try {
let channel = null;
let message = null;
if (supabase) {
const { data: panel } = await supabase
.from('role_panels')
.select('channel_id')
.eq('message_id', messageId)
.eq('guild_id', interaction.guildId)
.single();
if (panel?.channel_id) {
channel = await interaction.guild.channels.fetch(panel.channel_id).catch(() => null);
}
}
if (!channel) {
channel = interaction.channel;
}
message = await channel.messages.fetch(messageId).catch(() => null);
if (!message) {
return interaction.editReply({ content: 'Could not find that message. Make sure the message ID is correct and the panel exists.' });
}
if (message.author.id !== client.user.id) {
return interaction.editReply({ content: 'That message was not sent by me. I can only edit my own messages.' });
}
const buttonData = {
role_id: role.id,
role_name: role.name,
label: label,
emoji: emoji,
style: styleStr
};
const existingRows = message.components.map(row => ActionRowBuilder.from(row));
const button = new ButtonBuilder()
.setCustomId(`role_${role.id}`)
.setLabel(label)
.setStyle(style);
if (emoji) {
button.setEmoji(emoji);
}
let added = false;
for (const row of existingRows) {
if (row.components.length < 5) {
row.addComponents(button);
added = true;
break;
}
}
if (!added) {
if (existingRows.length >= 5) {
return interaction.editReply({ content: 'Maximum buttons reached (25). Remove some roles first.' });
}
const newRow = new ActionRowBuilder().addComponents(button);
existingRows.push(newRow);
}
await message.edit({ components: existingRows });
if (supabase) {
const { data: panel } = await supabase
.from('role_panels')
.select('roles')
.eq('message_id', messageId)
.single();
const roles = panel?.roles || [];
roles.push(buttonData);
await supabase
.from('role_panels')
.update({ roles: roles })
.eq('message_id', messageId);
}
const successEmbed = new EmbedBuilder()
.setColor(0x22c55e)
.setTitle('✅ Role Added')
.setDescription(`Added ${role} to the panel!`)
.setTimestamp();
await interaction.editReply({ embeds: [successEmbed] });
} catch (error) {
console.error('Add role error:', error);
await interaction.editReply({ content: `Failed to add role: ${error.message}` });
}
}
async function handleRemoveRole(interaction, supabase, client) {
await interaction.deferReply({ ephemeral: true });
const messageId = interaction.options.getString('message_id');
const role = interaction.options.getRole('role');
try {
let channel = null;
let message = null;
if (supabase) {
const { data: panel } = await supabase
.from('role_panels')
.select('channel_id')
.eq('message_id', messageId)
.eq('guild_id', interaction.guildId)
.single();
if (panel?.channel_id) {
channel = await interaction.guild.channels.fetch(panel.channel_id).catch(() => null);
}
}
if (!channel) {
channel = interaction.channel;
}
message = await channel.messages.fetch(messageId).catch(() => null);
if (!message) {
return interaction.editReply({ content: 'Could not find that message. Make sure the message ID is correct and the panel exists.' });
}
const existingRows = message.components.map(row => ActionRowBuilder.from(row));
for (const row of existingRows) {
const buttonIndex = row.components.findIndex(btn => btn.data.custom_id === `role_${role.id}`);
if (buttonIndex !== -1) {
row.components.splice(buttonIndex, 1);
}
}
const filteredRows = existingRows.filter(row => row.components.length > 0);
await message.edit({ components: filteredRows });
if (supabase) {
const { data: panel } = await supabase
.from('role_panels')
.select('roles')
.eq('message_id', messageId)
.single();
const roles = (panel?.roles || []).filter(r => r.role_id !== role.id);
await supabase
.from('role_panels')
.update({ roles: roles })
.eq('message_id', messageId);
}
const successEmbed = new EmbedBuilder()
.setColor(0x22c55e)
.setTitle('✅ Role Removed')
.setDescription(`Removed ${role} from the panel!`)
.setTimestamp();
await interaction.editReply({ embeds: [successEmbed] });
} catch (error) {
console.error('Remove role error:', error);
await interaction.editReply({ content: `Failed to remove role: ${error.message}` });
}
}
async function handleList(interaction, supabase) {
await interaction.deferReply({ ephemeral: true });
if (!supabase) {
return interaction.editReply({ content: 'This feature requires database connection.' });
}
try {
const { data: panels, error } = await supabase
.from('role_panels')
.select('*')
.eq('guild_id', interaction.guildId)
.order('created_at', { ascending: false });
if (error) throw error;
if (!panels || panels.length === 0) {
return interaction.editReply({ content: 'No role panels found in this server.' });
}
const embed = new EmbedBuilder()
.setColor(0x7c3aed)
.setTitle('🎭 Role Panels')
.setDescription(panels.map(p =>
`**${p.title}**\n` +
`Channel: <#${p.channel_id}>\n` +
`Message ID: \`${p.message_id}\`\n` +
`Roles: ${p.roles?.length || 0}`
).join('\n\n'))
.setFooter({ text: `${panels.length} panel(s)` })
.setTimestamp();
await interaction.editReply({ embeds: [embed] });
} catch (error) {
console.error('List panels error:', error);
await interaction.editReply({ content: 'Failed to fetch panels.' });
}
}

View file

@ -0,0 +1,331 @@
const {
SlashCommandBuilder,
EmbedBuilder,
PermissionFlagsBits,
ChannelType
} = require('discord.js');
module.exports = {
data: new SlashCommandBuilder()
.setName('schedule')
.setDescription('Schedule a message to be sent later')
.setDefaultMemberPermissions(PermissionFlagsBits.ManageMessages)
.addSubcommand(sub =>
sub.setName('message')
.setDescription('Schedule a text message')
.addChannelOption(option =>
option.setName('channel')
.setDescription('Channel to send the message to')
.setRequired(true)
.addChannelTypes(ChannelType.GuildText, ChannelType.GuildAnnouncement)
)
.addStringOption(option =>
option.setName('content')
.setDescription('Message content')
.setRequired(true)
.setMaxLength(2000)
)
.addIntegerOption(option =>
option.setName('minutes')
.setDescription('Minutes from now')
.setRequired(true)
.setMinValue(1)
.setMaxValue(10080)
)
.addBooleanOption(option =>
option.setName('ping_everyone')
.setDescription('Ping @everyone')
.setRequired(false)
)
)
.addSubcommand(sub =>
sub.setName('embed')
.setDescription('Schedule an embed message')
.addChannelOption(option =>
option.setName('channel')
.setDescription('Channel to send the embed to')
.setRequired(true)
.addChannelTypes(ChannelType.GuildText, ChannelType.GuildAnnouncement)
)
.addStringOption(option =>
option.setName('title')
.setDescription('Embed title')
.setRequired(true)
.setMaxLength(256)
)
.addStringOption(option =>
option.setName('description')
.setDescription('Embed description')
.setRequired(true)
.setMaxLength(2000)
)
.addIntegerOption(option =>
option.setName('minutes')
.setDescription('Minutes from now')
.setRequired(true)
.setMinValue(1)
.setMaxValue(10080)
)
.addStringOption(option =>
option.setName('color')
.setDescription('Embed color')
.setRequired(false)
.addChoices(
{ name: '🟣 Purple', value: '7c3aed' },
{ name: '🟢 Green', value: '22c55e' },
{ name: '🔴 Red', value: 'ef4444' },
{ name: '🔵 Blue', value: '3b82f6' },
{ name: '🟡 Yellow', value: 'eab308' }
)
)
)
.addSubcommand(sub =>
sub.setName('list')
.setDescription('List scheduled messages')
)
.addSubcommand(sub =>
sub.setName('cancel')
.setDescription('Cancel a scheduled message')
.addStringOption(option =>
option.setName('id')
.setDescription('Scheduled message ID')
.setRequired(true)
)
),
async execute(interaction, supabase, client) {
const subcommand = interaction.options.getSubcommand();
if (subcommand === 'message') {
await handleMessage(interaction, supabase, client);
} else if (subcommand === 'embed') {
await handleEmbed(interaction, supabase, client);
} else if (subcommand === 'list') {
await handleList(interaction, supabase);
} else if (subcommand === 'cancel') {
await handleCancel(interaction, supabase, client);
}
},
};
async function handleMessage(interaction, supabase, client) {
await interaction.deferReply({ ephemeral: true });
const channel = interaction.options.getChannel('channel');
const content = interaction.options.getString('content');
const minutes = interaction.options.getInteger('minutes');
const pingEveryone = interaction.options.getBoolean('ping_everyone') || false;
const sendTime = Date.now() + (minutes * 60 * 1000);
const sendTimestamp = Math.floor(sendTime / 1000);
const scheduleId = `sched_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
try {
const scheduleData = {
id: scheduleId,
type: 'message',
channelId: channel.id,
content: pingEveryone ? `@everyone ${content}` : content,
sendTime: sendTime
};
client.scheduledMessages = client.scheduledMessages || new Map();
const timeout = setTimeout(() => sendScheduledMessage(scheduleId, client, supabase), minutes * 60 * 1000);
client.scheduledMessages.set(scheduleId, { ...scheduleData, timeout });
if (supabase) {
await supabase.from('scheduled_messages').insert({
id: scheduleId,
guild_id: interaction.guildId,
channel_id: channel.id,
type: 'message',
content: scheduleData.content,
send_time: new Date(sendTime).toISOString(),
created_by: interaction.user.id,
status: 'pending'
});
}
const embed = new EmbedBuilder()
.setColor(0x22c55e)
.setTitle('✅ Message Scheduled')
.setDescription(`Message will be sent to ${channel} <t:${sendTimestamp}:R>`)
.addFields(
{ name: 'ID', value: `\`${scheduleId}\``, inline: true },
{ name: 'Send Time', value: `<t:${sendTimestamp}:f>`, inline: true }
)
.setTimestamp();
await interaction.editReply({ embeds: [embed] });
} catch (error) {
console.error('Schedule message error:', error);
await interaction.editReply({ content: 'Failed to schedule message.' });
}
}
async function handleEmbed(interaction, supabase, client) {
await interaction.deferReply({ ephemeral: true });
const channel = interaction.options.getChannel('channel');
const title = interaction.options.getString('title');
const description = interaction.options.getString('description');
const minutes = interaction.options.getInteger('minutes');
const color = interaction.options.getString('color') || '7c3aed';
const sendTime = Date.now() + (minutes * 60 * 1000);
const sendTimestamp = Math.floor(sendTime / 1000);
const scheduleId = `sched_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
try {
const embedData = {
title: title,
description: description,
color: parseInt(color, 16)
};
const scheduleData = {
id: scheduleId,
type: 'embed',
channelId: channel.id,
embedData: embedData,
sendTime: sendTime
};
client.scheduledMessages = client.scheduledMessages || new Map();
const timeout = setTimeout(() => sendScheduledMessage(scheduleId, client, supabase), minutes * 60 * 1000);
client.scheduledMessages.set(scheduleId, { ...scheduleData, timeout });
if (supabase) {
await supabase.from('scheduled_messages').insert({
id: scheduleId,
guild_id: interaction.guildId,
channel_id: channel.id,
type: 'embed',
embed_data: embedData,
send_time: new Date(sendTime).toISOString(),
created_by: interaction.user.id,
status: 'pending'
});
}
const embed = new EmbedBuilder()
.setColor(0x22c55e)
.setTitle('✅ Embed Scheduled')
.setDescription(`Embed will be sent to ${channel} <t:${sendTimestamp}:R>`)
.addFields(
{ name: 'ID', value: `\`${scheduleId}\``, inline: true },
{ name: 'Send Time', value: `<t:${sendTimestamp}:f>`, inline: true },
{ name: 'Embed Title', value: title, inline: false }
)
.setTimestamp();
await interaction.editReply({ embeds: [embed] });
} catch (error) {
console.error('Schedule embed error:', error);
await interaction.editReply({ content: 'Failed to schedule embed.' });
}
}
async function handleList(interaction, supabase) {
await interaction.deferReply({ ephemeral: true });
if (!supabase) {
return interaction.editReply({ content: 'Database not available.' });
}
try {
const { data: scheduled, error } = await supabase
.from('scheduled_messages')
.select('*')
.eq('guild_id', interaction.guildId)
.eq('status', 'pending')
.order('send_time', { ascending: true });
if (error) throw error;
if (!scheduled || scheduled.length === 0) {
return interaction.editReply({ content: 'No scheduled messages.' });
}
const embed = new EmbedBuilder()
.setColor(0x7c3aed)
.setTitle('📅 Scheduled Messages')
.setDescription(scheduled.map(s => {
const sendTimestamp = Math.floor(new Date(s.send_time).getTime() / 1000);
return `**ID:** \`${s.id}\`\nType: ${s.type}\nChannel: <#${s.channel_id}>\nSends: <t:${sendTimestamp}:R>`;
}).join('\n\n'))
.setFooter({ text: `${scheduled.length} scheduled` })
.setTimestamp();
await interaction.editReply({ embeds: [embed] });
} catch (error) {
console.error('Schedule list error:', error);
await interaction.editReply({ content: 'Failed to fetch scheduled messages.' });
}
}
async function handleCancel(interaction, supabase, client) {
await interaction.deferReply({ ephemeral: true });
const scheduleId = interaction.options.getString('id');
try {
const scheduled = client.scheduledMessages?.get(scheduleId);
if (scheduled?.timeout) {
clearTimeout(scheduled.timeout);
client.scheduledMessages.delete(scheduleId);
}
if (supabase) {
await supabase
.from('scheduled_messages')
.update({ status: 'cancelled' })
.eq('id', scheduleId);
}
await interaction.editReply({ content: '✅ Scheduled message cancelled.' });
} catch (error) {
console.error('Schedule cancel error:', error);
await interaction.editReply({ content: 'Failed to cancel.' });
}
}
async function sendScheduledMessage(scheduleId, client, supabase) {
try {
const scheduled = client.scheduledMessages?.get(scheduleId);
if (!scheduled) return;
const channel = await client.channels.fetch(scheduled.channelId);
if (!channel) return;
if (scheduled.type === 'message') {
await channel.send(scheduled.content);
} else if (scheduled.type === 'embed') {
const embed = new EmbedBuilder()
.setColor(scheduled.embedData.color)
.setTitle(scheduled.embedData.title)
.setDescription(scheduled.embedData.description)
.setTimestamp();
await channel.send({ embeds: [embed] });
}
client.scheduledMessages.delete(scheduleId);
if (supabase) {
await supabase
.from('scheduled_messages')
.update({ status: 'sent' })
.eq('id', scheduleId);
}
} catch (error) {
console.error('Send scheduled message error:', error);
}
}

View file

@ -3,51 +3,158 @@ const { SlashCommandBuilder, EmbedBuilder, ChannelType } = require('discord.js')
module.exports = {
data: new SlashCommandBuilder()
.setName('serverinfo')
.setDescription('View information about this server'),
.setDescription('View detailed information about this server'),
async execute(interaction, supabase, client) {
await interaction.deferReply();
const guild = interaction.guild;
await guild.members.fetch().catch(() => {});
const textChannels = guild.channels.cache.filter(c => c.type === ChannelType.GuildText).size;
const voiceChannels = guild.channels.cache.filter(c => c.type === ChannelType.GuildVoice).size;
const categories = guild.channels.cache.filter(c => c.type === ChannelType.GuildCategory).size;
const stageChannels = guild.channels.cache.filter(c => c.type === ChannelType.GuildStageVoice).size;
const forumChannels = guild.channels.cache.filter(c => c.type === ChannelType.GuildForum).size;
const roles = guild.roles.cache.size - 1;
const emojis = guild.emojis.cache.size;
const stickers = guild.stickers.cache.size;
const boostLevel = guild.premiumTier;
const boostCount = guild.premiumSubscriptionCount || 0;
const boostEmojis = ['', '🌙', '🌟', '✨'];
const totalMembers = guild.memberCount;
const onlineMembers = guild.members.cache.filter(m => m.presence?.status !== 'offline').size;
const botMembers = guild.members.cache.filter(m => m.user.bot).size;
const humanMembers = totalMembers - botMembers;
const owner = await guild.fetchOwner().catch(() => null);
const verificationLevels = {
0: 'None',
1: 'Low',
2: 'Medium',
3: 'High',
4: 'Highest'
};
const features = guild.features.slice(0, 5).map(f =>
f.split('_').map(w => w.charAt(0) + w.slice(1).toLowerCase()).join(' ')
);
const embed = new EmbedBuilder()
.setColor(0x7c3aed)
.setTitle(guild.name)
.setThumbnail(guild.iconURL({ size: 256 }))
.setAuthor({
name: guild.name,
iconURL: guild.iconURL({ size: 64 })
})
.setThumbnail(guild.iconURL({ size: 256, dynamic: true }))
.setDescription(guild.description || '*No server description*')
.addFields(
{ name: 'ID', value: guild.id, inline: true },
{ name: 'Owner', value: owner ? owner.user.tag : 'Unknown', inline: true },
{ name: 'Created', value: `<t:${Math.floor(guild.createdTimestamp / 1000)}:R>`, inline: true },
{ name: 'Members', value: `${guild.memberCount}`, inline: true },
{ name: 'Roles', value: `${roles}`, inline: true },
{ name: 'Emojis', value: `${emojis}`, inline: true },
{ name: 'Text Channels', value: `${textChannels}`, inline: true },
{ name: 'Voice Channels', value: `${voiceChannels}`, inline: true },
{ name: 'Categories', value: `${categories}`, inline: true },
{ name: 'Boost Level', value: `Tier ${boostLevel}`, inline: true },
{ name: 'Boosts', value: `${boostCount}`, inline: true },
{ name: 'Verification', value: guild.verificationLevel.toString(), inline: true }
{
name: '📋 General',
value: [
`**ID:** \`${guild.id}\``,
`**Owner:** ${owner ? owner.user.tag : 'Unknown'}`,
`**Created:** <t:${Math.floor(guild.createdTimestamp / 1000)}:R>`
].join('\n'),
inline: false
},
{
name: '👥 Members',
value: [
`**Total:** ${totalMembers.toLocaleString()}`,
`**Humans:** ${humanMembers.toLocaleString()}`,
`**Bots:** ${botMembers.toLocaleString()}`
].join('\n'),
inline: true
},
{
name: '💬 Channels',
value: [
`**Text:** ${textChannels}`,
`**Voice:** ${voiceChannels}`,
`**Categories:** ${categories}`,
forumChannels > 0 ? `**Forums:** ${forumChannels}` : null,
stageChannels > 0 ? `**Stages:** ${stageChannels}` : null
].filter(Boolean).join('\n'),
inline: true
},
{
name: '🎨 Assets',
value: [
`**Roles:** ${roles}`,
`**Emojis:** ${emojis}`,
`**Stickers:** ${stickers}`
].join('\n'),
inline: true
},
{
name: `${boostEmojis[boostLevel] || '💎'} Boost Status`,
value: [
`**Level:** ${boostLevel}/3`,
`**Boosts:** ${boostCount}`,
`**Progress:** ${getBoostProgress(boostLevel, boostCount)}`
].join('\n'),
inline: true
},
{
name: '🔒 Security',
value: [
`**Verification:** ${verificationLevels[guild.verificationLevel] || 'Unknown'}`,
`**2FA Required:** ${guild.mfaLevel === 1 ? 'Yes' : 'No'}`,
`**NSFW Level:** ${getNsfwLevel(guild.nsfwLevel)}`
].join('\n'),
inline: true
}
)
.setFooter({
text: `Requested by ${interaction.user.tag}`,
iconURL: interaction.user.displayAvatarURL({ size: 32 })
})
.setTimestamp();
if (guild.description) {
embed.setDescription(guild.description);
if (features.length > 0) {
embed.addFields({
name: '✨ Features',
value: features.map(f => `\`${f}\``).join(' • '),
inline: false
});
}
if (guild.bannerURL()) {
embed.setImage(guild.bannerURL({ size: 512 }));
}
await interaction.reply({ embeds: [embed] });
if (guild.splash) {
embed.addFields({
name: '🖼️ Splash',
value: `[View Invite Splash](${guild.splashURL({ size: 512 })})`,
inline: true
});
}
await interaction.editReply({ embeds: [embed] });
},
};
function getBoostProgress(level, count) {
const thresholds = [2, 7, 14];
if (level >= 3) return '✅ Max Level';
const needed = thresholds[level];
const progress = Math.min(100, Math.floor((count / needed) * 100));
return `${count}/${needed} (${progress}%)`;
}
function getNsfwLevel(level) {
const levels = {
0: 'Default',
1: 'Explicit',
2: 'Safe',
3: 'Age-Restricted'
};
return levels[level] || 'Unknown';
}

View file

@ -0,0 +1,185 @@
const { EmbedBuilder } = require('discord.js');
module.exports = {
name: 'messageCreate',
async execute(message, client, supabase) {
if (message.author.bot) return;
if (!message.guild) return;
const config = await getAutomodConfig(message.guild.id, client, supabase);
if (!config) return;
if (isExempt(message.member, config.exempt_roles)) return;
let violation = null;
if (config.links_enabled && containsLink(message.content)) {
violation = { type: 'link', action: config.links_action };
}
if (!violation && config.invites_enabled && containsInvite(message.content)) {
violation = { type: 'invite', action: 'delete' };
}
if (!violation && config.badwords?.length > 0 && containsBadWord(message.content, config.badwords)) {
violation = { type: 'badword', action: 'delete' };
}
if (!violation && config.mentions_enabled && exceedsMentionLimit(message, config.mentions_limit)) {
violation = { type: 'mentions', action: 'delete' };
}
if (!violation && config.spam_enabled) {
const isSpam = await checkSpam(message, client, config.spam_threshold);
if (isSpam) {
violation = { type: 'spam', action: 'timeout' };
}
}
if (violation) {
await handleViolation(message, violation, client, supabase);
}
}
};
async function getAutomodConfig(guildId, client, supabase) {
if (client.automodConfig?.has(guildId)) {
return client.automodConfig.get(guildId);
}
if (!supabase) return null;
try {
const { data, error } = await supabase
.from('automod_config')
.select('*')
.eq('guild_id', guildId)
.single();
if (error || !data) return null;
client.automodConfig = client.automodConfig || new Map();
client.automodConfig.set(guildId, data);
return data;
} catch {
return null;
}
}
function isExempt(member, exemptRoles) {
if (!member || !exemptRoles || exemptRoles.length === 0) return false;
if (member.permissions.has('Administrator')) return true;
return member.roles.cache.some(role => exemptRoles.includes(role.id));
}
function containsLink(content) {
const linkRegex = /https?:\/\/[^\s]+/gi;
return linkRegex.test(content);
}
function containsInvite(content) {
const inviteRegex = /(discord\.(gg|io|me|li)|discordapp\.com\/invite)\/[a-zA-Z0-9]+/gi;
return inviteRegex.test(content);
}
function containsBadWord(content, badwords) {
const lowerContent = content.toLowerCase();
return badwords.some(word => {
const regex = new RegExp(`\\b${escapeRegex(word)}\\b`, 'i');
return regex.test(lowerContent);
});
}
function escapeRegex(string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
function exceedsMentionLimit(message, limit) {
const mentions = message.mentions.users.size + message.mentions.roles.size;
return mentions > (limit || 5);
}
async function checkSpam(message, client, threshold) {
client.spamTracker = client.spamTracker || new Map();
const key = `${message.guild.id}-${message.author.id}`;
const now = Date.now();
const window = 5000;
let userMessages = client.spamTracker.get(key) || [];
userMessages = userMessages.filter(t => now - t < window);
userMessages.push(now);
client.spamTracker.set(key, userMessages);
return userMessages.length >= (threshold || 5);
}
async function handleViolation(message, violation, client, supabase) {
try {
await message.delete().catch(() => {});
const violationMessages = {
link: '🔗 Links are not allowed in this server.',
invite: '📨 Discord invites are not allowed.',
badword: '🚫 Your message contained a banned word.',
mentions: '📢 Too many mentions in your message.',
spam: '🔄 Slow down! You\'re sending messages too fast.'
};
const dmEmbed = new EmbedBuilder()
.setColor(0xef4444)
.setTitle('⚠️ Message Removed')
.setDescription(violationMessages[violation.type] || 'Your message violated server rules.')
.setFooter({ text: message.guild.name })
.setTimestamp();
await message.author.send({ embeds: [dmEmbed] }).catch(() => {});
if (violation.action === 'warn' && supabase) {
await supabase.from('warnings').insert({
guild_id: message.guild.id,
user_id: message.author.id,
user_tag: message.author.tag,
moderator_id: client.user.id,
moderator_tag: client.user.tag,
reason: `Auto-mod: ${violation.type} violation`
});
}
if (violation.action === 'timeout') {
await message.member.timeout(5 * 60 * 1000, `Auto-mod: ${violation.type} violation`).catch(() => {});
}
const { data: config } = await supabase
?.from('server_config')
.select('modlog_channel')
.eq('guild_id', message.guild.id)
.single() || { data: null };
if (config?.modlog_channel) {
const logChannel = await client.channels.fetch(config.modlog_channel).catch(() => null);
if (logChannel) {
const logEmbed = new EmbedBuilder()
.setColor(0xf97316)
.setTitle('🛡️ Auto-Mod Action')
.addFields(
{ name: 'User', value: `${message.author.tag} (${message.author.id})`, inline: true },
{ name: 'Violation', value: violation.type, inline: true },
{ name: 'Action', value: violation.action, inline: true },
{ name: 'Channel', value: `<#${message.channel.id}>`, inline: true }
)
.setFooter({ text: 'Auto-Moderation' })
.setTimestamp();
await logChannel.send({ embeds: [logEmbed] });
}
}
} catch (error) {
console.error('Auto-mod violation handling error:', error);
}
}

View file

@ -18,20 +18,62 @@ module.exports = {
const channel = await client.channels.fetch(config.goodbye_channel).catch(() => null);
if (!channel) return;
const membershipDuration = getMembershipDuration(member.joinedTimestamp);
const currentMemberCount = member.guild.memberCount;
const roles = member.roles.cache
.filter(r => r.id !== member.guild.id)
.sort((a, b) => b.position - a.position)
.map(r => r.name)
.slice(0, 5);
const goodbyeMessages = [
`**${member.user.tag}** has left the server.`,
`Goodbye, **${member.user.tag}**! We'll miss you!`,
`**${member.user.tag}** just left. Hope to see you again!`,
`**${member.user.tag}** has departed from the server.`,
];
const randomMessage = goodbyeMessages[Math.floor(Math.random() * goodbyeMessages.length)];
const embed = new EmbedBuilder()
.setColor(0xff6b6b)
.setTitle('Goodbye!')
.setDescription(`${member.user.tag} has left the server.`)
.setThumbnail(member.displayAvatarURL({ size: 256 }))
.setColor(0xef4444)
.setAuthor({
name: 'Member Left',
iconURL: member.guild.iconURL({ size: 64 })
})
.setDescription(randomMessage)
.setThumbnail(member.displayAvatarURL({ size: 512, dynamic: true }))
.addFields(
{ name: 'Members Now', value: `${member.guild.memberCount}`, inline: true },
{ name: 'Was Member For', value: member.joinedTimestamp
? `<t:${Math.floor(member.joinedTimestamp / 1000)}:R>`
: 'Unknown', inline: true }
{
name: '👤 User',
value: member.user.tag,
inline: true
},
{
name: '👥 Members Now',
value: currentMemberCount.toLocaleString(),
inline: true
},
{
name: '⏰ Was Member For',
value: membershipDuration,
inline: true
}
)
.setFooter({ text: `ID: ${member.id}` })
.setFooter({
text: `ID: ${member.id}`,
iconURL: member.displayAvatarURL({ size: 32 })
})
.setTimestamp();
if (roles.length > 0) {
embed.addFields({
name: '🎭 Had Roles',
value: roles.map(r => `\`${r}\``).join(', ') + (member.roles.cache.size > 6 ? ` +${member.roles.cache.size - 6} more` : ''),
inline: false
});
}
await channel.send({ embeds: [embed] });
} catch (error) {
@ -39,3 +81,37 @@ module.exports = {
}
}
};
function getMembershipDuration(joinedTimestamp) {
if (!joinedTimestamp) return 'Unknown';
const now = Date.now();
const diff = now - joinedTimestamp;
const seconds = Math.floor(diff / 1000);
const minutes = Math.floor(seconds / 60);
const hours = Math.floor(minutes / 60);
const days = Math.floor(hours / 24);
const months = Math.floor(days / 30);
const years = Math.floor(days / 365);
if (years > 0) {
const remainingMonths = months % 12;
return remainingMonths > 0
? `${years}y ${remainingMonths}mo`
: `${years} year${years > 1 ? 's' : ''}`;
} else if (months > 0) {
const remainingDays = days % 30;
return remainingDays > 0
? `${months}mo ${remainingDays}d`
: `${months} month${months > 1 ? 's' : ''}`;
} else if (days > 0) {
return `${days} day${days > 1 ? 's' : ''}`;
} else if (hours > 0) {
return `${hours} hour${hours > 1 ? 's' : ''}`;
} else if (minutes > 0) {
return `${minutes} minute${minutes > 1 ? 's' : ''}`;
} else {
return 'Just now';
}
}

View file

@ -1,9 +1,13 @@
const { EmbedBuilder } = require('discord.js');
const { EmbedBuilder, AttachmentBuilder } = require('discord.js');
module.exports = {
name: 'guildMemberAdd',
async execute(member, client, supabase) {
if (client.trackNewMember) {
client.trackNewMember();
}
if (!supabase) return;
try {
@ -25,18 +29,66 @@ module.exports = {
const channel = await client.channels.fetch(config.welcome_channel).catch(() => null);
if (!channel) return;
const memberNumber = member.guild.memberCount;
const accountAge = getAccountAge(member.user.createdTimestamp);
const isNewAccount = (Date.now() - member.user.createdTimestamp) < 7 * 24 * 60 * 60 * 1000;
const welcomeMessages = [
`Welcome to **${member.guild.name}**, ${member}! We're glad you're here!`,
`Hey ${member}, welcome to **${member.guild.name}**! Make yourself at home!`,
`${member} just joined **${member.guild.name}**! Welcome aboard!`,
`A wild ${member} appeared in **${member.guild.name}**! Welcome!`,
`${member} has arrived! Welcome to **${member.guild.name}**!`,
];
const randomMessage = welcomeMessages[Math.floor(Math.random() * welcomeMessages.length)];
const embed = new EmbedBuilder()
.setColor(0x7c3aed)
.setTitle('Welcome!')
.setDescription(`Welcome to **${member.guild.name}**, ${member}!`)
.setThumbnail(member.displayAvatarURL({ size: 256 }))
.setColor(0x22c55e)
.setAuthor({
name: 'Welcome to the server!',
iconURL: member.guild.iconURL({ size: 64 })
})
.setDescription(randomMessage)
.setThumbnail(member.displayAvatarURL({ size: 512, dynamic: true }))
.addFields(
{ name: 'Member #', value: `${member.guild.memberCount}`, inline: true },
{ name: 'Account Created', value: `<t:${Math.floor(member.user.createdTimestamp / 1000)}:R>`, inline: true }
{
name: '👤 Member',
value: `${member.user.tag}`,
inline: true
},
{
name: '🔢 Member #',
value: `${memberNumber.toLocaleString()}`,
inline: true
},
{
name: '📅 Account Age',
value: accountAge,
inline: true
}
)
.setFooter({ text: `ID: ${member.id}` })
.setFooter({
text: `ID: ${member.id}`,
iconURL: member.displayAvatarURL({ size: 32 })
})
.setTimestamp();
if (isNewAccount) {
embed.addFields({
name: '⚠️ Notice',
value: 'This is a new account (created within the last 7 days)',
inline: false
});
}
if (member.guild.rulesChannel) {
embed.addFields({
name: '📜 Get Started',
value: `Check out the rules in ${member.guild.rulesChannel}`,
inline: false
});
}
await channel.send({ embeds: [embed] });
}
@ -45,3 +97,22 @@ module.exports = {
}
}
};
function getAccountAge(createdTimestamp) {
const now = Date.now();
const diff = now - createdTimestamp;
const days = Math.floor(diff / (1000 * 60 * 60 * 24));
const months = Math.floor(days / 30);
const years = Math.floor(days / 365);
if (years > 0) {
return `${years} year${years > 1 ? 's' : ''} ago`;
} else if (months > 0) {
return `${months} month${months > 1 ? 's' : ''} ago`;
} else if (days > 0) {
return `${days} day${days > 1 ? 's' : ''} ago`;
} else {
return 'Today';
}
}