diff --git a/aethex-bot/.env.example b/aethex-bot/.env.example new file mode 100644 index 0000000..64903c2 --- /dev/null +++ b/aethex-bot/.env.example @@ -0,0 +1,36 @@ +# Discord Bot Configuration (REQUIRED) +DISCORD_BOT_TOKEN=your_bot_token_here +DISCORD_CLIENT_ID=your_client_id_here +DISCORD_CLIENT_SECRET=your_client_secret_here + +# Supabase Database (REQUIRED for full features) +SUPABASE_URL=https://your-project.supabase.co +SUPABASE_SERVICE_ROLE=your_service_role_key + +# Stripe Payments (for premium/branding tiers) +STRIPE_SECRET_KEY=sk_live_xxx +STRIPE_WEBHOOK_SECRET=whsec_xxx +STRIPE_PRICE_BRANDING_BASIC=price_xxx +STRIPE_PRICE_BRANDING_PRO=price_xxx +STRIPE_PRICE_BRANDING_ENTERPRISE=price_xxx + +# OAuth & Web Dashboard +BASE_URL=https://bot.aethex.dev +SESSION_SECRET=random_secret_string + +# Optional: AeThex Realm Guild IDs +HUB_GUILD_ID= +LABS_GUILD_ID= +GAMEFORGE_GUILD_ID= +CORP_GUILD_ID= +FOUNDATION_GUILD_ID= + +# Optional: Alert Channel +ALERT_CHANNEL_ID= + +# Optional: Admin Access +WHITELISTED_USERS=user_id_1,user_id_2 + +# Server Ports +PORT=8080 +HEALTH_PORT=3000 diff --git a/aethex-bot/commands/branding.js b/aethex-bot/commands/branding.js new file mode 100644 index 0000000..d05294c --- /dev/null +++ b/aethex-bot/commands/branding.js @@ -0,0 +1,479 @@ +const { SlashCommandBuilder, EmbedBuilder, PermissionFlagsBits, ActionRowBuilder, ButtonBuilder, ButtonStyle, StringSelectMenuBuilder } = require('discord.js'); +const { getBranding, updateBranding, claimHandle, hasFeature, BRANDING_TIERS, createBrandedEmbed } = require('../utils/brandingManager'); + +module.exports = { + data: new SlashCommandBuilder() + .setName('branding') + .setDescription('Configure white-label branding for your server') + .setDefaultMemberPermissions(PermissionFlagsBits.Administrator) + .addSubcommand(sub => sub + .setName('view') + .setDescription('View current branding settings') + ) + .addSubcommand(sub => sub + .setName('name') + .setDescription('Set custom bot name (Basic tier+)') + .addStringOption(opt => opt + .setName('name') + .setDescription('Custom bot name (max 80 chars)') + .setRequired(true) + .setMaxLength(80) + ) + ) + .addSubcommand(sub => sub + .setName('avatar') + .setDescription('Set custom bot avatar (Pro tier+)') + .addStringOption(opt => opt + .setName('url') + .setDescription('Avatar image URL (must be https)') + .setRequired(true) + ) + ) + .addSubcommand(sub => sub + .setName('footer') + .setDescription('Set custom embed footer (Basic tier+)') + .addStringOption(opt => opt + .setName('text') + .setDescription('Footer text (max 200 chars)') + .setRequired(true) + .setMaxLength(200) + ) + ) + .addSubcommand(sub => sub + .setName('color') + .setDescription('Set custom embed color (Basic tier+)') + .addStringOption(opt => opt + .setName('hex') + .setDescription('Hex color (e.g., #ff5500)') + .setRequired(true) + ) + ) + .addSubcommand(sub => sub + .setName('handle') + .setDescription('Claim your custom URL handle (Pro tier+)') + .addStringOption(opt => opt + .setName('handle') + .setDescription('Your custom handle (aethex.dev/YOUR-HANDLE)') + .setRequired(true) + .setMinLength(3) + .setMaxLength(30) + ) + ) + .addSubcommand(sub => sub + .setName('landing') + .setDescription('Configure your landing page (Pro tier+)') + .addStringOption(opt => opt + .setName('title') + .setDescription('Page title') + .setMaxLength(100) + ) + .addStringOption(opt => opt + .setName('description') + .setDescription('Page description') + .setMaxLength(500) + ) + .addStringOption(opt => opt + .setName('banner') + .setDescription('Banner image URL') + ) + .addStringOption(opt => opt + .setName('invite') + .setDescription('Discord invite URL') + ) + ) + .addSubcommand(sub => sub + .setName('toggle') + .setDescription('Enable or disable branding') + .addBooleanOption(opt => opt + .setName('enabled') + .setDescription('Enable custom branding?') + .setRequired(true) + ) + ) + .addSubcommand(sub => sub + .setName('preview') + .setDescription('Preview your branded embeds') + ) + .addSubcommand(sub => sub + .setName('tiers') + .setDescription('View branding tier pricing and features') + ) + .addSubcommand(sub => sub + .setName('reset') + .setDescription('Reset branding to defaults') + ), + + async execute(interaction, supabase) { + if (!supabase) { + return interaction.reply({ content: 'Database not available.', ephemeral: true }); + } + + const subcommand = interaction.options.getSubcommand(); + const guildId = interaction.guildId; + + await interaction.deferReply({ ephemeral: true }); + + try { + const branding = await getBranding(supabase, guildId); + + // View current branding + if (subcommand === 'view') { + const tierInfo = BRANDING_TIERS[branding.tier || 'free']; + + const embed = new EmbedBuilder() + .setColor(branding.custom_embed_color || '#6366f1') + .setTitle('šŸ·ļø White-Label Branding Settings') + .setDescription( + `**Current Tier:** ${tierInfo.name} (${tierInfo.price > 0 ? `$${tierInfo.price}/mo` : 'Free'})\n` + + `**Status:** ${branding.branding_enabled ? 'āœ… Enabled' : 'āŒ Disabled'}` + ) + .addFields( + { name: 'Custom Bot Name', value: branding.custom_bot_name || '*Not set*', inline: true }, + { name: 'Custom Footer', value: branding.custom_footer_text || '*Not set*', inline: true }, + { name: 'Embed Color', value: branding.custom_embed_color || '*Default*', inline: true }, + { name: 'Custom Avatar', value: branding.custom_bot_avatar_url ? 'āœ… Set' : '*Not set*', inline: true }, + { name: 'Custom Handle', value: branding.custom_handle ? `aethex.dev/${branding.custom_handle}` : '*Not claimed*', inline: true }, + { name: 'Landing Page', value: branding.landing_title ? 'āœ… Configured' : '*Not set*', inline: true } + ) + .setFooter({ text: 'Use /branding tiers to see upgrade options' }) + .setTimestamp(); + + if (branding.custom_bot_avatar_url) { + embed.setThumbnail(branding.custom_bot_avatar_url); + } + + const row = new ActionRowBuilder() + .addComponents( + new ButtonBuilder() + .setLabel('Manage on Dashboard') + .setURL(`https://bot.aethex.dev/dashboard?guild=${guildId}&page=branding`) + .setStyle(ButtonStyle.Link), + new ButtonBuilder() + .setLabel('Upgrade Tier') + .setURL('https://bot.aethex.dev/pricing#branding') + .setStyle(ButtonStyle.Link) + ); + + return interaction.editReply({ embeds: [embed], components: [row] }); + } + + // View pricing tiers + if (subcommand === 'tiers') { + const embed = new EmbedBuilder() + .setColor('#6366f1') + .setTitle('šŸ·ļø White-Label Branding Tiers') + .setDescription('Customize Warden to match your community brand!') + .addFields( + { + name: 'šŸ†“ Free', + value: '• Default AeThex | Warden branding\n• All bot features included\n• No customization', + inline: true + }, + { + name: 'šŸ’Ž Basic - $15/mo', + value: '• Custom bot name\n• Custom footer text\n• Custom embed color\n• Removes "AeThex" branding', + inline: true + }, + { + name: '⭐ Pro - $35/mo', + value: '• Everything in Basic\n• Custom bot avatar\n• Custom URL handle\n• Landing page\n• `aethex.dev/your-name`', + inline: true + }, + { + name: 'šŸ† Enterprise - $75/mo', + value: '• Everything in Pro\n• Analytics dashboard\n• Priority support\n• Custom domain support\n• White-glove setup', + inline: false + } + ) + .setFooter({ text: 'All tiers include full bot functionality • Payments via Stripe' }); + + const row = new ActionRowBuilder() + .addComponents( + new ButtonBuilder() + .setLabel('Subscribe Now') + .setURL('https://bot.aethex.dev/pricing#branding') + .setStyle(ButtonStyle.Link) + .setEmoji('šŸ’³') + ); + + return interaction.editReply({ embeds: [embed], components: [row] }); + } + + // Set custom name + if (subcommand === 'name') { + if (!hasFeature(branding.tier, 'custom_name')) { + return interaction.editReply({ + content: 'āŒ Custom bot names require **Basic tier** or higher. Use `/branding tiers` to see pricing.', + ephemeral: true + }); + } + + const name = interaction.options.getString('name'); + const result = await updateBranding(supabase, guildId, { custom_bot_name: name }, interaction.user.id); + + if (result.success) { + return interaction.editReply({ + embeds: [ + new EmbedBuilder() + .setColor('#22c55e') + .setTitle('āœ… Bot Name Updated') + .setDescription(`Bot will now appear as **${name}** in branded messages.`) + .setFooter({ text: 'Make sure branding is enabled with /branding toggle' }) + ] + }); + } + return interaction.editReply({ content: `āŒ Failed: ${result.error}` }); + } + + // Set custom avatar + if (subcommand === 'avatar') { + if (!hasFeature(branding.tier, 'custom_avatar')) { + return interaction.editReply({ + content: 'āŒ Custom avatars require **Pro tier** or higher. Use `/branding tiers` to see pricing.', + ephemeral: true + }); + } + + const url = interaction.options.getString('url'); + + // Basic URL validation + if (!url.startsWith('https://')) { + return interaction.editReply({ content: 'āŒ Avatar URL must start with https://' }); + } + + const result = await updateBranding(supabase, guildId, { custom_bot_avatar_url: url }, interaction.user.id); + + if (result.success) { + return interaction.editReply({ + embeds: [ + new EmbedBuilder() + .setColor('#22c55e') + .setTitle('āœ… Bot Avatar Updated') + .setDescription('Bot will now use your custom avatar in branded messages.') + .setThumbnail(url) + .setFooter({ text: 'Webhook-based messages will show custom avatar' }) + ] + }); + } + return interaction.editReply({ content: `āŒ Failed: ${result.error}` }); + } + + // Set custom footer + if (subcommand === 'footer') { + if (!hasFeature(branding.tier, 'custom_footer')) { + return interaction.editReply({ + content: 'āŒ Custom footers require **Basic tier** or higher. Use `/branding tiers` to see pricing.', + ephemeral: true + }); + } + + const text = interaction.options.getString('text'); + const result = await updateBranding(supabase, guildId, { custom_footer_text: text }, interaction.user.id); + + if (result.success) { + return interaction.editReply({ + embeds: [ + new EmbedBuilder() + .setColor('#22c55e') + .setTitle('āœ… Footer Updated') + .setDescription('Embeds will now show your custom footer.') + .setFooter({ text: text }) + ] + }); + } + return interaction.editReply({ content: `āŒ Failed: ${result.error}` }); + } + + // Set custom color + if (subcommand === 'color') { + if (!hasFeature(branding.tier, 'custom_color')) { + return interaction.editReply({ + content: 'āŒ Custom colors require **Basic tier** or higher. Use `/branding tiers` to see pricing.', + ephemeral: true + }); + } + + let hex = interaction.options.getString('hex'); + + // Validate and normalize hex color + if (!hex.startsWith('#')) hex = '#' + hex; + if (!/^#[0-9A-Fa-f]{6}$/.test(hex)) { + return interaction.editReply({ content: 'āŒ Invalid hex color. Use format: #ff5500' }); + } + + const result = await updateBranding(supabase, guildId, { custom_embed_color: hex }, interaction.user.id); + + if (result.success) { + return interaction.editReply({ + embeds: [ + new EmbedBuilder() + .setColor(hex) + .setTitle('āœ… Embed Color Updated') + .setDescription(`Embeds will now use **${hex}** as the accent color.`) + ] + }); + } + return interaction.editReply({ content: `āŒ Failed: ${result.error}` }); + } + + // Claim custom handle + if (subcommand === 'handle') { + const handle = interaction.options.getString('handle'); + const result = await claimHandle(supabase, guildId, handle, branding.tier); + + if (result.success) { + return interaction.editReply({ + embeds: [ + new EmbedBuilder() + .setColor('#22c55e') + .setTitle('āœ… Handle Claimed!') + .setDescription(`Your community page is now live at:\n\nšŸ”— **https://bot.aethex.dev/${result.handle}**`) + .addFields( + { name: 'Configure Your Page', value: 'Use `/branding landing` to customize your page content.' } + ) + .setFooter({ text: 'Share this link to promote your server!' }) + ] + }); + } + return interaction.editReply({ content: `āŒ ${result.error}` }); + } + + // Configure landing page + if (subcommand === 'landing') { + if (!hasFeature(branding.tier, 'landing_page')) { + return interaction.editReply({ + content: 'āŒ Landing pages require **Pro tier** or higher. Use `/branding tiers` to see pricing.', + ephemeral: true + }); + } + + if (!branding.custom_handle) { + return interaction.editReply({ + content: 'āŒ You must claim a handle first with `/branding handle`', + ephemeral: true + }); + } + + const updates = {}; + const title = interaction.options.getString('title'); + const description = interaction.options.getString('description'); + const banner = interaction.options.getString('banner'); + const invite = interaction.options.getString('invite'); + + if (title) updates.landing_title = title; + if (description) updates.landing_description = description; + if (banner) updates.landing_banner_url = banner; + if (invite) updates.landing_invite_url = invite; + + if (Object.keys(updates).length === 0) { + return interaction.editReply({ + content: 'Please provide at least one option to update.', + ephemeral: true + }); + } + + const result = await updateBranding(supabase, guildId, updates, interaction.user.id); + + if (result.success) { + return interaction.editReply({ + embeds: [ + new EmbedBuilder() + .setColor('#22c55e') + .setTitle('āœ… Landing Page Updated') + .setDescription(`View your page at:\nšŸ”— **https://bot.aethex.dev/${branding.custom_handle}**`) + .addFields( + Object.entries(updates).map(([key, value]) => ({ + name: key.replace('landing_', '').replace('_', ' '), + value: value.length > 50 ? value.substring(0, 50) + '...' : value, + inline: true + })) + ) + ] + }); + } + return interaction.editReply({ content: `āŒ Failed: ${result.error}` }); + } + + // Toggle branding + if (subcommand === 'toggle') { + const enabled = interaction.options.getBoolean('enabled'); + + if (enabled && branding.tier === 'free') { + return interaction.editReply({ + content: 'āŒ Custom branding requires a paid tier. Use `/branding tiers` to see pricing.', + ephemeral: true + }); + } + + const result = await updateBranding(supabase, guildId, { branding_enabled: enabled }, interaction.user.id); + + if (result.success) { + return interaction.editReply({ + embeds: [ + new EmbedBuilder() + .setColor(enabled ? '#22c55e' : '#ef4444') + .setTitle(enabled ? 'āœ… Branding Enabled' : 'āŒ Branding Disabled') + .setDescription( + enabled + ? 'Bot messages will now use your custom branding!' + : 'Bot messages will show default AeThex | Warden branding.' + ) + ] + }); + } + return interaction.editReply({ content: `āŒ Failed: ${result.error}` }); + } + + // Preview branded embed + if (subcommand === 'preview') { + const previewEmbed = await createBrandedEmbed(supabase, guildId, { + title: 'šŸŽ‰ Sample Announcement', + description: 'This is how your branded embeds will look!\n\nNotice the custom color and footer below.', + fields: [ + { name: 'Feature', value: 'Custom branding', inline: true }, + { name: 'Status', value: branding.branding_enabled ? 'āœ… Active' : 'āŒ Inactive', inline: true } + ], + timestamp: true + }); + + return interaction.editReply({ + content: branding.branding_enabled + ? '**Preview of your branded embed:**' + : '**Preview (branding not enabled - showing defaults):**', + embeds: [previewEmbed] + }); + } + + // Reset branding + if (subcommand === 'reset') { + const result = await updateBranding(supabase, guildId, { + custom_bot_name: null, + custom_bot_avatar_url: null, + custom_footer_text: null, + custom_embed_color: null, + landing_title: null, + landing_description: null, + landing_banner_url: null, + branding_enabled: false + }, interaction.user.id); + + if (result.success) { + return interaction.editReply({ + embeds: [ + new EmbedBuilder() + .setColor('#f59e0b') + .setTitle('šŸ”„ Branding Reset') + .setDescription('All custom branding has been cleared. Default AeThex | Warden branding restored.') + .setFooter({ text: 'Your custom handle has been preserved' }) + ] + }); + } + return interaction.editReply({ content: `āŒ Failed: ${result.error}` }); + } + + } catch (error) { + console.error('[Branding Command] Error:', error); + return interaction.editReply({ content: 'āŒ An error occurred. Please try again.' }); + } + } +}; diff --git a/aethex-bot/listeners/automod.js b/aethex-bot/listeners/automod.js index ff4b8ce..a86c5c8 100644 --- a/aethex-bot/listeners/automod.js +++ b/aethex-bot/listeners/automod.js @@ -1,4 +1,5 @@ const { EmbedBuilder } = require('discord.js'); +const { getEffectiveFooter, getEffectiveColor } = require('../utils/brandingManager'); module.exports = { name: 'messageCreate', @@ -130,11 +131,17 @@ async function handleViolation(message, violation, client, supabase) { spam: 'šŸ”„ Slow down! You\'re sending messages too fast.' }; + // Get branded footer and color for embeds + const brandedFooter = await getEffectiveFooter(supabase, message.guild.id); + const brandedColorObj = await getEffectiveColor(supabase, message.guild.id); + const embedColor = brandedColorObj.color || 0xef4444; + const logEmbedColor = brandedColorObj.color || 0xf97316; + const dmEmbed = new EmbedBuilder() - .setColor(0xef4444) + .setColor(embedColor) .setTitle('āš ļø Message Removed') .setDescription(violationMessages[violation.type] || 'Your message violated server rules.') - .setFooter({ text: message.guild.name }) + .setFooter({ text: brandedFooter }) .setTimestamp(); await message.author.send({ embeds: [dmEmbed] }).catch(() => {}); @@ -164,7 +171,7 @@ async function handleViolation(message, violation, client, supabase) { const logChannel = await client.channels.fetch(config.modlog_channel).catch(() => null); if (logChannel) { const logEmbed = new EmbedBuilder() - .setColor(0xf97316) + .setColor(logEmbedColor) .setTitle('šŸ›”ļø Auto-Mod Action') .addFields( { name: 'User', value: `${message.author.tag} (${message.author.id})`, inline: true }, @@ -172,7 +179,7 @@ async function handleViolation(message, violation, client, supabase) { { name: 'Action', value: violation.action, inline: true }, { name: 'Channel', value: `<#${message.channel.id}>`, inline: true } ) - .setFooter({ text: 'Auto-Moderation' }) + .setFooter({ text: brandedFooter }) .setTimestamp(); await logChannel.send({ embeds: [logEmbed] }); diff --git a/aethex-bot/listeners/goodbye.js b/aethex-bot/listeners/goodbye.js index dd2627a..9cffa2b 100644 --- a/aethex-bot/listeners/goodbye.js +++ b/aethex-bot/listeners/goodbye.js @@ -1,4 +1,5 @@ const { EmbedBuilder } = require('discord.js'); +const { getEffectiveFooter, getEffectiveColor, sendBrandedMessage } = require('../utils/brandingManager'); module.exports = { name: 'guildMemberRemove', @@ -35,8 +36,13 @@ module.exports = { ]; const randomMessage = goodbyeMessages[Math.floor(Math.random() * goodbyeMessages.length)]; + // Get branded footer and color + const brandedFooter = await getEffectiveFooter(supabase, member.guild.id); + const brandedColorObj = await getEffectiveColor(supabase, member.guild.id); + const embedColor = brandedColorObj.color || 0xef4444; + const embed = new EmbedBuilder() - .setColor(0xef4444) + .setColor(embedColor) .setAuthor({ name: 'Member Left', iconURL: member.guild.iconURL({ size: 64 }) @@ -61,7 +67,7 @@ module.exports = { } ) .setFooter({ - text: `ID: ${member.id}`, + text: brandedFooter, iconURL: member.displayAvatarURL({ size: 32 }) }) .setTimestamp(); @@ -74,7 +80,8 @@ module.exports = { }); } - await channel.send({ embeds: [embed] }); + // Use branded message for Pro+ tiers with custom bot name/avatar + await sendBrandedMessage(supabase, channel, { embeds: [embed] }, member.guild.id); } catch (error) { console.error('Goodbye error:', error.message); diff --git a/aethex-bot/listeners/welcome.js b/aethex-bot/listeners/welcome.js index 92bde63..4f88c17 100644 --- a/aethex-bot/listeners/welcome.js +++ b/aethex-bot/listeners/welcome.js @@ -1,5 +1,6 @@ const { EmbedBuilder, AttachmentBuilder } = require('discord.js'); const { generateWelcomeCard } = require('../utils/welcomeCard'); +const { getEffectiveFooter, getEffectiveColor, sendBrandedMessage } = require('../utils/brandingManager'); module.exports = { name: 'guildMemberAdd', @@ -56,10 +57,16 @@ module.exports = { ]; const randomMessage = welcomeMessages[Math.floor(Math.random() * welcomeMessages.length)]; + // Get branded footer and color + const brandedFooter = await getEffectiveFooter(supabase, member.guild.id); + const brandedColorObj = await getEffectiveColor(supabase, member.guild.id); + const embedColor = brandedColorObj.color || parseInt((config.welcome_card_accent_color || '#7c3aed').replace('#', ''), 16); + const embed = new EmbedBuilder() - .setColor(parseInt((config.welcome_card_accent_color || '#7c3aed').replace('#', ''), 16)) + .setColor(embedColor) .setDescription(randomMessage) - .setImage('attachment://welcome.png'); + .setImage('attachment://welcome.png') + .setFooter({ text: brandedFooter }); if (isNewAccount) { embed.addFields({ @@ -77,7 +84,8 @@ module.exports = { }); } - await channel.send({ embeds: [embed], files: [attachment] }); + // Use branded message for Pro+ tiers with custom bot name/avatar + await sendBrandedMessage(supabase, channel, { embeds: [embed], files: [attachment] }, member.guild.id); return; } catch (cardError) { console.error('Welcome card generation failed, falling back to text:', cardError.message); @@ -93,8 +101,13 @@ module.exports = { ]; const randomMessage = welcomeMessages[Math.floor(Math.random() * welcomeMessages.length)]; + // Get branded footer and color + const brandedFooter = await getEffectiveFooter(supabase, member.guild.id); + const brandedColorObj = await getEffectiveColor(supabase, member.guild.id); + const embedColor = brandedColorObj.color || 0x22c55e; + const embed = new EmbedBuilder() - .setColor(0x22c55e) + .setColor(embedColor) .setAuthor({ name: 'Welcome to the server!', iconURL: member.guild.iconURL({ size: 64 }) @@ -119,7 +132,7 @@ module.exports = { } ) .setFooter({ - text: `ID: ${member.id}`, + text: brandedFooter, iconURL: member.displayAvatarURL({ size: 32 }) }) .setTimestamp(); @@ -140,7 +153,8 @@ module.exports = { }); } - await channel.send({ embeds: [embed] }); + // Use branded message for Pro+ tiers with custom bot name/avatar + await sendBrandedMessage(supabase, channel, { embeds: [embed] }, member.guild.id); } } catch (error) { diff --git a/aethex-bot/listeners/xpTracker.js b/aethex-bot/listeners/xpTracker.js index a89f093..0356e2f 100644 --- a/aethex-bot/listeners/xpTracker.js +++ b/aethex-bot/listeners/xpTracker.js @@ -3,6 +3,7 @@ const { checkAchievements } = require('../commands/achievements'); const { getServerMode, getEmbedColor } = require('../utils/modeHelper'); const { updateStandaloneXp, calculateLevel: standaloneCalcLevel } = require('../utils/standaloneXp'); const { getActiveSeasonalEvents, getSeasonalMultiplier, getSeasonalBonusCoins } = require('../utils/seasonalEvents'); +const { getEffectiveFooter, getEffectiveColor, sendBrandedMessage } = require('../utils/brandingManager'); const xpCooldowns = new Map(); const xpConfigCache = new Map(); @@ -223,7 +224,7 @@ module.exports = { stats.dailyStreak = fullProfile?.daily_streak || 0; if (newLevel > oldLevel) { - await sendLevelUpAnnouncement(message, newLevel, newXp, config, client); + await sendLevelUpAnnouncement(message, newLevel, newXp, config, client, supabase); await checkMilestoneRoles(message.member, { level: newLevel, prestige: prestige, @@ -255,7 +256,7 @@ module.exports = { } }; -async function sendLevelUpAnnouncement(message, newLevel, newXp, config, client) { +async function sendLevelUpAnnouncement(message, newLevel, newXp, config, client, supabase) { try { const messageTemplate = config.levelup_message || 'šŸŽ‰ Congratulations {user}! You reached **Level {level}**!'; const channelId = config.levelup_channel_id; @@ -270,13 +271,19 @@ async function sendLevelUpAnnouncement(message, newLevel, newXp, config, client) .replace(/{xp}/g, newXp.toLocaleString()) .replace(/{server}/g, message.guild.name); + // Get branded footer and color + const brandedFooter = await getEffectiveFooter(supabase, message.guild.id); + const brandedColorObj = await getEffectiveColor(supabase, message.guild.id); + const finalColor = brandedColorObj.color || parseInt(embedColor.replace('#', ''), 16); + let messageContent; if (useEmbed) { const embed = new EmbedBuilder() .setDescription(formattedMessage) - .setColor(parseInt(embedColor.replace('#', ''), 16)) + .setColor(finalColor) .setThumbnail(message.author.displayAvatarURL({ dynamic: true })) + .setFooter({ text: brandedFooter }) .setTimestamp(); messageContent = { embeds: [embed] }; @@ -287,17 +294,17 @@ async function sendLevelUpAnnouncement(message, newLevel, newXp, config, client) if (sendDm) { const dmSent = await message.author.send(messageContent).catch(() => null); if (!dmSent) { - await message.channel.send(messageContent).catch(() => {}); + await sendBrandedMessage(supabase, message.channel, messageContent, message.guild.id); } } else if (channelId) { const channel = await client.channels.fetch(channelId).catch(() => null); if (channel) { - await channel.send(messageContent).catch(() => {}); + await sendBrandedMessage(supabase, channel, messageContent, message.guild.id); } else { - await message.channel.send(messageContent).catch(() => {}); + await sendBrandedMessage(supabase, message.channel, messageContent, message.guild.id); } } else { - await message.channel.send(messageContent).catch(() => {}); + await sendBrandedMessage(supabase, message.channel, messageContent, message.guild.id); } } catch (error) { console.error('Level-up announcement error:', error.message); @@ -643,7 +650,7 @@ async function handleStandaloneXp(message, client, supabase, config, discordUser // Level up announcement for standalone if (newLevel > oldLevel) { - await sendStandaloneLevelUp(message, newLevel, newXp, config, client); + await sendStandaloneLevelUp(message, newLevel, newXp, config, client, supabase); await checkMilestoneRolesStandalone(message.member, { level: newLevel, prestige: prestige, @@ -656,7 +663,7 @@ async function handleStandaloneXp(message, client, supabase, config, discordUser } } -async function sendStandaloneLevelUp(message, newLevel, newXp, config, client) { +async function sendStandaloneLevelUp(message, newLevel, newXp, config, client, supabase) { try { const messageTemplate = config.levelup_message || 'šŸŽ‰ Congratulations {user}! You reached **Level {level}**!'; const channelId = config.levelup_channel_id; @@ -671,14 +678,20 @@ async function sendStandaloneLevelUp(message, newLevel, newXp, config, client) { .replace(/{xp}/g, newXp.toLocaleString()) .replace(/{server}/g, message.guild.name); + // Get branded footer and color + const brandedFooter = await getEffectiveFooter(supabase, message.guild.id); + const brandedColorObj = await getEffectiveColor(supabase, message.guild.id); + const finalColor = brandedColorObj.color || parseInt(embedColor.replace('#', ''), 16); + const finalFooter = brandedFooter !== 'Powered by AeThex' ? brandedFooter : 'šŸ  Standalone Mode'; + let messageContent; if (useEmbed) { const embed = new EmbedBuilder() .setDescription(formattedMessage) - .setColor(parseInt(embedColor.replace('#', ''), 16)) + .setColor(finalColor) .setThumbnail(message.author.displayAvatarURL({ dynamic: true })) - .setFooter({ text: 'šŸ  Standalone Mode' }) + .setFooter({ text: finalFooter }) .setTimestamp(); messageContent = { embeds: [embed] }; @@ -689,17 +702,17 @@ async function sendStandaloneLevelUp(message, newLevel, newXp, config, client) { if (sendDm) { const dmSent = await message.author.send(messageContent).catch(() => null); if (!dmSent) { - await message.channel.send(messageContent).catch(() => {}); + await sendBrandedMessage(supabase, message.channel, messageContent, message.guild.id); } } else if (channelId) { const channel = await client.channels.fetch(channelId).catch(() => null); if (channel) { - await channel.send(messageContent).catch(() => {}); + await sendBrandedMessage(supabase, channel, messageContent, message.guild.id); } else { - await message.channel.send(messageContent).catch(() => {}); + await sendBrandedMessage(supabase, message.channel, messageContent, message.guild.id); } } else { - await message.channel.send(messageContent).catch(() => {}); + await sendBrandedMessage(supabase, message.channel, messageContent, message.guild.id); } } catch (error) { console.error('Standalone level-up announcement error:', error.message); diff --git a/aethex-bot/migrations/add_whitelabel_branding.sql b/aethex-bot/migrations/add_whitelabel_branding.sql new file mode 100644 index 0000000..da7190e --- /dev/null +++ b/aethex-bot/migrations/add_whitelabel_branding.sql @@ -0,0 +1,102 @@ +-- White-Label Branding System Migration +-- Allows paying servers to customize bot identity and get custom handles + +-- Server branding configuration +CREATE TABLE IF NOT EXISTS server_branding ( + id SERIAL PRIMARY KEY, + guild_id VARCHAR(32) UNIQUE NOT NULL, + + -- Custom Identity + custom_bot_name VARCHAR(80), + custom_bot_avatar_url VARCHAR(512), + custom_footer_text VARCHAR(200), + custom_embed_color VARCHAR(10), + + -- Custom Handle (aethex.dev/{handle}) + custom_handle VARCHAR(50) UNIQUE, + handle_verified BOOLEAN DEFAULT false, + + -- Landing Page Content + landing_title VARCHAR(100), + landing_description TEXT, + landing_banner_url VARCHAR(512), + landing_invite_url VARCHAR(200), + landing_website_url VARCHAR(200), + landing_social_links JSONB DEFAULT '{}', + landing_features JSONB DEFAULT '[]', + landing_theme VARCHAR(20) DEFAULT 'default', + + -- Branding Tier + tier VARCHAR(20) DEFAULT 'free', -- free, basic, pro, enterprise + branding_enabled BOOLEAN DEFAULT false, + + -- Subscription + subscription_id VARCHAR(100), + subscription_status VARCHAR(20), + subscription_started_at TIMESTAMP WITH TIME ZONE, + subscription_expires_at TIMESTAMP WITH TIME ZONE, + + -- Metadata + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + created_by VARCHAR(32) +); + +-- Indexes for fast lookups +CREATE INDEX IF NOT EXISTS idx_branding_handle ON server_branding(custom_handle) WHERE custom_handle IS NOT NULL; +CREATE INDEX IF NOT EXISTS idx_branding_tier ON server_branding(tier); +CREATE INDEX IF NOT EXISTS idx_branding_enabled ON server_branding(branding_enabled); + +-- Branding analytics (track custom landing page visits) +CREATE TABLE IF NOT EXISTS branding_analytics ( + id SERIAL PRIMARY KEY, + guild_id VARCHAR(32) NOT NULL, + event_type VARCHAR(50) NOT NULL, -- page_view, invite_click, social_click + referrer VARCHAR(512), + user_agent VARCHAR(512), + ip_hash VARCHAR(64), -- hashed for privacy + metadata JSONB DEFAULT '{}', + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); + +CREATE INDEX IF NOT EXISTS idx_branding_analytics_guild ON branding_analytics(guild_id); +CREATE INDEX IF NOT EXISTS idx_branding_analytics_date ON branding_analytics(created_at); + +-- Reserved handles (prevent abuse) +CREATE TABLE IF NOT EXISTS reserved_handles ( + id SERIAL PRIMARY KEY, + handle VARCHAR(50) UNIQUE NOT NULL, + reason VARCHAR(200), + reserved_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); + +-- Insert reserved handles +INSERT INTO reserved_handles (handle, reason) VALUES + ('admin', 'System reserved'), + ('api', 'System reserved'), + ('dashboard', 'System reserved'), + ('login', 'System reserved'), + ('auth', 'System reserved'), + ('oauth', 'System reserved'), + ('federation', 'System reserved'), + ('pricing', 'System reserved'), + ('features', 'System reserved'), + ('commands', 'System reserved'), + ('support', 'System reserved'), + ('help', 'System reserved'), + ('aethex', 'Brand reserved'), + ('warden', 'Brand reserved'), + ('official', 'Brand reserved'), + ('staff', 'Brand reserved'), + ('mod', 'Brand reserved'), + ('moderator', 'Brand reserved') +ON CONFLICT (handle) DO NOTHING; + +-- White-label pricing tiers reference: +-- Basic ($15/mo): Custom bot name, footer, embed color +-- Pro ($35/mo): + Custom avatar, custom handle, landing page +-- Enterprise ($75/mo): + Analytics, priority support, custom domain support + +COMMENT ON TABLE server_branding IS 'White-label branding configuration for servers'; +COMMENT ON COLUMN server_branding.custom_handle IS 'Custom URL handle at aethex.dev/{handle}'; +COMMENT ON COLUMN server_branding.tier IS 'Branding tier: free, basic, pro, enterprise'; diff --git a/aethex-bot/package-lock.json b/aethex-bot/package-lock.json index a7f63b5..40a6c66 100644 --- a/aethex-bot/package-lock.json +++ b/aethex-bot/package-lock.json @@ -13,13 +13,20 @@ "@discord/embedded-app-sdk": "^2.4.0", "@supabase/supabase-js": "^2.38.0", "axios": "^1.6.0", + "canvas": "^2.11.2", + "cookie-parser": "^1.4.6", + "cors": "^2.8.5", "discord-player": "^7.1.0", "discord.js": "^14.13.0", "dotenv": "^16.3.1", + "express": "^4.18.2", + "express-session": "^1.17.3", "kazagumo": "^3.4.0", "mediaplex": "^1.0.0", + "openai": "^4.20.0", "shoukaku": "^4.2.0", "sodium-native": "^5.0.10", + "stripe": "^14.0.0", "ws": "^8.18.3" }, "devDependencies": { @@ -668,6 +675,46 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@mapbox/node-pre-gyp": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", + "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==", + "license": "BSD-3-Clause", + "dependencies": { + "detect-libc": "^2.0.0", + "https-proxy-agent": "^5.0.0", + "make-dir": "^3.1.0", + "node-fetch": "^2.6.7", + "nopt": "^5.0.0", + "npmlog": "^5.0.1", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.11" + }, + "bin": { + "node-pre-gyp": "bin/node-pre-gyp" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.53.3", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.3.tgz", @@ -1103,6 +1150,16 @@ "undici-types": "~7.16.0" } }, + "node_modules/@types/node-fetch": { + "version": "2.6.13", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.13.tgz", + "integrity": "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "form-data": "^4.0.4" + } + }, "node_modules/@types/phoenix": { "version": "1.6.6", "resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.6.tgz", @@ -1143,6 +1200,12 @@ "node": ">=10.0.0" } }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "license": "ISC" + }, "node_modules/abort-controller": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", @@ -1155,6 +1218,19 @@ "node": ">=6.5" } }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/acorn": { "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", @@ -1167,6 +1243,39 @@ "node": ">=0.4.0" } }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/agentkeepalive": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", + "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", + "license": "MIT", + "dependencies": { + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/any-promise": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", @@ -1187,6 +1296,46 @@ "node": ">= 8" } }, + "node_modules/aproba": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.1.0.tgz", + "integrity": "sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==", + "license": "ISC" + }, + "node_modules/are-we-there-yet": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", + "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/are-we-there-yet/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -1208,7 +1357,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, "license": "MIT" }, "node_modules/bare-addon-resolve": { @@ -1294,6 +1442,45 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/body-parser": { + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", + "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.14.0", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, "node_modules/boolbase": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", @@ -1304,7 +1491,6 @@ "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -1363,6 +1549,15 @@ "esbuild": ">=0.18" } }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/cac": { "version": "6.7.14", "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", @@ -1385,6 +1580,37 @@ "node": ">= 0.4" } }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/canvas": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/canvas/-/canvas-2.11.2.tgz", + "integrity": "sha512-ItanGBMrmRV7Py2Z+Xhs7cT+FNt5K0vPL4p9EZ/UX/Mu7hFbkxSjKF2KVtPwX7UYWp7dRKnrTvReflgrItJbdw==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@mapbox/node-pre-gyp": "^1.0.0", + "nan": "^2.17.0", + "simple-get": "^3.0.3" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -1410,6 +1636,24 @@ "fsevents": "~2.3.2" } }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "license": "ISC", + "bin": { + "color-support": "bin.js" + } + }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -1435,7 +1679,6 @@ "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, "license": "MIT" }, "node_modules/confbox": { @@ -1453,6 +1696,78 @@ "node": "^14.18.0 || >=16.10.0" } }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", + "license": "ISC" + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-parser": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz", + "integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==", + "license": "MIT", + "dependencies": { + "cookie": "0.7.2", + "cookie-signature": "1.0.6" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/css-select": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", @@ -1513,6 +1828,18 @@ "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", "license": "MIT" }, + "node_modules/decompress-response": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-4.2.1.tgz", + "integrity": "sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==", + "license": "MIT", + "dependencies": { + "mimic-response": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -1522,6 +1849,40 @@ "node": ">=0.4.0" } }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "license": "MIT" + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, "node_modules/discord-api-types": { "version": "0.38.36", "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.38.36.tgz", @@ -1674,6 +2035,27 @@ "node": ">= 0.4" } }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/entities": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", @@ -1773,6 +2155,21 @@ "@esbuild/win32-x64": "0.27.1" } }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/event-target-shim": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", @@ -1797,6 +2194,111 @@ "node": ">=0.8.x" } }, + "node_modules/express": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", + "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.3", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.14.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-session": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.19.0.tgz", + "integrity": "sha512-0csaMkGq+vaiZTmSMMGkfdCOabYv192VbytFypcvI0MANrp+4i/7yEkJ0sbAEhycQjntaKGzYfjfXQyVb7BHMA==", + "license": "MIT", + "dependencies": { + "cookie": "~0.7.2", + "cookie-signature": "~1.0.7", + "debug": "~2.6.9", + "depd": "~2.0.0", + "on-headers": "~1.1.0", + "parseurl": "~1.3.3", + "safe-buffer": "~5.2.1", + "uid-safe": "~2.1.5" + }, + "engines": { + "node": ">= 0.8.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-session/node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/express-session/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express-session/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -1856,6 +2358,39 @@ "node": ">=8" } }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, "node_modules/fix-dts-default-cjs-exports": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/fix-dts-default-cjs-exports/-/fix-dts-default-cjs-exports-1.0.1.tgz", @@ -1903,6 +2438,34 @@ "node": ">= 6" } }, + "node_modules/form-data-encoder": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.2.tgz", + "integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==", + "license": "MIT" + }, + "node_modules/formdata-node": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz", + "integrity": "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==", + "license": "MIT", + "dependencies": { + "node-domexception": "1.0.0", + "web-streams-polyfill": "4.0.0-beta.3" + }, + "engines": { + "node": ">= 12.20" + } + }, + "node_modules/formdata-node/node_modules/web-streams-polyfill": { + "version": "4.0.0-beta.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz", + "integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/formdata-polyfill": { "version": "4.0.10", "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", @@ -1915,6 +2478,54 @@ "node": ">=12.20.0" } }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "license": "ISC" + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -1938,6 +2549,27 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/gauge": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", + "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.2", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.1", + "object-assign": "^4.1.1", + "signal-exit": "^3.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.2" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -1975,6 +2607,27 @@ "node": ">= 0.4" } }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/glob-parent": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", @@ -2037,6 +2690,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", + "license": "ISC" + }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", @@ -2064,6 +2723,48 @@ "integrity": "sha512-mJLY5tErGWtsw8hO2fJ2vK4IpG6S1AIgVkduRo4FqFJhgI2H3XLzgemRemk45zcnFyxNNpOfrIDle2KcnJM0lA==", "license": "ISC" }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.0.0" + } + }, "node_modules/iceberg-js": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/iceberg-js/-/iceberg-js-0.8.1.tgz", @@ -2073,6 +2774,18 @@ "node": ">=20.0.0" } }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -2100,6 +2813,32 @@ "dev": true, "license": "ISC" }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", @@ -2123,6 +2862,15 @@ "node": ">=0.10.0" } }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -2252,6 +3000,30 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "license": "MIT", + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -2261,6 +3033,15 @@ "node": ">= 0.4" } }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/mediaplex": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/mediaplex/-/mediaplex-1.0.0.tgz", @@ -2542,6 +3323,36 @@ "node": ">= 10" } }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", @@ -2563,11 +3374,22 @@ "node": ">= 0.6" } }, + "node_modules/mimic-response": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-2.1.0.tgz", + "integrity": "sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" @@ -2576,6 +3398,52 @@ "node": "*" } }, + "node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/mlly": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.0.tgz", @@ -2605,6 +3473,21 @@ "thenify-all": "^1.0.0" } }, + "node_modules/nan": { + "version": "2.25.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.25.0.tgz", + "integrity": "sha512-0M90Ag7Xn5KMLLZ7zliPWP3rT90P6PN+IzVFS0VqmnPktBk3700xUVv8Ikm9EUaUE5SDWdp/BIxdENzVznpm1g==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/node-domexception": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", @@ -2682,6 +3565,21 @@ "url": "https://opencollective.com/nodemon" } }, + "node_modules/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "license": "ISC", + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -2692,6 +3590,19 @@ "node": ">=0.10.0" } }, + "node_modules/npmlog": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", + "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "are-we-there-yet": "^2.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^3.0.0", + "set-blocking": "^2.0.0" + } + }, "node_modules/nth-check": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", @@ -2713,6 +3624,137 @@ "node": ">=0.10.0" } }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/openai": { + "version": "4.104.0", + "resolved": "https://registry.npmjs.org/openai/-/openai-4.104.0.tgz", + "integrity": "sha512-p99EFNsA/yX6UhVO93f5kJsDRLAg+CTA2RBqdHK4RtK8u5IJw32Hyb2dTGKbnnFmnuoBv5r7Z2CURI9sGZpSuA==", + "license": "Apache-2.0", + "dependencies": { + "@types/node": "^18.11.18", + "@types/node-fetch": "^2.6.4", + "abort-controller": "^3.0.0", + "agentkeepalive": "^4.2.1", + "form-data-encoder": "1.7.2", + "formdata-node": "^4.3.2", + "node-fetch": "^2.6.7" + }, + "bin": { + "openai": "bin/cli" + }, + "peerDependencies": { + "ws": "^8.18.0", + "zod": "^3.23.8" + }, + "peerDependenciesMeta": { + "ws": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, + "node_modules/openai/node_modules/@types/node": { + "version": "18.19.130", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz", + "integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==", + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/openai/node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/openai/node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "license": "MIT" + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" + }, "node_modules/pathe": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", @@ -2822,6 +3864,19 @@ "node": ">= 0.6.0" } }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", @@ -2835,6 +3890,54 @@ "dev": true, "license": "MIT" }, + "node_modules/qs": { + "version": "6.14.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", + "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/random-bytes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", + "integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/readable-stream": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", @@ -2930,6 +4033,22 @@ } } }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/rollup": { "version": "4.53.3", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.3.tgz", @@ -2991,11 +4110,16 @@ ], "license": "MIT" }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, "node_modules/semver": { "version": "7.7.3", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", - "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -3004,6 +4128,72 @@ "node": ">=10" } }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "license": "ISC" + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, "node_modules/shoukaku": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/shoukaku/-/shoukaku-4.2.0.tgz", @@ -3017,6 +4207,115 @@ "npm": ">=7.0.0" } }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-3.1.1.tgz", + "integrity": "sha512-CQ5LTKGfCpvE1K0n2us+kuMPbk/q0EKl82s4aheV9oXjFEz6W/Y7oQFVJuU6QG77hRT4Ghb5RURteF5vnWjupA==", + "license": "MIT", + "dependencies": { + "decompress-response": "^4.2.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, "node_modules/simple-update-notifier": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", @@ -3083,6 +4382,15 @@ "node": ">= 12" } }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -3092,6 +4400,45 @@ "safe-buffer": "~5.2.0" } }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/stripe": { + "version": "14.25.0", + "resolved": "https://registry.npmjs.org/stripe/-/stripe-14.25.0.tgz", + "integrity": "sha512-wQS3GNMofCXwH8TSje8E1SE8zr6ODiGtHQgPtO95p9Mb4FhKC9jvXR2NUTpZ9ZINlckJcFidCmaTFV4P6vsb9g==", + "license": "MIT", + "dependencies": { + "@types/node": ">=8.1.0", + "qs": "^6.11.0" + }, + "engines": { + "node": ">=12.*" + } + }, "node_modules/strtok3": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-6.3.0.tgz", @@ -3144,6 +4491,24 @@ "node": ">=4" } }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "deprecated": "Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/thenify": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", @@ -3230,6 +4595,15 @@ "node": ">=8.0" } }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, "node_modules/token-types": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/token-types/-/token-types-4.2.1.tgz", @@ -3370,6 +4744,19 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", @@ -3390,6 +4777,18 @@ "integrity": "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==", "license": "MIT" }, + "node_modules/uid-safe": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", + "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==", + "license": "MIT", + "dependencies": { + "random-bytes": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/undefsafe": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", @@ -3421,6 +4820,30 @@ "./packages/isomorphic-unfetch" ] }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/uuid": { "version": "11.1.0", "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", @@ -3434,6 +4857,15 @@ "uuid": "dist/esm/bin/uuid" } }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/web-streams-polyfill": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", @@ -3465,6 +4897,21 @@ "integrity": "sha512-5kwCfWml7+b2NO7KrLMhYihjRx0teKkd3yGp1Xk5Vaf2JGdSh+rgVhEALAD9c/59dP+YwJHXoEO7e8QPy7gOkw==", "license": "Apache-2.0" }, + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "license": "ISC", + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, "node_modules/ws": { "version": "8.18.3", "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", @@ -3486,6 +4933,12 @@ } } }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, "node_modules/zod": { "version": "3.25.76", "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", diff --git a/aethex-bot/public/community.html b/aethex-bot/public/community.html new file mode 100644 index 0000000..00ad213 --- /dev/null +++ b/aethex-bot/public/community.html @@ -0,0 +1,560 @@ + + + + + + Community - AeThex + + + + + + +
+
+ +
+
+

Loading community...

+
+ +
+

404

+

Community Not Found

+

This community page doesn't exist or has been removed.

+ Go Home +
+ +
+
+ + +
+ Community Avatar +

+ Community + +

+

@handle

+ +

+ + +
+ + + + + + +
+
+ + + + diff --git a/aethex-bot/public/dashboard.html b/aethex-bot/public/dashboard.html index e832d17..5b05b98 100644 --- a/aethex-bot/public/dashboard.html +++ b/aethex-bot/public/dashboard.html @@ -1270,6 +1270,9 @@ + @@ -2457,6 +2460,259 @@ + + @@ -2642,6 +2898,7 @@ case 'activity-roles': loadActivityRoles(); break; case 'cooldowns': loadCooldowns(); break; case 'backups': loadBackups(); break; + case 'branding': loadBranding(); break; } } @@ -4728,6 +4985,289 @@ }); }); + // Branding tab navigation + document.querySelectorAll('[data-branding-tab]').forEach(tab => { + tab.addEventListener('click', () => { + document.querySelectorAll('[data-branding-tab]').forEach(t => t.classList.remove('active')); + tab.classList.add('active'); + document.querySelectorAll('.branding-section').forEach(s => s.classList.add('hidden')); + document.getElementById(tab.dataset.brandingTab).classList.remove('hidden'); + }); + }); + + // Branding color picker sync + document.getElementById('brandingColorPicker')?.addEventListener('input', (e) => { + document.getElementById('brandingColor').value = e.target.value; + }); + document.getElementById('brandingColor')?.addEventListener('input', (e) => { + if (/^#[0-9A-Fa-f]{6}$/.test(e.target.value)) { + document.getElementById('brandingColorPicker').value = e.target.value; + } + }); + + let currentBranding = null; + + async function loadBranding() { + if (!currentGuild) return; + + try { + const res = await fetch('/api/guild/' + currentGuild + '/branding'); + if (res.ok) { + currentBranding = await res.json(); + populateBrandingForm(currentBranding); + } else { + currentBranding = null; + resetBrandingForm(); + } + } catch (e) { + console.error('Failed to load branding:', e); + resetBrandingForm(); + } + } + + function populateBrandingForm(data) { + // Tier status + const tier = data.tier || 'free'; + const tierBadge = document.getElementById('brandingTierBadge'); + tierBadge.textContent = tier.charAt(0).toUpperCase() + tier.slice(1); + tierBadge.className = 'badge'; + if (tier === 'pro') tierBadge.style.background = 'var(--success)'; + else if (tier === 'enterprise') tierBadge.style.background = 'var(--warning)'; + else if (tier === 'basic') tierBadge.style.background = 'var(--primary)'; + + document.getElementById('brandingCurrentTier').textContent = tier.charAt(0).toUpperCase() + tier.slice(1); + document.getElementById('brandingSubStatus').textContent = data.subscription_status || 'Not subscribed'; + document.getElementById('brandingExpires').textContent = data.subscription_expires_at + ? new Date(data.subscription_expires_at).toLocaleDateString() + : '-'; + + // Identity + document.getElementById('brandingBotName').value = data.custom_bot_name || ''; + document.getElementById('brandingFooter').value = data.custom_footer_text || ''; + document.getElementById('brandingColor').value = data.custom_embed_color || '#6366f1'; + document.getElementById('brandingColorPicker').value = data.custom_embed_color || '#6366f1'; + document.getElementById('brandingAvatar').value = data.custom_bot_avatar_url || ''; + document.getElementById('brandingEnabled').checked = data.branding_enabled || false; + + // Handle + document.getElementById('brandingHandle').value = data.custom_handle || ''; + if (data.custom_handle) { + document.getElementById('handleStatusText').textContent = 'āœ… Handle claimed: aethex.dev/' + data.custom_handle; + document.getElementById('handleStatus').style.background = 'rgba(16,185,129,0.2)'; + document.getElementById('viewLandingBtn').href = '/' + data.custom_handle; + document.getElementById('viewLandingBtn').style.display = 'inline-block'; + } else { + document.getElementById('viewLandingBtn').style.display = 'none'; + } + + // Landing page + document.getElementById('landingTitle').value = data.landing_title || ''; + document.getElementById('landingDescription').value = data.landing_description || ''; + document.getElementById('landingBanner').value = data.landing_banner_url || ''; + document.getElementById('landingInvite').value = data.landing_invite_url || ''; + document.getElementById('landingWebsite').value = data.landing_website_url || ''; + document.getElementById('landingTheme').value = data.landing_theme || 'default'; + + // Social links + const socials = data.landing_social_links || {}; + document.getElementById('socialTwitter').value = socials.twitter || ''; + document.getElementById('socialYoutube').value = socials.youtube || ''; + document.getElementById('socialTwitch').value = socials.twitch || ''; + + updatePreview(); + } + + function resetBrandingForm() { + document.getElementById('brandingTierBadge').textContent = 'Free'; + document.getElementById('brandingCurrentTier').textContent = 'Free'; + document.getElementById('brandingSubStatus').textContent = 'Not subscribed'; + document.getElementById('brandingExpires').textContent = '-'; + + document.getElementById('brandingBotName').value = ''; + document.getElementById('brandingFooter').value = ''; + document.getElementById('brandingColor').value = '#6366f1'; + document.getElementById('brandingColorPicker').value = '#6366f1'; + document.getElementById('brandingAvatar').value = ''; + document.getElementById('brandingEnabled').checked = false; + + document.getElementById('brandingHandle').value = ''; + document.getElementById('handleStatusText').textContent = 'Enter a handle to check availability'; + document.getElementById('handleStatus').style.background = 'var(--secondary)'; + document.getElementById('viewLandingBtn').style.display = 'none'; + + document.getElementById('landingTitle').value = ''; + document.getElementById('landingDescription').value = ''; + document.getElementById('landingBanner').value = ''; + document.getElementById('landingInvite').value = ''; + document.getElementById('landingWebsite').value = ''; + document.getElementById('landingTheme').value = 'default'; + + document.getElementById('socialTwitter').value = ''; + document.getElementById('socialYoutube').value = ''; + document.getElementById('socialTwitch').value = ''; + + updatePreview(); + } + + async function refreshBranding() { + await loadBranding(); + alert('Branding status refreshed'); + } + + async function saveBrandingIdentity() { + if (!currentGuild) return alert('Please select a server first'); + + const data = { + custom_bot_name: document.getElementById('brandingBotName').value || null, + custom_footer_text: document.getElementById('brandingFooter').value || null, + custom_embed_color: document.getElementById('brandingColor').value || null, + custom_bot_avatar_url: document.getElementById('brandingAvatar').value || null, + branding_enabled: document.getElementById('brandingEnabled').checked + }; + + try { + const res = await fetch('/api/guild/' + currentGuild + '/branding', { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data) + }); + + if (res.ok) { + alert('Branding identity saved!'); + await loadBranding(); + } else { + const err = await res.json(); + alert(err.error || 'Failed to save branding'); + } + } catch (e) { + alert('Failed to save branding'); + } + } + + async function checkHandleAvailability() { + const handle = document.getElementById('brandingHandle').value.trim().toLowerCase(); + if (!handle) return alert('Please enter a handle'); + + if (!/^[a-z0-9-]{3,50}$/.test(handle)) { + document.getElementById('handleStatusText').textContent = 'āŒ Invalid format. Use 3-50 alphanumeric characters or hyphens.'; + document.getElementById('handleStatus').style.background = 'rgba(239,68,68,0.2)'; + return; + } + + try { + const res = await fetch('/api/community/' + handle); + if (res.status === 404) { + document.getElementById('handleStatusText').textContent = 'āœ… Handle is available!'; + document.getElementById('handleStatus').style.background = 'rgba(16,185,129,0.2)'; + } else { + document.getElementById('handleStatusText').textContent = 'āŒ Handle is already taken'; + document.getElementById('handleStatus').style.background = 'rgba(239,68,68,0.2)'; + } + } catch (e) { + document.getElementById('handleStatusText').textContent = 'Error checking availability'; + } + } + + async function claimHandle() { + if (!currentGuild) return alert('Please select a server first'); + + const handle = document.getElementById('brandingHandle').value.trim().toLowerCase(); + if (!handle) return alert('Please enter a handle'); + + if (!/^[a-z0-9-]{3,50}$/.test(handle)) { + return alert('Invalid handle format. Use 3-50 alphanumeric characters or hyphens.'); + } + + try { + const res = await fetch('/api/guild/' + currentGuild + '/branding', { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ custom_handle: handle }) + }); + + if (res.ok) { + alert('Handle claimed successfully!'); + await loadBranding(); + } else { + const err = await res.json(); + alert(err.error || 'Failed to claim handle'); + } + } catch (e) { + alert('Failed to claim handle'); + } + } + + async function saveLandingPage() { + if (!currentGuild) return alert('Please select a server first'); + + const data = { + landing_title: document.getElementById('landingTitle').value || null, + landing_description: document.getElementById('landingDescription').value || null, + landing_banner_url: document.getElementById('landingBanner').value || null, + landing_invite_url: document.getElementById('landingInvite').value || null, + landing_website_url: document.getElementById('landingWebsite').value || null, + landing_theme: document.getElementById('landingTheme').value + }; + + try { + const res = await fetch('/api/guild/' + currentGuild + '/branding', { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data) + }); + + if (res.ok) { + alert('Landing page saved!'); + await loadBranding(); + } else { + const err = await res.json(); + alert(err.error || 'Failed to save landing page'); + } + } catch (e) { + alert('Failed to save landing page'); + } + } + + async function saveSocialLinks() { + if (!currentGuild) return alert('Please select a server first'); + + const socials = { + twitter: document.getElementById('socialTwitter').value || null, + youtube: document.getElementById('socialYoutube').value || null, + twitch: document.getElementById('socialTwitch').value || null + }; + + try { + const res = await fetch('/api/guild/' + currentGuild + '/branding', { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ landing_social_links: socials }) + }); + + if (res.ok) { + alert('Social links saved!'); + await loadBranding(); + } else { + const err = await res.json(); + alert(err.error || 'Failed to save social links'); + } + } catch (e) { + alert('Failed to save social links'); + } + } + + function updatePreview() { + const name = document.getElementById('brandingBotName').value || 'AeThex Bot'; + const footer = document.getElementById('brandingFooter').value || 'Powered by AeThex'; + const color = document.getElementById('brandingColor').value || '#6366f1'; + const avatar = document.getElementById('brandingAvatar').value || '/logo.png'; + + document.getElementById('previewBotName').textContent = name; + document.getElementById('previewFooter').textContent = footer; + document.getElementById('previewAvatar').src = avatar; + document.getElementById('embedPreview').style.borderLeftColor = color; + } + init(); diff --git a/aethex-bot/server/webServer.js b/aethex-bot/server/webServer.js index ae9279b..3e4626a 100644 --- a/aethex-bot/server/webServer.js +++ b/aethex-bot/server/webServer.js @@ -70,6 +70,21 @@ function createWebServer(discordClient, supabase, options = {}) { }, { onConflict: 'guild_id' }); console.log(`[Stripe] Guild ${guild_id} purchased featured slot`); + } else if (plan_type?.startsWith('branding_')) { + // White-label branding subscription + const brandingTier = plan_type.replace('branding_', ''); + + await supabase.from('server_branding').upsert({ + guild_id: guild_id, + tier: brandingTier, + subscription_id: session.subscription, + subscription_status: 'active', + subscription_started_at: new Date().toISOString(), + branding_enabled: true, + updated_at: new Date().toISOString() + }, { onConflict: 'guild_id' }); + + console.log(`[Stripe] Guild ${guild_id} subscribed to branding tier: ${brandingTier}`); } break; } @@ -82,10 +97,19 @@ function createWebServer(discordClient, supabase, options = {}) { await supabase.from('federation_servers').update({ subscription_status: 'active' }).eq('subscription_id', subscription.id); + + // Also update branding subscriptions + await supabase.from('server_branding').update({ + subscription_status: 'active' + }).eq('subscription_id', subscription.id); } else if (status === 'past_due' || status === 'unpaid') { await supabase.from('federation_servers').update({ subscription_status: status }).eq('subscription_id', subscription.id); + + await supabase.from('server_branding').update({ + subscription_status: status + }).eq('subscription_id', subscription.id); } break; } @@ -104,6 +128,15 @@ function createWebServer(discordClient, supabase, options = {}) { active: false }).eq('subscription_id', subscription.id); + // Cancel branding subscription - downgrade to free + await supabase.from('server_branding').update({ + tier: 'free', + subscription_id: null, + subscription_status: null, + branding_enabled: false, + updated_at: new Date().toISOString() + }).eq('subscription_id', subscription.id); + console.log(`[Stripe] Subscription ${subscription.id} canceled`); break; } @@ -3127,6 +3160,287 @@ function createWebServer(discordClient, supabase, options = {}) { res.sendFile(path.join(__dirname, '../public/index.html')); }); + // ============================================ + // White-Label Branding API Routes + // ============================================ + const brandingManager = require('../utils/brandingManager'); + + // Get branding config for a guild + app.get('/api/guild/:guildId/branding', async (req, res) => { + if (!supabase) { + return res.status(503).json({ error: 'Database not available' }); + } + + const userId = req.session.user?.id; + if (!userId) { + return res.status(401).json({ error: 'Not authenticated' }); + } + + const { guildId } = req.params; + const userGuild = req.session.user.guilds?.find(g => g.id === guildId); + if (!userGuild || !userGuild.isAdmin) { + return res.status(403).json({ error: 'No admin access to this server' }); + } + + try { + const branding = await brandingManager.getBranding(supabase, guildId); + res.json({ branding, tiers: brandingManager.BRANDING_TIERS }); + } catch (error) { + res.status(500).json({ error: 'Failed to fetch branding' }); + } + }); + + // Update branding config + app.post('/api/guild/:guildId/branding', async (req, res) => { + if (!supabase) { + return res.status(503).json({ error: 'Database not available' }); + } + + const userId = req.session.user?.id; + if (!userId) { + return res.status(401).json({ error: 'Not authenticated' }); + } + + const { guildId } = req.params; + const userGuild = req.session.user.guilds?.find(g => g.id === guildId); + if (!userGuild || !userGuild.isAdmin) { + return res.status(403).json({ error: 'No admin access to this server' }); + } + + try { + const result = await brandingManager.updateBranding(supabase, guildId, req.body, userId); + res.json(result); + } catch (error) { + res.status(500).json({ error: 'Failed to update branding' }); + } + }); + + // Claim custom handle + app.post('/api/guild/:guildId/branding/handle', async (req, res) => { + if (!supabase) { + return res.status(503).json({ error: 'Database not available' }); + } + + const userId = req.session.user?.id; + if (!userId) { + return res.status(401).json({ error: 'Not authenticated' }); + } + + const { guildId } = req.params; + const { handle } = req.body; + + const userGuild = req.session.user.guilds?.find(g => g.id === guildId); + if (!userGuild || !userGuild.isAdmin) { + return res.status(403).json({ error: 'No admin access to this server' }); + } + + try { + const branding = await brandingManager.getBranding(supabase, guildId); + const result = await brandingManager.claimHandle(supabase, guildId, handle, branding.tier); + res.json(result); + } catch (error) { + res.status(500).json({ error: 'Failed to claim handle' }); + } + }); + + // Get branding analytics (Enterprise tier) + app.get('/api/guild/:guildId/branding/analytics', async (req, res) => { + if (!supabase) { + return res.status(503).json({ error: 'Database not available' }); + } + + const userId = req.session.user?.id; + if (!userId) { + return res.status(401).json({ error: 'Not authenticated' }); + } + + const { guildId } = req.params; + const { days = 30 } = req.query; + + const userGuild = req.session.user.guilds?.find(g => g.id === guildId); + if (!userGuild || !userGuild.isAdmin) { + return res.status(403).json({ error: 'No admin access to this server' }); + } + + try { + const branding = await brandingManager.getBranding(supabase, guildId); + + if (!brandingManager.hasFeature(branding.tier, 'analytics')) { + return res.status(403).json({ error: 'Analytics requires Enterprise tier' }); + } + + const since = new Date(); + since.setDate(since.getDate() - parseInt(days)); + + const { data: analytics } = await supabase + .from('branding_analytics') + .select('*') + .eq('guild_id', guildId) + .gte('created_at', since.toISOString()) + .order('created_at', { ascending: false }); + + // Aggregate stats + const pageViews = analytics?.filter(a => a.event_type === 'page_view').length || 0; + const inviteClicks = analytics?.filter(a => a.event_type === 'invite_click').length || 0; + const socialClicks = analytics?.filter(a => a.event_type === 'social_click').length || 0; + + res.json({ + summary: { pageViews, inviteClicks, socialClicks, period: `${days} days` }, + events: analytics || [] + }); + } catch (error) { + res.status(500).json({ error: 'Failed to fetch analytics' }); + } + }); + + // Public: Community landing page API + app.get('/api/community/:handle', async (req, res) => { + if (!supabase) { + return res.status(503).json({ error: 'Database not available' }); + } + + const { handle } = req.params; + + try { + const { data: branding } = await supabase + .from('server_branding') + .select('*') + .eq('custom_handle', handle.toLowerCase()) + .eq('handle_verified', true) + .maybeSingle(); + + if (!branding) { + return res.status(404).json({ error: 'Community not found' }); + } + + // Get server info from federation_servers or Discord + let serverInfo = null; + const { data: fedServer } = await supabase + .from('federation_servers') + .select('guild_name, guild_icon, member_count, description') + .eq('guild_id', branding.guild_id) + .maybeSingle(); + + if (fedServer) { + serverInfo = fedServer; + } else { + // Try to get from Discord cache + const guild = discordClient.guilds.cache.get(branding.guild_id); + if (guild) { + serverInfo = { + guild_name: guild.name, + guild_icon: guild.iconURL({ size: 256 }), + member_count: guild.memberCount + }; + } + } + + // Track page view + await brandingManager.trackAnalytics(supabase, branding.guild_id, 'page_view', { + referrer: req.headers.referer, + userAgent: req.headers['user-agent'] + }); + + res.json({ + handle: branding.custom_handle, + name: branding.custom_bot_name || serverInfo?.guild_name || 'Community', + title: branding.landing_title || serverInfo?.guild_name, + description: branding.landing_description || serverInfo?.description || 'Welcome to our community!', + banner: branding.landing_banner_url, + avatar: branding.custom_bot_avatar_url || serverInfo?.guild_icon, + invite: branding.landing_invite_url, + website: branding.landing_website_url, + socials: branding.landing_social_links || {}, + features: branding.landing_features || [], + theme: branding.landing_theme || 'default', + memberCount: serverInfo?.member_count, + tier: branding.tier + }); + } catch (error) { + console.error('[Community] Error:', error); + res.status(500).json({ error: 'Failed to fetch community' }); + } + }); + + // Track community analytics events + app.post('/api/community/:handle/track', async (req, res) => { + if (!supabase) { + return res.status(503).json({ error: 'Database not available' }); + } + + const { handle } = req.params; + const { event } = req.body; + + try { + const { data: branding } = await supabase + .from('server_branding') + .select('guild_id') + .eq('custom_handle', handle.toLowerCase()) + .maybeSingle(); + + if (branding) { + await brandingManager.trackAnalytics(supabase, branding.guild_id, event, { + referrer: req.headers.referer, + userAgent: req.headers['user-agent'] + }); + } + + res.json({ success: true }); + } catch (error) { + res.json({ success: false }); + } + }); + + // Branding subscription checkout (Stripe) + app.post('/api/branding/checkout', async (req, res) => { + if (!stripe) { + return res.status(503).json({ error: 'Payments not configured' }); + } + + const userId = req.session.user?.id; + if (!userId) { + return res.status(401).json({ error: 'Not authenticated' }); + } + + const { guildId, tier } = req.body; + + const userGuild = req.session.user.guilds?.find(g => g.id === guildId); + if (!userGuild || !userGuild.isAdmin) { + return res.status(403).json({ error: 'No admin access to this server' }); + } + + const prices = { + basic: process.env.STRIPE_PRICE_BRANDING_BASIC, + pro: process.env.STRIPE_PRICE_BRANDING_PRO, + enterprise: process.env.STRIPE_PRICE_BRANDING_ENTERPRISE + }; + + const priceId = prices[tier]; + if (!priceId) { + return res.status(400).json({ error: 'Invalid tier' }); + } + + try { + const session = await stripe.checkout.sessions.create({ + mode: 'subscription', + payment_method_types: ['card'], + line_items: [{ price: priceId, quantity: 1 }], + success_url: `${BASE_URL}/dashboard?guild=${guildId}&page=branding&success=true`, + cancel_url: `${BASE_URL}/dashboard?guild=${guildId}&page=branding&canceled=true`, + metadata: { + guild_id: guildId, + user_id: userId, + plan_type: `branding_${tier}` + } + }); + + res.json({ url: session.url }); + } catch (error) { + console.error('[Branding Checkout] Error:', error); + res.status(500).json({ error: 'Failed to create checkout session' }); + } + }); + // ============================================ // NEXUS Security API Integration Routes // ============================================ @@ -3281,9 +3595,38 @@ function createWebServer(discordClient, supabase, options = {}) { } }); - // Catch-all route for SPA - use middleware instead of wildcard - app.use((req, res, next) => { + // Catch-all route for SPA and custom community handles + const reservedPaths = ['api', 'dashboard', 'federation', 'pricing', 'features', 'commands', 'auth', 'oauth', 'health', 'leaderboard']; + + app.use(async (req, res, next) => { if (req.method === 'GET' && !req.path.startsWith('/api/')) { + const pathParts = req.path.split('/').filter(Boolean); + + // Check if this might be a custom community handle + if (pathParts.length === 1 && !reservedPaths.includes(pathParts[0].toLowerCase())) { + const potentialHandle = pathParts[0].toLowerCase(); + + // Check if handle exists in database + if (supabase) { + try { + const { data: branding } = await supabase + .from('server_branding') + .select('custom_handle') + .eq('custom_handle', potentialHandle) + .eq('handle_verified', true) + .maybeSingle(); + + if (branding) { + // Serve community landing page + return res.sendFile(path.join(__dirname, '../public/community.html')); + } + } catch (err) { + console.error('[Community Route] DB error:', err.message); + } + } + } + + // Default: serve index.html res.sendFile(path.join(__dirname, '../public/index.html')); } else if (req.path.startsWith('/api/')) { res.status(404).json({ error: 'API endpoint not found' }); diff --git a/aethex-bot/utils/brandingManager.js b/aethex-bot/utils/brandingManager.js new file mode 100644 index 0000000..63f54e4 --- /dev/null +++ b/aethex-bot/utils/brandingManager.js @@ -0,0 +1,375 @@ +/** + * White-Label Branding System + * Allows paying servers to customize bot identity + */ + +const { WebhookClient, EmbedBuilder } = require('discord.js'); + +// Branding tier definitions +const BRANDING_TIERS = { + free: { + name: 'Free', + price: 0, + features: [] + }, + basic: { + name: 'Basic', + price: 15, + features: ['custom_name', 'custom_footer', 'custom_color'] + }, + pro: { + name: 'Pro', + price: 35, + features: ['custom_name', 'custom_footer', 'custom_color', 'custom_avatar', 'custom_handle', 'landing_page'] + }, + enterprise: { + name: 'Enterprise', + price: 75, + features: ['custom_name', 'custom_footer', 'custom_color', 'custom_avatar', 'custom_handle', 'landing_page', 'analytics', 'priority_support', 'custom_domain'] + } +}; + +// Reserved handles that can't be claimed +const SYSTEM_HANDLES = [ + 'admin', 'api', 'dashboard', 'login', 'auth', 'oauth', 'federation', + 'pricing', 'features', 'commands', 'support', 'help', 'aethex', 'warden', + 'official', 'staff', 'mod', 'moderator', 'bot', 'system', 'root' +]; + +// Cache for branding data (reduces DB calls) +const brandingCache = new Map(); +const CACHE_TTL = 5 * 60 * 1000; // 5 minutes + +/** + * Get branding configuration for a guild + */ +async function getBranding(supabase, guildId) { + // Check cache first + const cached = brandingCache.get(guildId); + if (cached && Date.now() - cached.timestamp < CACHE_TTL) { + return cached.data; + } + + try { + const { data, error } = await supabase + .from('server_branding') + .select('*') + .eq('guild_id', guildId) + .maybeSingle(); + + if (error) throw error; + + const result = data || getDefaultBranding(); + + // Cache the result + brandingCache.set(guildId, { data: result, timestamp: Date.now() }); + + return result; + } catch (err) { + console.error('[Branding] Error fetching branding:', err); + return getDefaultBranding(); + } +} + +/** + * Get default branding (AeThex Warden) + */ +function getDefaultBranding() { + return { + custom_bot_name: null, + custom_bot_avatar_url: null, + custom_footer_text: null, + custom_embed_color: null, + custom_handle: null, + tier: 'free', + branding_enabled: false + }; +} + +/** + * Check if a feature is available for a tier + */ +function hasFeature(tier, feature) { + const tierConfig = BRANDING_TIERS[tier]; + return tierConfig?.features?.includes(feature) || false; +} + +/** + * Get effective bot name for a guild + */ +async function getEffectiveBotName(supabase, guildId, defaultName = 'AeThex | Warden') { + const branding = await getBranding(supabase, guildId); + + if (branding.branding_enabled && branding.custom_bot_name && hasFeature(branding.tier, 'custom_name')) { + return branding.custom_bot_name; + } + + return defaultName; +} + +/** + * Get effective avatar URL for a guild + */ +async function getEffectiveAvatar(supabase, guildId, defaultAvatar) { + const branding = await getBranding(supabase, guildId); + + if (branding.branding_enabled && branding.custom_bot_avatar_url && hasFeature(branding.tier, 'custom_avatar')) { + return branding.custom_bot_avatar_url; + } + + return defaultAvatar; +} + +/** + * Get effective embed color for a guild + */ +async function getEffectiveColor(supabase, guildId, defaultColor = '#6366f1') { + const branding = await getBranding(supabase, guildId); + + if (branding.branding_enabled && branding.custom_embed_color && hasFeature(branding.tier, 'custom_color')) { + return branding.custom_embed_color; + } + + return defaultColor; +} + +/** + * Get effective footer text for a guild + */ +async function getEffectiveFooter(supabase, guildId, defaultFooter = 'AeThex | Warden') { + const branding = await getBranding(supabase, guildId); + + if (branding.branding_enabled && branding.custom_footer_text && hasFeature(branding.tier, 'custom_footer')) { + return branding.custom_footer_text; + } + + return defaultFooter; +} + +/** + * Create a branded embed for a guild + */ +async function createBrandedEmbed(supabase, guildId, options = {}) { + const branding = await getBranding(supabase, guildId); + + const embed = new EmbedBuilder(); + + // Set color + const color = branding.branding_enabled && branding.custom_embed_color && hasFeature(branding.tier, 'custom_color') + ? branding.custom_embed_color + : options.defaultColor || '#6366f1'; + embed.setColor(color); + + // Set footer + const footerText = branding.branding_enabled && branding.custom_footer_text && hasFeature(branding.tier, 'custom_footer') + ? branding.custom_footer_text + : options.defaultFooter || 'AeThex | Warden'; + embed.setFooter({ text: footerText }); + + // Apply other options + if (options.title) embed.setTitle(options.title); + if (options.description) embed.setDescription(options.description); + if (options.fields) embed.addFields(options.fields); + if (options.thumbnail) embed.setThumbnail(options.thumbnail); + if (options.image) embed.setImage(options.image); + if (options.timestamp) embed.setTimestamp(); + + return embed; +} + +/** + * Send a branded message via webhook (for full white-label experience) + */ +async function sendBrandedMessage(channel, supabase, guildId, options = {}) { + const branding = await getBranding(supabase, guildId); + + // If branding not enabled or no custom avatar, send normally + if (!branding.branding_enabled || !hasFeature(branding.tier, 'custom_avatar')) { + return channel.send(options); + } + + try { + // Get or create webhook for branding + const webhooks = await channel.fetchWebhooks(); + let webhook = webhooks.find(wh => wh.name === 'AeThex-Branding'); + + if (!webhook) { + webhook = await channel.createWebhook({ + name: 'AeThex-Branding', + reason: 'White-label branding system' + }); + } + + // Send via webhook with custom identity + const webhookClient = new WebhookClient({ url: webhook.url }); + + return await webhookClient.send({ + username: branding.custom_bot_name || 'AeThex | Warden', + avatarURL: branding.custom_bot_avatar_url || undefined, + content: options.content, + embeds: options.embeds, + files: options.files, + components: options.components + }); + } catch (err) { + console.error('[Branding] Webhook send failed, falling back to normal:', err.message); + return channel.send(options); + } +} + +/** + * Update branding configuration + */ +async function updateBranding(supabase, guildId, updates, updatedBy) { + try { + const { error } = await supabase + .from('server_branding') + .upsert({ + guild_id: guildId, + ...updates, + updated_at: new Date().toISOString(), + created_by: updatedBy + }, { onConflict: 'guild_id' }); + + if (error) throw error; + + // Invalidate cache + brandingCache.delete(guildId); + + return { success: true }; + } catch (err) { + console.error('[Branding] Update error:', err); + return { success: false, error: err.message }; + } +} + +/** + * Claim a custom handle + */ +async function claimHandle(supabase, guildId, handle, tier) { + // Validate handle format + const cleanHandle = handle.toLowerCase().replace(/[^a-z0-9-]/g, ''); + + if (cleanHandle.length < 3 || cleanHandle.length > 30) { + return { success: false, error: 'Handle must be 3-30 characters (letters, numbers, hyphens only)' }; + } + + // Check if system reserved + if (SYSTEM_HANDLES.includes(cleanHandle)) { + return { success: false, error: 'This handle is reserved' }; + } + + // Check tier permission + if (!hasFeature(tier, 'custom_handle')) { + return { success: false, error: 'Custom handles require Pro tier or higher' }; + } + + try { + // Check if reserved in database + const { data: reserved } = await supabase + .from('reserved_handles') + .select('handle') + .eq('handle', cleanHandle) + .maybeSingle(); + + if (reserved) { + return { success: false, error: 'This handle is reserved' }; + } + + // Check if already taken + const { data: existing } = await supabase + .from('server_branding') + .select('guild_id') + .eq('custom_handle', cleanHandle) + .neq('guild_id', guildId) + .maybeSingle(); + + if (existing) { + return { success: false, error: 'This handle is already taken' }; + } + + // Claim it + const { error } = await supabase + .from('server_branding') + .upsert({ + guild_id: guildId, + custom_handle: cleanHandle, + handle_verified: true, + updated_at: new Date().toISOString() + }, { onConflict: 'guild_id' }); + + if (error) throw error; + + // Invalidate cache + brandingCache.delete(guildId); + + return { success: true, handle: cleanHandle }; + } catch (err) { + console.error('[Branding] Claim handle error:', err); + return { success: false, error: 'Failed to claim handle' }; + } +} + +/** + * Get branding by custom handle + */ +async function getBrandingByHandle(supabase, handle) { + try { + const { data, error } = await supabase + .from('server_branding') + .select('*, federation_servers!inner(guild_name, guild_icon, member_count, description)') + .eq('custom_handle', handle.toLowerCase()) + .eq('handle_verified', true) + .maybeSingle(); + + if (error) throw error; + return data; + } catch (err) { + console.error('[Branding] Get by handle error:', err); + return null; + } +} + +/** + * Track landing page analytics + */ +async function trackAnalytics(supabase, guildId, eventType, metadata = {}) { + try { + await supabase.from('branding_analytics').insert({ + guild_id: guildId, + event_type: eventType, + referrer: metadata.referrer || null, + user_agent: metadata.userAgent || null, + ip_hash: metadata.ipHash || null, + metadata: metadata.extra || {} + }); + } catch (err) { + // Silent fail for analytics + console.error('[Branding] Analytics error:', err.message); + } +} + +/** + * Invalidate branding cache for a guild + */ +function invalidateCache(guildId) { + brandingCache.delete(guildId); +} + +module.exports = { + BRANDING_TIERS, + getBranding, + getDefaultBranding, + hasFeature, + getEffectiveBotName, + getEffectiveAvatar, + getEffectiveColor, + getEffectiveFooter, + createBrandedEmbed, + sendBrandedMessage, + updateBranding, + claimHandle, + getBrandingByHandle, + trackAnalytics, + invalidateCache +};