375 lines
10 KiB
JavaScript
375 lines
10 KiB
JavaScript
/**
|
|
* 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
|
|
};
|