new file: aethex-bot/migrations/add_whitelabel_branding.sql

This commit is contained in:
MrPiglr 2026-03-05 02:22:31 -07:00
parent e344e3d4cc
commit d07ccd0f53
12 changed files with 3964 additions and 35 deletions

36
aethex-bot/.env.example Normal file
View file

@ -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

View file

@ -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.' });
}
}
};

View file

@ -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] });

View file

@ -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);

View file

@ -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) {

View file

@ -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);

View file

@ -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';

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,560 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Community - AeThex</title>
<meta name="description" content="Join our community on Discord">
<meta name="theme-color" content="#6366f1">
<link rel="icon" href="/logo.png" type="image/png">
<style>
:root {
--background: #030712;
--foreground: #f8fafc;
--card: rgba(15, 23, 42, 0.6);
--card-border: rgba(99, 102, 241, 0.15);
--card-border-hover: rgba(99, 102, 241, 0.4);
--primary: #6366f1;
--primary-light: #818cf8;
--secondary: rgba(30, 41, 59, 0.5);
--muted: #64748b;
--border: rgba(51, 65, 85, 0.5);
--success: #10b981;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Segoe UI', system-ui, sans-serif;
background: var(--background);
color: var(--foreground);
min-height: 100vh;
line-height: 1.6;
}
.bg-grid {
position: fixed;
inset: 0;
background-image:
linear-gradient(rgba(99, 102, 241, 0.03) 1px, transparent 1px),
linear-gradient(90deg, rgba(99, 102, 241, 0.03) 1px, transparent 1px);
background-size: 64px 64px;
pointer-events: none;
z-index: -2;
}
.bg-glow {
position: fixed;
top: -50%;
left: 50%;
transform: translateX(-50%);
width: 150%;
height: 100%;
background: radial-gradient(ellipse at center, rgba(99, 102, 241, 0.15) 0%, transparent 60%);
pointer-events: none;
z-index: -1;
}
.container {
max-width: 900px;
margin: 0 auto;
padding: 2rem 1.5rem;
}
.loading {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 80vh;
gap: 1rem;
}
.loading .spinner {
width: 48px;
height: 48px;
border: 3px solid var(--border);
border-top-color: var(--primary);
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.error-page {
display: none;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 80vh;
text-align: center;
gap: 1rem;
}
.error-page h1 {
font-size: 6rem;
color: var(--primary);
line-height: 1;
}
.error-page p {
color: var(--muted);
font-size: 1.1rem;
}
.community-page {
display: none;
}
.banner {
width: 100%;
height: 200px;
background: linear-gradient(135deg, var(--primary), #3b82f6);
border-radius: 16px;
overflow: hidden;
margin-bottom: -60px;
position: relative;
}
.banner img {
width: 100%;
height: 100%;
object-fit: cover;
}
.banner-overlay {
position: absolute;
inset: 0;
background: linear-gradient(to bottom, transparent 50%, var(--background) 100%);
}
.profile-section {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
padding: 0 1rem;
}
.avatar {
width: 120px;
height: 120px;
border-radius: 50%;
border: 4px solid var(--background);
background: var(--card);
object-fit: cover;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
}
.community-name {
font-size: 2rem;
font-weight: 700;
margin-top: 1rem;
}
.community-handle {
color: var(--muted);
font-size: 0.9rem;
margin-top: 0.25rem;
}
.member-count {
display: inline-flex;
align-items: center;
gap: 0.5rem;
background: var(--secondary);
padding: 0.5rem 1rem;
border-radius: 20px;
margin-top: 1rem;
font-size: 0.9rem;
}
.member-count .dot {
width: 8px;
height: 8px;
background: var(--success);
border-radius: 50%;
}
.description {
max-width: 600px;
margin: 1.5rem auto;
color: var(--muted);
font-size: 1.05rem;
}
.cta-buttons {
display: flex;
gap: 1rem;
flex-wrap: wrap;
justify-content: center;
margin-top: 1.5rem;
}
.btn {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.875rem 1.75rem;
border-radius: 8px;
font-weight: 600;
text-decoration: none;
transition: all 0.2s;
border: none;
cursor: pointer;
font-size: 1rem;
}
.btn-primary {
background: linear-gradient(135deg, #5865F2, #7289DA);
color: white;
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(88, 101, 242, 0.4);
}
.btn-secondary {
background: var(--secondary);
color: var(--foreground);
border: 1px solid var(--border);
}
.btn-secondary:hover {
border-color: var(--primary);
}
.features-section {
margin-top: 3rem;
}
.features-title {
font-size: 1.25rem;
text-align: center;
margin-bottom: 1.5rem;
color: var(--muted);
}
.features-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
}
.feature-card {
background: var(--card);
border: 1px solid var(--card-border);
border-radius: 12px;
padding: 1.25rem;
text-align: center;
transition: all 0.2s;
}
.feature-card:hover {
border-color: var(--card-border-hover);
transform: translateY(-2px);
}
.feature-icon {
font-size: 2rem;
margin-bottom: 0.5rem;
}
.feature-name {
font-weight: 600;
}
.socials-section {
margin-top: 2.5rem;
display: flex;
justify-content: center;
gap: 1rem;
flex-wrap: wrap;
}
.social-link {
display: flex;
align-items: center;
justify-content: center;
width: 44px;
height: 44px;
background: var(--secondary);
border: 1px solid var(--border);
border-radius: 50%;
color: var(--foreground);
text-decoration: none;
transition: all 0.2s;
font-size: 1.25rem;
}
.social-link:hover {
border-color: var(--primary);
transform: scale(1.1);
}
.powered-by {
margin-top: 4rem;
text-align: center;
padding: 1.5rem;
border-top: 1px solid var(--border);
}
.powered-by a {
color: var(--muted);
text-decoration: none;
font-size: 0.85rem;
display: inline-flex;
align-items: center;
gap: 0.5rem;
transition: color 0.2s;
}
.powered-by a:hover {
color: var(--primary);
}
.powered-by img {
width: 20px;
height: 20px;
border-radius: 4px;
}
.tier-badge {
display: inline-flex;
align-items: center;
gap: 0.25rem;
padding: 0.25rem 0.5rem;
border-radius: 4px;
font-size: 0.7rem;
font-weight: 600;
text-transform: uppercase;
margin-left: 0.5rem;
}
.tier-badge.pro {
background: linear-gradient(135deg, #a855f7, #6366f1);
color: white;
}
.tier-badge.enterprise {
background: linear-gradient(135deg, #f59e0b, #ef4444);
color: white;
}
@media (max-width: 640px) {
.banner { height: 150px; }
.avatar { width: 100px; height: 100px; }
.community-name { font-size: 1.5rem; }
.cta-buttons { flex-direction: column; width: 100%; }
.btn { width: 100%; justify-content: center; }
}
</style>
</head>
<body>
<div class="bg-grid"></div>
<div class="bg-glow"></div>
<div id="loading" class="loading">
<div class="spinner"></div>
<p>Loading community...</p>
</div>
<div id="error" class="error-page">
<h1>404</h1>
<h2>Community Not Found</h2>
<p>This community page doesn't exist or has been removed.</p>
<a href="/" class="btn btn-primary">Go Home</a>
</div>
<div id="community" class="community-page">
<div class="container">
<div class="banner" id="banner">
<img id="banner-img" src="" alt="Banner" style="display: none;">
<div class="banner-overlay"></div>
</div>
<div class="profile-section">
<img id="avatar" class="avatar" src="/logo.png" alt="Community Avatar">
<h1 class="community-name">
<span id="name">Community</span>
<span id="tier-badge" class="tier-badge" style="display: none;"></span>
</h1>
<p class="community-handle" id="handle">@handle</p>
<div class="member-count" id="member-count" style="display: none;">
<span class="dot"></span>
<span id="members">0</span> members
</div>
<p class="description" id="description"></p>
<div class="cta-buttons">
<a href="#" id="invite-btn" class="btn btn-primary" style="display: none;" onclick="trackEvent('invite_click')">
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M19.27 5.33C17.94 4.71 16.5 4.26 15 4a.09.09 0 0 0-.07.03c-.18.33-.39.76-.53 1.09a16.09 16.09 0 0 0-4.8 0c-.14-.34-.35-.76-.54-1.09c-.01-.02-.04-.03-.07-.03c-1.5.26-2.93.71-4.27 1.33c-.01 0-.02.01-.03.02c-2.72 4.07-3.47 8.03-3.1 11.95c0 .02.01.04.03.05c1.8 1.32 3.53 2.12 5.24 2.65c.03.01.06 0 .07-.02c.4-.55.76-1.13 1.07-1.74c.02-.04 0-.08-.04-.09c-.57-.22-1.11-.48-1.64-.78c-.04-.02-.04-.08-.01-.11c.11-.08.22-.17.33-.25c.02-.02.05-.02.07-.01c3.44 1.57 7.15 1.57 10.55 0c.02-.01.05-.01.07.01c.11.09.22.17.33.26c.04.03.04.09-.01.11c-.52.31-1.07.56-1.64.78c-.04.01-.05.06-.04.09c.32.61.68 1.19 1.07 1.74c.03.01.06.02.09.01c1.72-.53 3.45-1.33 5.25-2.65c.02-.01.03-.03.03-.05c.44-4.53-.73-8.46-3.1-11.95c-.01-.01-.02-.02-.04-.02zM8.52 14.91c-1.03 0-1.89-.95-1.89-2.12s.84-2.12 1.89-2.12c1.06 0 1.9.96 1.89 2.12c0 1.17-.84 2.12-1.89 2.12zm6.97 0c-1.03 0-1.89-.95-1.89-2.12s.84-2.12 1.89-2.12c1.06 0 1.9.96 1.89 2.12c0 1.17-.83 2.12-1.89 2.12z"/></svg>
Join Discord
</a>
<a href="#" id="website-btn" class="btn btn-secondary" style="display: none;" onclick="trackEvent('website_click')">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>
Website
</a>
</div>
</div>
<div id="features-section" class="features-section" style="display: none;">
<h3 class="features-title">What We Offer</h3>
<div id="features-grid" class="features-grid"></div>
</div>
<div id="socials-section" class="socials-section" style="display: none;"></div>
<div class="powered-by">
<a href="/">
<img src="/logo.png" alt="AeThex">
Powered by AeThex | Warden
</a>
</div>
</div>
</div>
<script>
const handle = window.location.pathname.split('/').filter(Boolean)[0];
async function loadCommunity() {
try {
const res = await fetch(`/api/community/${handle}`);
if (!res.ok) {
document.getElementById('loading').style.display = 'none';
document.getElementById('error').style.display = 'flex';
return;
}
const data = await res.json();
// Update page
document.title = `${data.title || data.name} - AeThex`;
document.querySelector('meta[name="description"]').content = data.description || '';
// Avatar
if (data.avatar) {
document.getElementById('avatar').src = data.avatar;
}
// Banner
if (data.banner) {
const bannerImg = document.getElementById('banner-img');
bannerImg.src = data.banner;
bannerImg.style.display = 'block';
}
// Name and handle
document.getElementById('name').textContent = data.title || data.name;
document.getElementById('handle').textContent = `aethex.dev/${data.handle}`;
// Tier badge
if (data.tier === 'pro' || data.tier === 'enterprise') {
const badge = document.getElementById('tier-badge');
badge.textContent = data.tier === 'pro' ? '⭐ Pro' : '🏆 Enterprise';
badge.className = `tier-badge ${data.tier}`;
badge.style.display = 'inline-flex';
}
// Member count
if (data.memberCount) {
document.getElementById('members').textContent = data.memberCount.toLocaleString();
document.getElementById('member-count').style.display = 'inline-flex';
}
// Description
document.getElementById('description').textContent = data.description || '';
// Invite button
if (data.invite) {
const inviteBtn = document.getElementById('invite-btn');
inviteBtn.href = data.invite;
inviteBtn.style.display = 'inline-flex';
}
// Website button
if (data.website) {
const websiteBtn = document.getElementById('website-btn');
websiteBtn.href = data.website;
websiteBtn.style.display = 'inline-flex';
}
// Features
if (data.features && data.features.length > 0) {
const featuresSection = document.getElementById('features-section');
const featuresGrid = document.getElementById('features-grid');
data.features.forEach(feature => {
const card = document.createElement('div');
card.className = 'feature-card';
card.innerHTML = `
<div class="feature-icon">${feature.icon || '✨'}</div>
<div class="feature-name">${feature.name}</div>
`;
featuresGrid.appendChild(card);
});
featuresSection.style.display = 'block';
}
// Social links
if (data.socials && Object.keys(data.socials).length > 0) {
const socialsSection = document.getElementById('socials-section');
const socialIcons = {
twitter: '<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor"><path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"/></svg>',
youtube: '<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor"><path d="M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z"/></svg>',
tiktok: '<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor"><path d="M12.525.02c1.31-.02 2.61-.01 3.91-.02.08 1.53.63 3.09 1.75 4.17 1.12 1.11 2.7 1.62 4.24 1.79v4.03c-1.44-.05-2.89-.35-4.2-.97-.57-.26-1.1-.59-1.62-.93-.01 2.92.01 5.84-.02 8.75-.08 1.4-.54 2.79-1.35 3.94-1.31 1.92-3.58 3.17-5.91 3.21-1.43.08-2.86-.31-4.08-1.03-2.02-1.19-3.44-3.37-3.65-5.71-.02-.5-.03-1-.01-1.49.18-1.9 1.12-3.72 2.58-4.96 1.66-1.44 3.98-2.13 6.15-1.72.02 1.48-.04 2.96-.04 4.44-.99-.32-2.15-.23-3.02.37-.63.41-1.11 1.04-1.36 1.75-.21.51-.15 1.07-.14 1.61.24 1.64 1.82 3.02 3.5 2.87 1.12-.01 2.19-.66 2.77-1.61.19-.33.4-.67.41-1.06.1-1.79.06-3.57.07-5.36.01-4.03-.01-8.05.02-12.07z"/></svg>',
instagram: '<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2.163c3.204 0 3.584.012 4.85.07 3.252.148 4.771 1.691 4.919 4.919.058 1.265.069 1.645.069 4.849 0 3.205-.012 3.584-.069 4.849-.149 3.225-1.664 4.771-4.919 4.919-1.266.058-1.644.07-4.85.07-3.204 0-3.584-.012-4.849-.07-3.26-.149-4.771-1.699-4.919-4.92-.058-1.265-.07-1.644-.07-4.849 0-3.204.013-3.583.07-4.849.149-3.227 1.664-4.771 4.919-4.919 1.266-.057 1.645-.069 4.849-.069zM12 0C8.741 0 8.333.014 7.053.072 2.695.272.273 2.69.073 7.052.014 8.333 0 8.741 0 12c0 3.259.014 3.668.072 4.948.2 4.358 2.618 6.78 6.98 6.98C8.333 23.986 8.741 24 12 24c3.259 0 3.668-.014 4.948-.072 4.354-.2 6.782-2.618 6.979-6.98.059-1.28.073-1.689.073-4.948 0-3.259-.014-3.667-.072-4.947-.196-4.354-2.617-6.78-6.979-6.98C15.668.014 15.259 0 12 0zm0 5.838a6.162 6.162 0 1 0 0 12.324 6.162 6.162 0 0 0 0-12.324zM12 16a4 4 0 1 1 0-8 4 4 0 0 1 0 8zm6.406-11.845a1.44 1.44 0 1 0 0 2.881 1.44 1.44 0 0 0 0-2.881z"/></svg>',
github: '<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor"><path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/></svg>',
twitch: '<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor"><path d="M11.571 4.714h1.715v5.143H11.57zm4.715 0H18v5.143h-1.714zM6 0L1.714 4.286v15.428h5.143V24l4.286-4.286h3.428L22.286 12V0zm14.571 11.143l-3.428 3.428h-3.429l-3 3v-3H6.857V1.714h13.714z"/></svg>',
roblox: '<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor"><path d="M5.164 0L0 18.576 18.836 24 24 5.424zM15.097 15.458l-6.093-1.534 1.534-6.093 6.093 1.534z"/></svg>'
};
Object.entries(data.socials).forEach(([platform, url]) => {
if (url) {
const link = document.createElement('a');
link.className = 'social-link';
link.href = url;
link.target = '_blank';
link.rel = 'noopener noreferrer';
link.innerHTML = socialIcons[platform] || platform[0].toUpperCase();
link.title = platform;
link.onclick = () => trackEvent('social_click');
socialsSection.appendChild(link);
}
});
if (socialsSection.children.length > 0) {
socialsSection.style.display = 'flex';
}
}
// Show community page
document.getElementById('loading').style.display = 'none';
document.getElementById('community').style.display = 'block';
} catch (error) {
console.error('Failed to load community:', error);
document.getElementById('loading').style.display = 'none';
document.getElementById('error').style.display = 'flex';
}
}
function trackEvent(eventType) {
fetch(`/api/community/${handle}/track`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ event: eventType })
}).catch(() => {});
}
loadCommunity();
</script>
</body>
</html>

View file

@ -1270,6 +1270,9 @@
<div class="nav-item" data-page="backups">
<span class="nav-icon">💾</span> Backups
</div>
<div class="nav-item" data-page="branding">
<span class="nav-icon">🎨</span> Branding
</div>
</div>
</nav>
@ -2457,6 +2460,259 @@
</div>
</div>
</div>
<div id="page-branding" class="page hidden">
<div class="page-header">
<h1 class="page-title">Server <span class="text-gradient">Branding</span></h1>
<p class="page-subtitle">Customize your bot's appearance and create your community landing page</p>
</div>
<div id="brandingStatus" class="card" style="margin-bottom:1.5rem">
<div class="card-header" style="display:flex;justify-content:space-between;align-items:center">
<h3 class="card-title">Your Plan</h3>
<span id="brandingTierBadge" class="badge">Free</span>
</div>
<div class="card-body" id="brandingStatusContent">
<div class="stats-grid">
<div class="stat-card">
<div class="stat-label">Current Tier</div>
<div class="stat-value" id="brandingCurrentTier">Free</div>
</div>
<div class="stat-card">
<div class="stat-label">Status</div>
<div class="stat-value" id="brandingSubStatus">-</div>
</div>
<div class="stat-card">
<div class="stat-label">Expires</div>
<div class="stat-value" id="brandingExpires">-</div>
</div>
</div>
<div style="margin-top:1rem;display:flex;gap:1rem;flex-wrap:wrap">
<a href="/pricing.html#branding" target="_blank" class="btn btn-primary">View Plans</a>
<button class="btn btn-secondary" onclick="refreshBranding()">Refresh Status</button>
</div>
</div>
</div>
<div class="tabs" style="margin-bottom:1.5rem">
<div class="tab active" data-branding-tab="branding-identity">Identity</div>
<div class="tab" data-branding-tab="branding-handle">Handle</div>
<div class="tab" data-branding-tab="branding-landing">Landing Page</div>
<div class="tab" data-branding-tab="branding-preview">Preview</div>
</div>
<div id="branding-identity" class="branding-section">
<div class="card">
<div class="card-header">
<h3 class="card-title">Bot Identity</h3>
<span class="badge" style="font-size:0.7rem">Basic+</span>
</div>
<div class="card-body">
<div class="form-group" style="margin-bottom:1rem">
<label class="form-label">Custom Bot Name</label>
<input type="text" class="form-input" id="brandingBotName" placeholder="Your Community Bot" maxlength="80">
<small style="color:var(--muted)">This name appears in bot messages (Pro+ uses webhooks)</small>
</div>
<div class="form-group" style="margin-bottom:1rem">
<label class="form-label">Custom Footer Text</label>
<input type="text" class="form-input" id="brandingFooter" placeholder="Powered by Your Brand" maxlength="200">
<small style="color:var(--muted)">Replaces "Powered by AeThex" in embeds</small>
</div>
<div class="form-group" style="margin-bottom:1rem">
<label class="form-label">Embed Color</label>
<div style="display:flex;gap:0.5rem;align-items:center">
<input type="color" id="brandingColorPicker" value="#6366f1" style="width:50px;height:40px;border:none;cursor:pointer">
<input type="text" class="form-input" id="brandingColor" placeholder="#6366f1" maxlength="10" style="width:120px">
</div>
</div>
<div class="form-group" style="margin-bottom:1rem">
<label class="form-label">Custom Avatar URL <span class="badge" style="font-size:0.6rem">Pro+</span></label>
<input type="text" class="form-input" id="brandingAvatar" placeholder="https://your-domain.com/avatar.png">
<small style="color:var(--muted)">Used with webhook messages for custom bot avatar</small>
</div>
<div class="form-group">
<label class="form-label" style="display:flex;align-items:center;gap:0.5rem">
<input type="checkbox" id="brandingEnabled" style="width:auto">
Enable Branding
</label>
<small style="color:var(--muted)">Toggle custom branding on/off without losing settings</small>
</div>
<button class="btn btn-primary" style="margin-top:1rem" onclick="saveBrandingIdentity()">Save Identity Settings</button>
</div>
</div>
</div>
<div id="branding-handle" class="branding-section hidden">
<div class="card">
<div class="card-header">
<h3 class="card-title">Custom Handle</h3>
<span class="badge" style="font-size:0.7rem">Pro+</span>
</div>
<div class="card-body">
<div class="form-group" style="margin-bottom:1rem">
<label class="form-label">Your Handle</label>
<div style="display:flex;gap:0.5rem;align-items:center">
<span style="color:var(--muted)">aethex.dev/</span>
<input type="text" class="form-input" id="brandingHandle" placeholder="your-community" maxlength="50" style="flex:1">
</div>
<small style="color:var(--muted)">Alphanumeric and hyphens only, 3-50 characters</small>
</div>
<div id="handleStatus" style="margin-bottom:1rem;padding:1rem;border-radius:8px;background:var(--secondary)">
<p id="handleStatusText">Enter a handle to check availability</p>
</div>
<button class="btn btn-primary" onclick="claimHandle()">Claim Handle</button>
<button class="btn btn-secondary" onclick="checkHandleAvailability()" style="margin-left:0.5rem">Check Availability</button>
</div>
</div>
<div class="card" style="margin-top:1.5rem">
<div class="card-header">
<h3 class="card-title">Handle Benefits</h3>
</div>
<div class="card-body">
<ul style="list-style:none;padding:0">
<li style="padding:0.5rem 0;border-bottom:1px solid var(--border)">✅ Custom URL: <code>aethex.dev/your-handle</code></li>
<li style="padding:0.5rem 0;border-bottom:1px solid var(--border)">✅ Community landing page</li>
<li style="padding:0.5rem 0;border-bottom:1px solid var(--border)">✅ SEO-friendly presence</li>
<li style="padding:0.5rem 0">✅ Analytics (Enterprise)</li>
</ul>
</div>
</div>
</div>
<div id="branding-landing" class="branding-section hidden">
<div class="card">
<div class="card-header">
<h3 class="card-title">Landing Page Content</h3>
<span class="badge" style="font-size:0.7rem">Pro+</span>
</div>
<div class="card-body">
<div class="form-group" style="margin-bottom:1rem">
<label class="form-label">Page Title</label>
<input type="text" class="form-input" id="landingTitle" placeholder="Welcome to Our Community" maxlength="100">
</div>
<div class="form-group" style="margin-bottom:1rem">
<label class="form-label">Description</label>
<textarea class="form-input" id="landingDescription" placeholder="Join the best gaming community on Discord..." rows="4" style="resize:vertical"></textarea>
</div>
<div class="form-group" style="margin-bottom:1rem">
<label class="form-label">Banner Image URL</label>
<input type="text" class="form-input" id="landingBanner" placeholder="https://your-domain.com/banner.jpg">
</div>
<div class="form-group" style="margin-bottom:1rem">
<label class="form-label">Discord Invite URL</label>
<input type="text" class="form-input" id="landingInvite" placeholder="https://discord.gg/your-invite">
</div>
<div class="form-group" style="margin-bottom:1rem">
<label class="form-label">Website URL (optional)</label>
<input type="text" class="form-input" id="landingWebsite" placeholder="https://your-website.com">
</div>
<div class="form-group" style="margin-bottom:1rem">
<label class="form-label">Theme</label>
<select class="form-select" id="landingTheme">
<option value="default">Default</option>
<option value="dark">Dark</option>
<option value="gaming">Gaming</option>
<option value="minimal">Minimal</option>
</select>
</div>
<button class="btn btn-primary" onclick="saveLandingPage()">Save Landing Page</button>
<a id="viewLandingBtn" href="#" target="_blank" class="btn btn-secondary" style="margin-left:0.5rem;display:none">View Page</a>
</div>
</div>
<div class="card" style="margin-top:1.5rem">
<div class="card-header">
<h3 class="card-title">Social Links</h3>
</div>
<div class="card-body">
<div class="form-group" style="margin-bottom:1rem">
<label class="form-label">Twitter/X</label>
<input type="text" class="form-input" id="socialTwitter" placeholder="https://twitter.com/yourhandle">
</div>
<div class="form-group" style="margin-bottom:1rem">
<label class="form-label">YouTube</label>
<input type="text" class="form-input" id="socialYoutube" placeholder="https://youtube.com/@yourchannel">
</div>
<div class="form-group" style="margin-bottom:1rem">
<label class="form-label">Twitch</label>
<input type="text" class="form-input" id="socialTwitch" placeholder="https://twitch.tv/yourchannel">
</div>
<button class="btn btn-primary" onclick="saveSocialLinks()">Save Social Links</button>
</div>
</div>
</div>
<div id="branding-preview" class="branding-section hidden">
<div class="card">
<div class="card-header">
<h3 class="card-title">Embed Preview</h3>
</div>
<div class="card-body">
<div id="embedPreview" style="background:#2f3136;border-radius:4px;padding:1rem;max-width:520px;border-left:4px solid #6366f1">
<div style="display:flex;align-items:center;gap:0.5rem;margin-bottom:0.5rem">
<img id="previewAvatar" src="/logo.png" style="width:40px;height:40px;border-radius:50%" alt="">
<span id="previewBotName" style="font-weight:600;color:#fff">AeThex Bot</span>
</div>
<div style="color:#dcddde;margin:0.5rem 0">
<strong>Welcome!</strong>
<p style="margin:0.25rem 0">This is an example embed message from your server.</p>
</div>
<div id="previewFooter" style="color:#72767d;font-size:0.75rem;margin-top:0.5rem;padding-top:0.5rem;border-top:1px solid #40444b">
Powered by AeThex
</div>
</div>
<button class="btn btn-secondary" style="margin-top:1rem" onclick="updatePreview()">Refresh Preview</button>
</div>
</div>
<div class="card" style="margin-top:1.5rem">
<div class="card-header">
<h3 class="card-title">Pricing Tiers</h3>
</div>
<div class="card-body">
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:1rem">
<div style="padding:1rem;background:var(--secondary);border-radius:8px;border:1px solid var(--border)">
<h4 style="margin-bottom:0.5rem">Free</h4>
<p style="font-size:1.5rem;font-weight:bold">$0</p>
<ul style="font-size:0.85rem;color:var(--muted);padding-left:1rem;margin-top:0.5rem">
<li>Default AeThex branding</li>
</ul>
</div>
<div style="padding:1rem;background:var(--secondary);border-radius:8px;border:1px solid var(--primary)">
<h4 style="margin-bottom:0.5rem;color:var(--primary)">Basic</h4>
<p style="font-size:1.5rem;font-weight:bold">$15<span style="font-size:0.8rem;color:var(--muted)">/mo</span></p>
<ul style="font-size:0.85rem;color:var(--muted);padding-left:1rem;margin-top:0.5rem">
<li>Custom bot name</li>
<li>Custom footer</li>
<li>Custom embed color</li>
</ul>
</div>
<div style="padding:1rem;background:var(--secondary);border-radius:8px;border:1px solid var(--success)">
<h4 style="margin-bottom:0.5rem;color:var(--success)">Pro</h4>
<p style="font-size:1.5rem;font-weight:bold">$35<span style="font-size:0.8rem;color:var(--muted)">/mo</span></p>
<ul style="font-size:0.85rem;color:var(--muted);padding-left:1rem;margin-top:0.5rem">
<li>Everything in Basic</li>
<li>Custom avatar</li>
<li>Custom handle</li>
<li>Landing page</li>
</ul>
</div>
<div style="padding:1rem;background:var(--secondary);border-radius:8px;border:1px solid var(--warning)">
<h4 style="margin-bottom:0.5rem;color:var(--warning)">Enterprise</h4>
<p style="font-size:1.5rem;font-weight:bold">$75<span style="font-size:0.8rem;color:var(--muted)">/mo</span></p>
<ul style="font-size:0.85rem;color:var(--muted);padding-left:1rem;margin-top:0.5rem">
<li>Everything in Pro</li>
<li>Analytics dashboard</li>
<li>Priority support</li>
<li>Custom domain</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
</main>
</div>
@ -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();
</script>
</body>

View file

@ -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' });

View file

@ -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
};