diff --git a/.replit b/.replit index 5199916..a3cd43c 100644 --- a/.replit +++ b/.replit @@ -23,7 +23,7 @@ localPort = 8080 externalPort = 8080 [[ports]] -localPort = 46469 +localPort = 45017 externalPort = 3000 [workflows] diff --git a/aethex-bot/bot.js b/aethex-bot/bot.js index 432d059..8ec819e 100644 --- a/aethex-bot/bot.js +++ b/aethex-bot/bot.js @@ -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); + } + } } }); diff --git a/aethex-bot/commands/announce.js b/aethex-bot/commands/announce.js index 9f8d8f9..e1d46a2 100644 --- a/aethex-bot/commands/announce.js +++ b/aethex-bot/commands/announce.js @@ -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); +} diff --git a/aethex-bot/commands/automod.js b/aethex-bot/commands/automod.js new file mode 100644 index 0000000..855956f --- /dev/null +++ b/aethex-bot/commands/automod.js @@ -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] }); +} diff --git a/aethex-bot/commands/embed.js b/aethex-bot/commands/embed.js new file mode 100644 index 0000000..085bc21 --- /dev/null +++ b/aethex-bot/commands/embed.js @@ -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); + }, +}; diff --git a/aethex-bot/commands/giveaway.js b/aethex-bot/commands/giveaway.js new file mode 100644 index 0000000..02a17a3 --- /dev/null +++ b/aethex-bot/commands/giveaway.js @@ -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: ``, 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 `) + .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: \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; diff --git a/aethex-bot/commands/help.js b/aethex-bot/commands/help.js index 324b1dd..f5d4d0f 100644 --- a/aethex-bot/commands/help.js +++ b/aethex-bot/commands/help.js @@ -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; diff --git a/aethex-bot/commands/leaderboard.js b/aethex-bot/commands/leaderboard.js index a7157d4..9438a6e 100644 --- a/aethex-bot/commands/leaderboard.js +++ b/aethex-bot/commands/leaderboard.js @@ -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); diff --git a/aethex-bot/commands/profile.js b/aethex-bot/commands/profile.js index d54f926..5e4aa71 100644 --- a/aethex-bot/commands/profile.js +++ b/aethex-bot/commands/profile.js @@ -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}]`; +} diff --git a/aethex-bot/commands/rolepanel.js b/aethex-bot/commands/rolepanel.js new file mode 100644 index 0000000..a487694 --- /dev/null +++ b/aethex-bot/commands/rolepanel.js @@ -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.' }); + } +} diff --git a/aethex-bot/commands/schedule.js b/aethex-bot/commands/schedule.js new file mode 100644 index 0000000..6df051b --- /dev/null +++ b/aethex-bot/commands/schedule.js @@ -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} `) + .addFields( + { name: 'ID', value: `\`${scheduleId}\``, inline: true }, + { name: 'Send Time', value: ``, 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} `) + .addFields( + { name: 'ID', value: `\`${scheduleId}\``, inline: true }, + { name: 'Send Time', value: ``, 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: `; + }).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); + } +} diff --git a/aethex-bot/commands/serverinfo.js b/aethex-bot/commands/serverinfo.js index 2cba9d7..9c5f5a8 100644 --- a/aethex-bot/commands/serverinfo.js +++ b/aethex-bot/commands/serverinfo.js @@ -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: ``, 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:** ` + ].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'; +} diff --git a/aethex-bot/listeners/automod.js b/aethex-bot/listeners/automod.js new file mode 100644 index 0000000..ff4b8ce --- /dev/null +++ b/aethex-bot/listeners/automod.js @@ -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); + } +} diff --git a/aethex-bot/listeners/goodbye.js b/aethex-bot/listeners/goodbye.js index 01b40d0..dd2627a 100644 --- a/aethex-bot/listeners/goodbye.js +++ b/aethex-bot/listeners/goodbye.js @@ -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 - ? `` - : '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'; + } +} diff --git a/aethex-bot/listeners/welcome.js b/aethex-bot/listeners/welcome.js index 34a0556..86955d4 100644 --- a/aethex-bot/listeners/welcome.js +++ b/aethex-bot/listeners/welcome.js @@ -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: ``, 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'; + } +}