From ca07d17417e066fd6761bd938f83fb7ddfaf4a69 Mon Sep 17 00:00:00 2001 From: sirpiglr <49359077-sirpiglr@users.noreply.replit.com> Date: Mon, 8 Dec 2025 04:09:56 +0000 Subject: [PATCH] Add new commands and improve bot functionality Introduce several new slash commands including ban, kick, timeout, and userinfo. Enhance existing commands like config and rank with new features and configurations. Add new listeners for welcome and goodbye messages. Implement XP tracking for user leveling and integrate it with role rewards. Update documentation to reflect these changes. Replit-Commit-Author: Agent Replit-Commit-Session-Id: aed2e46d-25bb-4b73-81a1-bb9e8437c261 Replit-Commit-Checkpoint-Type: intermediate_checkpoint Replit-Commit-Event-Id: 1be8d824-5029-4875-bed8-0bd1d810892d Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/3bdfff67-975a-46ad-9845-fbb6b4a4c4b5/aed2e46d-25bb-4b73-81a1-bb9e8437c261/SQxsvtx Replit-Helium-Checkpoint-Created: true --- .replit | 2 +- aethex-bot/bot.js | 41 ++++++ aethex-bot/commands/avatar.js | 36 +++++ aethex-bot/commands/badges.js | 120 +++++++++++++++++ aethex-bot/commands/ban.js | 93 +++++++++++++ aethex-bot/commands/config.js | 184 ++++++++++++++++++++++++++ aethex-bot/commands/daily.js | 110 ++++++++++++++++ aethex-bot/commands/foundation.js | 82 ++++++++++++ aethex-bot/commands/kick.js | 86 ++++++++++++ aethex-bot/commands/modlog.js | 73 +++++++++++ aethex-bot/commands/rank.js | 86 ++++++++++++ aethex-bot/commands/serverinfo.js | 53 ++++++++ aethex-bot/commands/studio.js | 83 ++++++++++++ aethex-bot/commands/timeout.js | 105 +++++++++++++++ aethex-bot/commands/userinfo.js | 80 +++++++++++ aethex-bot/commands/warn.js | 90 +++++++++++++ aethex-bot/listeners/goodbye.js | 41 ++++++ aethex-bot/listeners/welcome.js | 47 +++++++ aethex-bot/listeners/xpTracker.js | 95 ++++++++++++++ replit.md | 211 +++++++++++++++++++++--------- 20 files changed, 1657 insertions(+), 61 deletions(-) create mode 100644 aethex-bot/commands/avatar.js create mode 100644 aethex-bot/commands/badges.js create mode 100644 aethex-bot/commands/ban.js create mode 100644 aethex-bot/commands/config.js create mode 100644 aethex-bot/commands/daily.js create mode 100644 aethex-bot/commands/foundation.js create mode 100644 aethex-bot/commands/kick.js create mode 100644 aethex-bot/commands/modlog.js create mode 100644 aethex-bot/commands/rank.js create mode 100644 aethex-bot/commands/serverinfo.js create mode 100644 aethex-bot/commands/studio.js create mode 100644 aethex-bot/commands/timeout.js create mode 100644 aethex-bot/commands/userinfo.js create mode 100644 aethex-bot/commands/warn.js create mode 100644 aethex-bot/listeners/goodbye.js create mode 100644 aethex-bot/listeners/welcome.js create mode 100644 aethex-bot/listeners/xpTracker.js diff --git a/.replit b/.replit index f9aee22..b7e8f28 100644 --- a/.replit +++ b/.replit @@ -23,7 +23,7 @@ localPort = 8080 externalPort = 8080 [[ports]] -localPort = 33701 +localPort = 37941 externalPort = 3000 [workflows] diff --git a/aethex-bot/bot.js b/aethex-bot/bot.js index e18d28b..f02f448 100644 --- a/aethex-bot/bot.js +++ b/aethex-bot/bot.js @@ -334,6 +334,46 @@ if (fs.existsSync(sentinelPath)) { } } +// ============================================================================= +// GENERAL LISTENER LOADING (Welcome, Goodbye, XP Tracker) +// ============================================================================= + +const listenersPath = path.join(__dirname, "listeners"); +const generalListenerFiles = ['welcome.js', 'goodbye.js', 'xpTracker.js']; +for (const file of generalListenerFiles) { + const filePath = path.join(listenersPath, file); + if (fs.existsSync(filePath)) { + const listener = require(filePath); + if ("name" in listener && "execute" in listener) { + client.on(listener.name, (...args) => listener.execute(...args, client, supabase)); + console.log(`Loaded listener: ${file}`); + } + } +} + +// ============================================================================= +// SERVER CONFIGS MAP +// ============================================================================= + +const serverConfigs = new Map(); +client.serverConfigs = serverConfigs; + +async function loadServerConfigs() { + if (!supabase) return; + try { + const { data, error } = await supabase + .from('server_config') + .select('*'); + if (error) throw error; + for (const config of data || []) { + serverConfigs.set(config.guild_id, config); + } + console.log(`[Config] Loaded ${serverConfigs.size} server configurations`); + } catch (e) { + console.warn('[Config] Could not load server configs:', e.message); + } +} + // ============================================================================= // FEED SYNC SETUP (Modified: Guard for missing Supabase) // ============================================================================= @@ -1020,6 +1060,7 @@ client.once("clientReady", async () => { // Load persisted data from Supabase await loadFederationMappings(); await loadActiveTickets(); + await loadServerConfigs(); // Auto-register commands on startup console.log("Registering slash commands with Discord..."); diff --git a/aethex-bot/commands/avatar.js b/aethex-bot/commands/avatar.js new file mode 100644 index 0000000..af6b0ca --- /dev/null +++ b/aethex-bot/commands/avatar.js @@ -0,0 +1,36 @@ +const { SlashCommandBuilder, EmbedBuilder } = require('discord.js'); + +module.exports = { + data: new SlashCommandBuilder() + .setName('avatar') + .setDescription('Get a user\'s avatar') + .addUserOption(option => + option.setName('user') + .setDescription('User to get avatar of (defaults to yourself)') + .setRequired(false) + ), + + async execute(interaction, supabase, client) { + const target = interaction.options.getUser('user') || interaction.user; + + const sizes = [128, 256, 512, 1024, 2048]; + const links = sizes.map(size => + `[${size}](${target.displayAvatarURL({ size, extension: 'png' })})` + ).join(' | '); + + const embed = new EmbedBuilder() + .setColor(0x7c3aed) + .setTitle(`${target.tag}'s Avatar`) + .setDescription(`Download: ${links}`) + .setImage(target.displayAvatarURL({ size: 1024, extension: 'png' })) + .setTimestamp(); + + const member = await interaction.guild.members.fetch(target.id).catch(() => null); + if (member && member.avatar) { + embed.setThumbnail(member.displayAvatarURL({ size: 256 })); + embed.setFooter({ text: 'Thumbnail shows server-specific avatar' }); + } + + await interaction.reply({ embeds: [embed] }); + }, +}; diff --git a/aethex-bot/commands/badges.js b/aethex-bot/commands/badges.js new file mode 100644 index 0000000..720b75c --- /dev/null +++ b/aethex-bot/commands/badges.js @@ -0,0 +1,120 @@ +const { SlashCommandBuilder, EmbedBuilder } = require('discord.js'); + +const BADGE_INFO = { + 'verified': { emoji: 'āœ…', name: 'Verified', description: 'Linked Discord to AeThex' }, + 'early_adopter': { emoji: '🌟', name: 'Early Adopter', description: 'Joined during early access' }, + 'contributor': { emoji: 'šŸ’Ž', name: 'Contributor', description: 'Contributed to the community' }, + 'creator': { emoji: 'šŸŽØ', name: 'Creator', description: 'Published projects on Studio' }, + 'supporter': { emoji: 'ā¤ļø', name: 'Supporter', description: 'Donated to the Foundation' }, + 'level_10': { emoji: 'šŸ”Ÿ', name: 'Level 10', description: 'Reached level 10' }, + 'level_25': { emoji: 'šŸ†', name: 'Level 25', description: 'Reached level 25' }, + 'level_50': { emoji: 'šŸ‘‘', name: 'Level 50', description: 'Reached level 50' }, + 'streak_7': { emoji: 'šŸ”„', name: 'Week Streak', description: '7 day daily claim streak' }, + 'streak_30': { emoji: 'šŸ’Ŗ', name: 'Month Streak', description: '30 day daily claim streak' }, + 'helpful': { emoji: 'šŸ¤', name: 'Helpful', description: 'Helped 10+ community members' }, + 'bug_hunter': { emoji: 'šŸ›', name: 'Bug Hunter', description: 'Reported a valid bug' }, +}; + +module.exports = { + data: new SlashCommandBuilder() + .setName('badges') + .setDescription('View your earned badges across all platforms') + .addUserOption(option => + option.setName('user') + .setDescription('User to view (defaults to yourself)') + .setRequired(false) + ), + + async execute(interaction, supabase, client) { + if (!supabase) { + return interaction.reply({ content: 'Database not configured.', ephemeral: true }); + } + + const target = interaction.options.getUser('user') || interaction.user; + await interaction.deferReply(); + + try { + const { data: link } = await supabase + .from('discord_links') + .select('user_id') + .eq('discord_id', target.id) + .single(); + + if (!link) { + return interaction.editReply({ + embeds: [ + new EmbedBuilder() + .setColor(0xff6b6b) + .setDescription(`${target.id === interaction.user.id ? 'You are' : `${target.tag} is`} not linked to AeThex.`) + ] + }); + } + + const { data: profile } = await supabase + .from('user_profiles') + .select('username, avatar_url, badges, xp, daily_streak') + .eq('id', link.user_id) + .single(); + + let earnedBadges = []; + + if (profile?.badges) { + earnedBadges = typeof profile.badges === 'string' + ? JSON.parse(profile.badges) + : profile.badges; + } + + earnedBadges.push('verified'); + + const xp = profile?.xp || 0; + const level = Math.floor(Math.sqrt(xp / 100)); + if (level >= 10) earnedBadges.push('level_10'); + if (level >= 25) earnedBadges.push('level_25'); + if (level >= 50) earnedBadges.push('level_50'); + + const streak = profile?.daily_streak || 0; + if (streak >= 7) earnedBadges.push('streak_7'); + if (streak >= 30) earnedBadges.push('streak_30'); + + earnedBadges = [...new Set(earnedBadges)]; + + const embed = new EmbedBuilder() + .setColor(0x7c3aed) + .setTitle(`${profile?.username || target.tag}'s Badges`) + .setThumbnail(profile?.avatar_url || target.displayAvatarURL()) + .setTimestamp(); + + if (earnedBadges.length > 0) { + const badgeDisplay = earnedBadges.map(key => { + const info = BADGE_INFO[key]; + if (info) { + return `${info.emoji} **${info.name}**\n${info.description}`; + } + return `šŸ… **${key}**`; + }).join('\n\n'); + + embed.setDescription(badgeDisplay); + embed.addFields({ name: 'Total Badges', value: `${earnedBadges.length}`, inline: true }); + } else { + embed.setDescription('No badges earned yet. Keep engaging to earn badges!'); + } + + const allBadgeKeys = Object.keys(BADGE_INFO); + const lockedBadges = allBadgeKeys.filter(k => !earnedBadges.includes(k)); + + if (lockedBadges.length > 0 && lockedBadges.length <= 6) { + const lockedDisplay = lockedBadges.map(key => { + const info = BADGE_INFO[key]; + return `šŸ”’ ${info.name}`; + }).join(', '); + embed.addFields({ name: 'Locked Badges', value: lockedDisplay }); + } + + await interaction.editReply({ embeds: [embed] }); + + } catch (error) { + console.error('Badges error:', error); + await interaction.editReply({ content: 'Failed to fetch badge data.' }); + } + }, +}; diff --git a/aethex-bot/commands/ban.js b/aethex-bot/commands/ban.js new file mode 100644 index 0000000..2bf2734 --- /dev/null +++ b/aethex-bot/commands/ban.js @@ -0,0 +1,93 @@ +const { SlashCommandBuilder, EmbedBuilder, PermissionFlagsBits } = require('discord.js'); + +module.exports = { + data: new SlashCommandBuilder() + .setName('ban') + .setDescription('Ban a user from the server') + .setDefaultMemberPermissions(PermissionFlagsBits.BanMembers) + .addUserOption(option => + option.setName('user') + .setDescription('User to ban') + .setRequired(true) + ) + .addStringOption(option => + option.setName('reason') + .setDescription('Reason for ban') + .setRequired(false) + .setMaxLength(500) + ) + .addIntegerOption(option => + option.setName('delete_days') + .setDescription('Days of messages to delete (0-7)') + .setRequired(false) + .setMinValue(0) + .setMaxValue(7) + ), + + async execute(interaction, supabase, client) { + const target = interaction.options.getUser('user'); + const reason = interaction.options.getString('reason') || 'No reason provided'; + const deleteDays = interaction.options.getInteger('delete_days') || 0; + const moderator = interaction.user; + + if (target.id === interaction.user.id) { + return interaction.reply({ content: 'You cannot ban yourself.', ephemeral: true }); + } + + const member = await interaction.guild.members.fetch(target.id).catch(() => null); + + if (member && !member.bannable) { + return interaction.reply({ content: 'I cannot ban this user. They may have higher permissions.', ephemeral: true }); + } + + try { + await target.send({ + embeds: [ + new EmbedBuilder() + .setColor(0xff0000) + .setTitle(`Banned from ${interaction.guild.name}`) + .addFields({ name: 'Reason', value: reason }) + .setTimestamp() + ] + }).catch(() => {}); + + await interaction.guild.members.ban(target, { + reason: `${reason} | Banned by ${moderator.tag}`, + deleteMessageSeconds: deleteDays * 24 * 60 * 60 + }); + + if (supabase) { + try { + await supabase.from('mod_actions').insert({ + guild_id: interaction.guildId, + action: 'ban', + user_id: target.id, + user_tag: target.tag, + moderator_id: moderator.id, + moderator_tag: moderator.tag, + reason: reason, + }); + } catch (e) { + console.warn('Failed to log ban:', e.message); + } + } + + const embed = new EmbedBuilder() + .setColor(0xff0000) + .setTitle('User Banned') + .setThumbnail(target.displayAvatarURL()) + .addFields( + { name: 'User', value: `${target.tag} (${target.id})`, inline: true }, + { name: 'Moderator', value: moderator.tag, inline: true }, + { name: 'Reason', value: reason } + ) + .setTimestamp(); + + await interaction.reply({ embeds: [embed] }); + + } catch (error) { + console.error('Ban error:', error); + await interaction.reply({ content: 'Failed to ban user.', ephemeral: true }); + } + }, +}; diff --git a/aethex-bot/commands/config.js b/aethex-bot/commands/config.js new file mode 100644 index 0000000..6c851c3 --- /dev/null +++ b/aethex-bot/commands/config.js @@ -0,0 +1,184 @@ +const { SlashCommandBuilder, EmbedBuilder, PermissionFlagsBits, ChannelType } = require('discord.js'); + +module.exports = { + data: new SlashCommandBuilder() + .setName('config') + .setDescription('Configure server settings') + .setDefaultMemberPermissions(PermissionFlagsBits.Administrator) + .addSubcommand(sub => + sub.setName('view') + .setDescription('View current server configuration') + ) + .addSubcommand(sub => + sub.setName('welcome') + .setDescription('Set the welcome channel') + .addChannelOption(opt => + opt.setName('channel') + .setDescription('Channel for welcome messages') + .addChannelTypes(ChannelType.GuildText) + .setRequired(true) + ) + ) + .addSubcommand(sub => + sub.setName('goodbye') + .setDescription('Set the goodbye channel') + .addChannelOption(opt => + opt.setName('channel') + .setDescription('Channel for goodbye messages') + .addChannelTypes(ChannelType.GuildText) + .setRequired(true) + ) + ) + .addSubcommand(sub => + sub.setName('modlog') + .setDescription('Set the moderation log channel') + .addChannelOption(opt => + opt.setName('channel') + .setDescription('Channel for mod logs') + .addChannelTypes(ChannelType.GuildText) + .setRequired(true) + ) + ) + .addSubcommand(sub => + sub.setName('levelup') + .setDescription('Set the level-up announcement channel') + .addChannelOption(opt => + opt.setName('channel') + .setDescription('Channel for level-up messages') + .addChannelTypes(ChannelType.GuildText) + .setRequired(true) + ) + ) + .addSubcommand(sub => + sub.setName('autorole') + .setDescription('Set auto-role for new members') + .addRoleOption(opt => + opt.setName('role') + .setDescription('Role to assign on join') + .setRequired(true) + ) + ) + .addSubcommand(sub => + sub.setName('levelrole') + .setDescription('Add a role reward for reaching a level') + .addRoleOption(opt => + opt.setName('role') + .setDescription('Role to give') + .setRequired(true) + ) + .addIntegerOption(opt => + opt.setName('level') + .setDescription('Level required') + .setRequired(true) + .setMinValue(1) + .setMaxValue(100) + ) + ), + + async execute(interaction, supabase, client) { + if (!supabase) { + return interaction.reply({ content: 'Database not configured.', ephemeral: true }); + } + + const subcommand = interaction.options.getSubcommand(); + await interaction.deferReply({ ephemeral: true }); + + try { + if (subcommand === 'view') { + const { data: config } = await supabase + .from('server_config') + .select('*') + .eq('guild_id', interaction.guildId) + .single(); + + const embed = new EmbedBuilder() + .setColor(0x7c3aed) + .setTitle('Server Configuration') + .addFields( + { name: 'Welcome Channel', value: config?.welcome_channel ? `<#${config.welcome_channel}>` : 'Not set', inline: true }, + { name: 'Goodbye Channel', value: config?.goodbye_channel ? `<#${config.goodbye_channel}>` : 'Not set', inline: true }, + { name: 'Mod Log Channel', value: config?.modlog_channel ? `<#${config.modlog_channel}>` : 'Not set', inline: true }, + { name: 'Level-Up Channel', value: config?.level_up_channel ? `<#${config.level_up_channel}>` : 'Not set', inline: true }, + { name: 'Auto Role', value: config?.auto_role ? `<@&${config.auto_role}>` : 'Not set', inline: true } + ) + .setTimestamp(); + + const { data: levelRoles } = await supabase + .from('level_roles') + .select('role_id, level_required') + .eq('guild_id', interaction.guildId) + .order('level_required', { ascending: true }); + + if (levelRoles && levelRoles.length > 0) { + const roleText = levelRoles.map(lr => `Level ${lr.level_required}: <@&${lr.role_id}>`).join('\n'); + embed.addFields({ name: 'Level Roles', value: roleText }); + } + + return interaction.editReply({ embeds: [embed] }); + } + + const updateField = { + welcome: 'welcome_channel', + goodbye: 'goodbye_channel', + modlog: 'modlog_channel', + levelup: 'level_up_channel', + autorole: 'auto_role', + }; + + if (subcommand === 'levelrole') { + const role = interaction.options.getRole('role'); + const level = interaction.options.getInteger('level'); + + await supabase.from('level_roles').upsert({ + guild_id: interaction.guildId, + role_id: role.id, + level_required: level, + }, { onConflict: 'guild_id,role_id' }); + + if (!client.serverConfigs) client.serverConfigs = new Map(); + + return interaction.editReply({ + embeds: [ + new EmbedBuilder() + .setColor(0x00ff00) + .setDescription(`${role} will now be given at level ${level}!`) + ] + }); + } + + const fieldName = updateField[subcommand]; + let value; + + if (subcommand === 'autorole') { + value = interaction.options.getRole('role').id; + } else { + value = interaction.options.getChannel('channel').id; + } + + await supabase.from('server_config').upsert({ + guild_id: interaction.guildId, + [fieldName]: value, + updated_at: new Date().toISOString(), + }, { onConflict: 'guild_id' }); + + if (!client.serverConfigs) client.serverConfigs = new Map(); + const current = client.serverConfigs.get(interaction.guildId) || {}; + current[fieldName] = value; + client.serverConfigs.set(interaction.guildId, current); + + const displayValue = subcommand === 'autorole' ? `<@&${value}>` : `<#${value}>`; + + await interaction.editReply({ + embeds: [ + new EmbedBuilder() + .setColor(0x00ff00) + .setDescription(`${subcommand.charAt(0).toUpperCase() + subcommand.slice(1)} set to ${displayValue}!`) + ] + }); + + } catch (error) { + console.error('Config error:', error); + await interaction.editReply({ content: 'Failed to update configuration.' }); + } + }, +}; diff --git a/aethex-bot/commands/daily.js b/aethex-bot/commands/daily.js new file mode 100644 index 0000000..928b2d5 --- /dev/null +++ b/aethex-bot/commands/daily.js @@ -0,0 +1,110 @@ +const { SlashCommandBuilder, EmbedBuilder } = require('discord.js'); + +const DAILY_XP = 50; +const STREAK_BONUS = 10; +const MAX_STREAK_BONUS = 100; + +module.exports = { + data: new SlashCommandBuilder() + .setName('daily') + .setDescription('Claim your daily XP bonus'), + + async execute(interaction, supabase, client) { + if (!supabase) { + return interaction.reply({ content: 'Database not configured.', ephemeral: true }); + } + + await interaction.deferReply(); + + try { + const { data: link } = await supabase + .from('discord_links') + .select('user_id') + .eq('discord_id', interaction.user.id) + .single(); + + if (!link) { + return interaction.editReply({ + embeds: [ + new EmbedBuilder() + .setColor(0xff6b6b) + .setDescription('You need to link your account first! Use `/verify` to get started.') + ] + }); + } + + const { data: profile } = await supabase + .from('user_profiles') + .select('xp, daily_streak, last_daily') + .eq('id', link.user_id) + .single(); + + const now = new Date(); + const lastDaily = profile?.last_daily ? new Date(profile.last_daily) : null; + const currentXp = profile?.xp || 0; + let streak = profile?.daily_streak || 0; + + if (lastDaily) { + const hoursSince = (now - lastDaily) / (1000 * 60 * 60); + + if (hoursSince < 20) { + const nextClaim = new Date(lastDaily.getTime() + 20 * 60 * 60 * 1000); + return interaction.editReply({ + embeds: [ + new EmbedBuilder() + .setColor(0xfbbf24) + .setTitle('Already Claimed!') + .setDescription(`You've already claimed your daily XP.\nNext claim: `) + .addFields({ name: 'Current Streak', value: `šŸ”„ ${streak} days` }) + ] + }); + } + + if (hoursSince > 48) { + streak = 0; + } + } + + streak += 1; + const streakBonus = Math.min(streak * STREAK_BONUS, MAX_STREAK_BONUS); + const totalXp = DAILY_XP + streakBonus; + const newXp = currentXp + totalXp; + + await supabase + .from('user_profiles') + .update({ + xp: newXp, + daily_streak: streak, + last_daily: now.toISOString() + }) + .eq('id', link.user_id); + + const newLevel = Math.floor(Math.sqrt(newXp / 100)); + const oldLevel = Math.floor(Math.sqrt(currentXp / 100)); + + const embed = new EmbedBuilder() + .setColor(0x00ff00) + .setTitle('Daily Reward Claimed!') + .setDescription(`You received **+${totalXp} XP**!`) + .addFields( + { name: 'Base XP', value: `+${DAILY_XP}`, inline: true }, + { name: 'Streak Bonus', value: `+${streakBonus}`, inline: true }, + { name: 'Current Streak', value: `šŸ”„ ${streak} days`, inline: true }, + { name: 'Total XP', value: newXp.toLocaleString(), inline: true }, + { name: 'Level', value: `${newLevel}`, inline: true } + ) + .setFooter({ text: 'Come back tomorrow to keep your streak!' }) + .setTimestamp(); + + if (newLevel > oldLevel) { + embed.addFields({ name: 'šŸŽ‰ Level Up!', value: `You reached level ${newLevel}!` }); + } + + await interaction.editReply({ embeds: [embed] }); + + } catch (error) { + console.error('Daily error:', error); + await interaction.editReply({ content: 'Failed to claim daily reward.' }); + } + }, +}; diff --git a/aethex-bot/commands/foundation.js b/aethex-bot/commands/foundation.js new file mode 100644 index 0000000..b99851a --- /dev/null +++ b/aethex-bot/commands/foundation.js @@ -0,0 +1,82 @@ +const { SlashCommandBuilder, EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } = require('discord.js'); + +module.exports = { + data: new SlashCommandBuilder() + .setName('foundation') + .setDescription('View AeThex Foundation info and your contribution stats') + .addUserOption(option => + option.setName('user') + .setDescription('User to view (defaults to yourself)') + .setRequired(false) + ), + + async execute(interaction, supabase, client) { + const target = interaction.options.getUser('user') || interaction.user; + await interaction.deferReply(); + + let contributionData = null; + + if (supabase) { + try { + const { data: link } = await supabase + .from('discord_links') + .select('user_id') + .eq('discord_id', target.id) + .single(); + + if (link) { + const { data: contribution } = await supabase + .from('foundation_contributions') + .select('total_donated, volunteer_hours, badges') + .eq('user_id', link.user_id) + .single(); + + contributionData = contribution; + } + } catch (e) { + console.warn('Foundation data fetch failed:', e.message); + } + } + + const embed = new EmbedBuilder() + .setColor(0x7c3aed) + .setTitle('AeThex Foundation') + .setDescription('Supporting creators, developers, and the gaming community.') + .setThumbnail(target.displayAvatarURL()) + .addFields( + { name: 'Mission', value: 'Empowering the next generation of creators through education, resources, and community support.' } + ) + .setTimestamp(); + + if (contributionData) { + embed.addFields( + { name: `${target.tag}'s Contributions`, value: '\u200b' }, + { name: 'Total Donated', value: `$${contributionData.total_donated || 0}`, inline: true }, + { name: 'Volunteer Hours', value: `${contributionData.volunteer_hours || 0}h`, inline: true } + ); + + if (contributionData.badges && contributionData.badges.length > 0) { + embed.addFields({ name: 'Badges', value: contributionData.badges.join(', ') }); + } + } else if (target.id === interaction.user.id) { + embed.addFields({ + name: 'Your Contributions', + value: 'Link your account with `/verify` to track your contributions!' + }); + } + + const row = new ActionRowBuilder() + .addComponents( + new ButtonBuilder() + .setLabel('Visit Foundation') + .setStyle(ButtonStyle.Link) + .setURL('https://aethex.foundation'), + new ButtonBuilder() + .setLabel('Donate') + .setStyle(ButtonStyle.Link) + .setURL('https://aethex.foundation/donate') + ); + + await interaction.editReply({ embeds: [embed], components: [row] }); + }, +}; diff --git a/aethex-bot/commands/kick.js b/aethex-bot/commands/kick.js new file mode 100644 index 0000000..2c46be5 --- /dev/null +++ b/aethex-bot/commands/kick.js @@ -0,0 +1,86 @@ +const { SlashCommandBuilder, EmbedBuilder, PermissionFlagsBits } = require('discord.js'); + +module.exports = { + data: new SlashCommandBuilder() + .setName('kick') + .setDescription('Kick a user from the server') + .setDefaultMemberPermissions(PermissionFlagsBits.KickMembers) + .addUserOption(option => + option.setName('user') + .setDescription('User to kick') + .setRequired(true) + ) + .addStringOption(option => + option.setName('reason') + .setDescription('Reason for kick') + .setRequired(false) + .setMaxLength(500) + ), + + async execute(interaction, supabase, client) { + const target = interaction.options.getUser('user'); + const reason = interaction.options.getString('reason') || 'No reason provided'; + const moderator = interaction.user; + + const member = await interaction.guild.members.fetch(target.id).catch(() => null); + + if (!member) { + return interaction.reply({ content: 'User not found in this server.', ephemeral: true }); + } + + if (!member.kickable) { + return interaction.reply({ content: 'I cannot kick this user. They may have higher permissions.', ephemeral: true }); + } + + if (target.id === interaction.user.id) { + return interaction.reply({ content: 'You cannot kick yourself.', ephemeral: true }); + } + + try { + await target.send({ + embeds: [ + new EmbedBuilder() + .setColor(0xff6b6b) + .setTitle(`Kicked from ${interaction.guild.name}`) + .addFields({ name: 'Reason', value: reason }) + .setTimestamp() + ] + }).catch(() => {}); + + await member.kick(reason); + + if (supabase) { + try { + await supabase.from('mod_actions').insert({ + guild_id: interaction.guildId, + action: 'kick', + user_id: target.id, + user_tag: target.tag, + moderator_id: moderator.id, + moderator_tag: moderator.tag, + reason: reason, + }); + } catch (e) { + console.warn('Failed to log kick:', e.message); + } + } + + const embed = new EmbedBuilder() + .setColor(0xff6b6b) + .setTitle('User Kicked') + .setThumbnail(target.displayAvatarURL()) + .addFields( + { name: 'User', value: `${target.tag} (${target.id})`, inline: true }, + { name: 'Moderator', value: moderator.tag, inline: true }, + { name: 'Reason', value: reason } + ) + .setTimestamp(); + + await interaction.reply({ embeds: [embed] }); + + } catch (error) { + console.error('Kick error:', error); + await interaction.reply({ content: 'Failed to kick user.', ephemeral: true }); + } + }, +}; diff --git a/aethex-bot/commands/modlog.js b/aethex-bot/commands/modlog.js new file mode 100644 index 0000000..fda333c --- /dev/null +++ b/aethex-bot/commands/modlog.js @@ -0,0 +1,73 @@ +const { SlashCommandBuilder, EmbedBuilder, PermissionFlagsBits } = require('discord.js'); + +module.exports = { + data: new SlashCommandBuilder() + .setName('modlog') + .setDescription('View moderation history for a user') + .setDefaultMemberPermissions(PermissionFlagsBits.ModerateMembers) + .addUserOption(option => + option.setName('user') + .setDescription('User to check') + .setRequired(true) + ), + + async execute(interaction, supabase, client) { + if (!supabase) { + return interaction.reply({ content: 'Database not configured.', ephemeral: true }); + } + + const target = interaction.options.getUser('user'); + await interaction.deferReply({ ephemeral: true }); + + try { + const { data: warnings } = await supabase + .from('warnings') + .select('*') + .eq('guild_id', interaction.guildId) + .eq('user_id', target.id) + .order('created_at', { ascending: false }) + .limit(10); + + const { data: actions } = await supabase + .from('mod_actions') + .select('*') + .eq('guild_id', interaction.guildId) + .eq('user_id', target.id) + .order('created_at', { ascending: false }) + .limit(10); + + const embed = new EmbedBuilder() + .setColor(0x7c3aed) + .setTitle(`Moderation Log: ${target.tag}`) + .setThumbnail(target.displayAvatarURL()) + .setTimestamp(); + + if ((!warnings || warnings.length === 0) && (!actions || actions.length === 0)) { + embed.setDescription('No moderation history found for this user.'); + } else { + if (warnings && warnings.length > 0) { + const warnText = warnings.map((w, i) => { + const date = new Date(w.created_at).toLocaleDateString(); + return `**${i + 1}.** ${w.reason}\n By: ${w.moderator_tag} | ${date}`; + }).join('\n\n'); + embed.addFields({ name: `Warnings (${warnings.length})`, value: warnText.slice(0, 1024) }); + } + + if (actions && actions.length > 0) { + const actionText = actions.map((a, i) => { + const date = new Date(a.created_at).toLocaleDateString(); + const duration = a.duration_minutes ? ` (${a.duration_minutes}m)` : ''; + return `**${a.action.toUpperCase()}${duration}** - ${a.reason}\n By: ${a.moderator_tag} | ${date}`; + }).join('\n\n'); + embed.addFields({ name: `Actions (${actions.length})`, value: actionText.slice(0, 1024) }); + } + } + + await interaction.editReply({ embeds: [embed] }); + + } catch (error) { + console.error('Modlog error:', error); + await interaction.editReply({ content: 'Failed to fetch moderation log.' }); + } + }, +}; diff --git a/aethex-bot/commands/rank.js b/aethex-bot/commands/rank.js new file mode 100644 index 0000000..16f82d4 --- /dev/null +++ b/aethex-bot/commands/rank.js @@ -0,0 +1,86 @@ +const { SlashCommandBuilder, EmbedBuilder } = require('discord.js'); + +module.exports = { + data: new SlashCommandBuilder() + .setName('rank') + .setDescription('View your unified level and XP across all platforms') + .addUserOption(option => + option.setName('user') + .setDescription('User to check (defaults to yourself)') + .setRequired(false) + ), + + async execute(interaction, supabase, client) { + if (!supabase) { + return interaction.reply({ content: 'Database not configured.', ephemeral: true }); + } + + const target = interaction.options.getUser('user') || interaction.user; + await interaction.deferReply(); + + try { + const { data: link } = await supabase + .from('discord_links') + .select('user_id, primary_arm') + .eq('discord_id', target.id) + .single(); + + if (!link) { + return interaction.editReply({ + embeds: [ + new EmbedBuilder() + .setColor(0xff6b6b) + .setDescription(`${target.id === interaction.user.id ? 'You are' : `${target.tag} is`} not linked to AeThex. Use \`/verify\` to link your account.`) + ] + }); + } + + const { data: profile } = await supabase + .from('user_profiles') + .select('username, avatar_url, xp, bio') + .eq('id', link.user_id) + .single(); + + const xp = profile?.xp || 0; + const level = Math.floor(Math.sqrt(xp / 100)); + const currentLevelXp = level * level * 100; + const nextLevelXp = (level + 1) * (level + 1) * 100; + const progress = xp - currentLevelXp; + const needed = nextLevelXp - currentLevelXp; + const progressPercent = Math.floor((progress / needed) * 100); + + const progressBar = createProgressBar(progressPercent); + + const { count: rankPosition } = await supabase + .from('user_profiles') + .select('*', { count: 'exact', head: true }) + .gt('xp', xp); + + const embed = new EmbedBuilder() + .setColor(0x7c3aed) + .setTitle(`${profile?.username || target.tag}'s Rank`) + .setThumbnail(profile?.avatar_url || target.displayAvatarURL()) + .addFields( + { name: 'Level', value: `**${level}**`, inline: true }, + { name: 'Total XP', value: `**${xp.toLocaleString()}**`, inline: true }, + { name: 'Rank', value: `#${(rankPosition || 0) + 1}`, inline: true }, + { name: 'Progress to Next Level', value: `${progressBar}\n${progress.toLocaleString()} / ${needed.toLocaleString()} XP (${progressPercent}%)` }, + { name: 'Primary Realm', value: link.primary_arm || 'None set', inline: true } + ) + .setFooter({ text: 'XP earned across Discord & AeThex platforms' }) + .setTimestamp(); + + await interaction.editReply({ embeds: [embed] }); + + } catch (error) { + console.error('Rank error:', error); + await interaction.editReply({ content: 'Failed to fetch rank data.' }); + } + }, +}; + +function createProgressBar(percent) { + const filled = Math.floor(percent / 10); + const empty = 10 - filled; + return 'ā–ˆ'.repeat(filled) + 'ā–‘'.repeat(empty); +} diff --git a/aethex-bot/commands/serverinfo.js b/aethex-bot/commands/serverinfo.js new file mode 100644 index 0000000..2cba9d7 --- /dev/null +++ b/aethex-bot/commands/serverinfo.js @@ -0,0 +1,53 @@ +const { SlashCommandBuilder, EmbedBuilder, ChannelType } = require('discord.js'); + +module.exports = { + data: new SlashCommandBuilder() + .setName('serverinfo') + .setDescription('View information about this server'), + + async execute(interaction, supabase, client) { + const guild = interaction.guild; + + const textChannels = guild.channels.cache.filter(c => c.type === ChannelType.GuildText).size; + const voiceChannels = guild.channels.cache.filter(c => c.type === ChannelType.GuildVoice).size; + const categories = guild.channels.cache.filter(c => c.type === ChannelType.GuildCategory).size; + + const roles = guild.roles.cache.size - 1; + const emojis = guild.emojis.cache.size; + + const boostLevel = guild.premiumTier; + const boostCount = guild.premiumSubscriptionCount || 0; + + const owner = await guild.fetchOwner().catch(() => null); + + const embed = new EmbedBuilder() + .setColor(0x7c3aed) + .setTitle(guild.name) + .setThumbnail(guild.iconURL({ size: 256 })) + .addFields( + { name: 'ID', value: guild.id, inline: true }, + { name: 'Owner', value: owner ? owner.user.tag : 'Unknown', inline: true }, + { name: 'Created', value: ``, inline: true }, + { name: 'Members', value: `${guild.memberCount}`, inline: true }, + { name: 'Roles', value: `${roles}`, inline: true }, + { name: 'Emojis', value: `${emojis}`, inline: true }, + { name: 'Text Channels', value: `${textChannels}`, inline: true }, + { name: 'Voice Channels', value: `${voiceChannels}`, inline: true }, + { name: 'Categories', value: `${categories}`, inline: true }, + { name: 'Boost Level', value: `Tier ${boostLevel}`, inline: true }, + { name: 'Boosts', value: `${boostCount}`, inline: true }, + { name: 'Verification', value: guild.verificationLevel.toString(), inline: true } + ) + .setTimestamp(); + + if (guild.description) { + embed.setDescription(guild.description); + } + + if (guild.bannerURL()) { + embed.setImage(guild.bannerURL({ size: 512 })); + } + + await interaction.reply({ embeds: [embed] }); + }, +}; diff --git a/aethex-bot/commands/studio.js b/aethex-bot/commands/studio.js new file mode 100644 index 0000000..ef7ac9f --- /dev/null +++ b/aethex-bot/commands/studio.js @@ -0,0 +1,83 @@ +const { SlashCommandBuilder, EmbedBuilder } = require('discord.js'); + +module.exports = { + data: new SlashCommandBuilder() + .setName('studio') + .setDescription('View your AeThex Studio profile and projects') + .addUserOption(option => + option.setName('user') + .setDescription('User to view (defaults to yourself)') + .setRequired(false) + ), + + async execute(interaction, supabase, client) { + if (!supabase) { + return interaction.reply({ content: 'Database not configured.', ephemeral: true }); + } + + const target = interaction.options.getUser('user') || interaction.user; + await interaction.deferReply(); + + try { + const { data: link } = await supabase + .from('discord_links') + .select('user_id') + .eq('discord_id', target.id) + .single(); + + if (!link) { + return interaction.editReply({ + embeds: [ + new EmbedBuilder() + .setColor(0xff6b6b) + .setDescription(`${target.id === interaction.user.id ? 'You are' : `${target.tag} is`} not linked to AeThex.`) + ] + }); + } + + const { data: profile } = await supabase + .from('user_profiles') + .select('username, avatar_url, bio') + .eq('id', link.user_id) + .single(); + + const { data: projects, count: projectCount } = await supabase + .from('studio_projects') + .select('id, title, description, created_at, likes', { count: 'exact' }) + .eq('user_id', link.user_id) + .order('created_at', { ascending: false }) + .limit(5); + + const embed = new EmbedBuilder() + .setColor(0x7c3aed) + .setTitle(`${profile?.username || target.tag}'s Studio`) + .setThumbnail(profile?.avatar_url || target.displayAvatarURL()) + .setDescription(profile?.bio || 'No bio set') + .addFields( + { name: 'Total Projects', value: `${projectCount || 0}`, inline: true } + ) + .setTimestamp(); + + if (projects && projects.length > 0) { + const projectList = projects.map((p, i) => { + const likes = p.likes || 0; + return `**${i + 1}. ${p.title}**\nā¤ļø ${likes} likes`; + }).join('\n\n'); + embed.addFields({ name: 'Recent Projects', value: projectList }); + } else { + embed.addFields({ name: 'Projects', value: 'No projects yet' }); + } + + embed.addFields({ + name: 'Visit Studio', + value: '[aethex.studio](https://aethex.studio)' + }); + + await interaction.editReply({ embeds: [embed] }); + + } catch (error) { + console.error('Studio error:', error); + await interaction.editReply({ content: 'Failed to fetch studio data.' }); + } + }, +}; diff --git a/aethex-bot/commands/timeout.js b/aethex-bot/commands/timeout.js new file mode 100644 index 0000000..2167a49 --- /dev/null +++ b/aethex-bot/commands/timeout.js @@ -0,0 +1,105 @@ +const { SlashCommandBuilder, EmbedBuilder, PermissionFlagsBits } = require('discord.js'); + +module.exports = { + data: new SlashCommandBuilder() + .setName('timeout') + .setDescription('Timeout a user') + .setDefaultMemberPermissions(PermissionFlagsBits.ModerateMembers) + .addUserOption(option => + option.setName('user') + .setDescription('User to timeout') + .setRequired(true) + ) + .addIntegerOption(option => + option.setName('duration') + .setDescription('Duration in minutes') + .setRequired(true) + .setMinValue(1) + .setMaxValue(40320) + ) + .addStringOption(option => + option.setName('reason') + .setDescription('Reason for timeout') + .setRequired(false) + .setMaxLength(500) + ), + + async execute(interaction, supabase, client) { + const target = interaction.options.getUser('user'); + const duration = interaction.options.getInteger('duration'); + const reason = interaction.options.getString('reason') || 'No reason provided'; + const moderator = interaction.user; + + const member = await interaction.guild.members.fetch(target.id).catch(() => null); + + if (!member) { + return interaction.reply({ content: 'User not found in this server.', ephemeral: true }); + } + + if (!member.moderatable) { + return interaction.reply({ content: 'I cannot timeout this user. They may have higher permissions.', ephemeral: true }); + } + + if (target.id === interaction.user.id) { + return interaction.reply({ content: 'You cannot timeout yourself.', ephemeral: true }); + } + + const durationMs = duration * 60 * 1000; + const endsAt = new Date(Date.now() + durationMs); + + try { + await member.timeout(durationMs, `${reason} | By ${moderator.tag}`); + + if (supabase) { + try { + await supabase.from('mod_actions').insert({ + guild_id: interaction.guildId, + action: 'timeout', + user_id: target.id, + user_tag: target.tag, + moderator_id: moderator.id, + moderator_tag: moderator.tag, + reason: reason, + duration_minutes: duration, + }); + } catch (e) { + console.warn('Failed to log timeout:', e.message); + } + } + + const embed = new EmbedBuilder() + .setColor(0xfbbf24) + .setTitle('User Timed Out') + .setThumbnail(target.displayAvatarURL()) + .addFields( + { name: 'User', value: `${target.tag} (${target.id})`, inline: true }, + { name: 'Moderator', value: moderator.tag, inline: true }, + { name: 'Duration', value: `${duration} minutes`, inline: true }, + { name: 'Ends', value: ``, inline: true }, + { name: 'Reason', value: reason } + ) + .setTimestamp(); + + await interaction.reply({ embeds: [embed] }); + + try { + await target.send({ + embeds: [ + new EmbedBuilder() + .setColor(0xfbbf24) + .setTitle(`Timed out in ${interaction.guild.name}`) + .addFields( + { name: 'Duration', value: `${duration} minutes` }, + { name: 'Reason', value: reason } + ) + .setTimestamp() + ] + }); + } catch (e) {} + + } catch (error) { + console.error('Timeout error:', error); + await interaction.reply({ content: 'Failed to timeout user.', ephemeral: true }); + } + }, +}; diff --git a/aethex-bot/commands/userinfo.js b/aethex-bot/commands/userinfo.js new file mode 100644 index 0000000..d3234f7 --- /dev/null +++ b/aethex-bot/commands/userinfo.js @@ -0,0 +1,80 @@ +const { SlashCommandBuilder, EmbedBuilder } = require('discord.js'); + +module.exports = { + data: new SlashCommandBuilder() + .setName('userinfo') + .setDescription('View information about a user') + .addUserOption(option => + option.setName('user') + .setDescription('User to view (defaults to yourself)') + .setRequired(false) + ), + + async execute(interaction, supabase, client) { + const target = interaction.options.getUser('user') || interaction.user; + const member = await interaction.guild.members.fetch(target.id).catch(() => null); + + const embed = new EmbedBuilder() + .setColor(0x7c3aed) + .setTitle(target.tag) + .setThumbnail(target.displayAvatarURL({ size: 256 })) + .addFields( + { name: 'ID', value: target.id, inline: true }, + { name: 'Bot', value: target.bot ? 'Yes' : 'No', inline: true }, + { name: 'Account Created', value: ``, inline: true } + ); + + if (member) { + embed.addFields( + { name: 'Joined Server', value: ``, inline: true }, + { name: 'Nickname', value: member.nickname || 'None', inline: true }, + { name: 'Highest Role', value: member.roles.highest.toString(), inline: true } + ); + + const roles = member.roles.cache + .filter(r => r.id !== interaction.guildId) + .sort((a, b) => b.position - a.position) + .map(r => r.toString()) + .slice(0, 10); + + if (roles.length > 0) { + embed.addFields({ + name: `Roles (${member.roles.cache.size - 1})`, + value: roles.join(', ') + (member.roles.cache.size > 11 ? '...' : '') + }); + } + } + + if (supabase) { + try { + const { data: link } = await supabase + .from('discord_links') + .select('user_id, primary_arm, linked_at') + .eq('discord_id', target.id) + .single(); + + if (link) { + const { data: profile } = await supabase + .from('user_profiles') + .select('username, xp') + .eq('id', link.user_id) + .single(); + + embed.addFields( + { name: 'AeThex Linked', value: 'Yes', inline: true }, + { name: 'Platform Username', value: profile?.username || 'Unknown', inline: true }, + { name: 'Realm', value: link.primary_arm || 'None', inline: true } + ); + + if (profile?.xp) { + const level = Math.floor(Math.sqrt(profile.xp / 100)); + embed.addFields({ name: 'Level', value: `${level} (${profile.xp} XP)`, inline: true }); + } + } + } catch (e) {} + } + + embed.setTimestamp(); + await interaction.reply({ embeds: [embed] }); + }, +}; diff --git a/aethex-bot/commands/warn.js b/aethex-bot/commands/warn.js new file mode 100644 index 0000000..75258fb --- /dev/null +++ b/aethex-bot/commands/warn.js @@ -0,0 +1,90 @@ +const { SlashCommandBuilder, EmbedBuilder, PermissionFlagsBits } = require('discord.js'); + +module.exports = { + data: new SlashCommandBuilder() + .setName('warn') + .setDescription('Warn a user') + .setDefaultMemberPermissions(PermissionFlagsBits.ModerateMembers) + .addUserOption(option => + option.setName('user') + .setDescription('User to warn') + .setRequired(true) + ) + .addStringOption(option => + option.setName('reason') + .setDescription('Reason for warning') + .setRequired(true) + .setMaxLength(500) + ), + + async execute(interaction, supabase, client) { + const target = interaction.options.getUser('user'); + const reason = interaction.options.getString('reason'); + const moderator = interaction.user; + + if (target.id === interaction.user.id) { + return interaction.reply({ content: 'You cannot warn yourself.', ephemeral: true }); + } + + if (target.bot) { + return interaction.reply({ content: 'You cannot warn bots.', ephemeral: true }); + } + + let warningCount = 1; + + if (supabase) { + try { + await supabase.from('warnings').insert({ + guild_id: interaction.guildId, + user_id: target.id, + user_tag: target.tag, + moderator_id: moderator.id, + moderator_tag: moderator.tag, + reason: reason, + }); + + const { count } = await supabase + .from('warnings') + .select('*', { count: 'exact', head: true }) + .eq('guild_id', interaction.guildId) + .eq('user_id', target.id); + + warningCount = count || 1; + } catch (e) { + console.warn('Failed to save warning:', e.message); + } + } + + const embed = new EmbedBuilder() + .setColor(0xfbbf24) + .setTitle('User Warned') + .setThumbnail(target.displayAvatarURL()) + .addFields( + { name: 'User', value: `${target.tag} (${target.id})`, inline: true }, + { name: 'Moderator', value: moderator.tag, inline: true }, + { name: 'Warning #', value: `${warningCount}`, inline: true }, + { name: 'Reason', value: reason } + ) + .setTimestamp(); + + await interaction.reply({ embeds: [embed] }); + + try { + await target.send({ + embeds: [ + new EmbedBuilder() + .setColor(0xfbbf24) + .setTitle(`Warning in ${interaction.guild.name}`) + .setDescription(`You have been warned by a moderator.`) + .addFields( + { name: 'Reason', value: reason }, + { name: 'Warning #', value: `${warningCount}` } + ) + .setTimestamp() + ] + }); + } catch (e) { + await interaction.followUp({ content: 'Could not DM user about the warning.', ephemeral: true }); + } + }, +}; diff --git a/aethex-bot/listeners/goodbye.js b/aethex-bot/listeners/goodbye.js new file mode 100644 index 0000000..01b40d0 --- /dev/null +++ b/aethex-bot/listeners/goodbye.js @@ -0,0 +1,41 @@ +const { EmbedBuilder } = require('discord.js'); + +module.exports = { + name: 'guildMemberRemove', + + async execute(member, client, supabase) { + if (!supabase) return; + + try { + const { data: config } = await supabase + .from('server_config') + .select('goodbye_channel') + .eq('guild_id', member.guild.id) + .single(); + + if (!config || !config.goodbye_channel) return; + + const channel = await client.channels.fetch(config.goodbye_channel).catch(() => null); + if (!channel) return; + + const embed = new EmbedBuilder() + .setColor(0xff6b6b) + .setTitle('Goodbye!') + .setDescription(`${member.user.tag} has left the server.`) + .setThumbnail(member.displayAvatarURL({ size: 256 })) + .addFields( + { name: 'Members Now', value: `${member.guild.memberCount}`, inline: true }, + { name: 'Was Member For', value: member.joinedTimestamp + ? `` + : 'Unknown', inline: true } + ) + .setFooter({ text: `ID: ${member.id}` }) + .setTimestamp(); + + await channel.send({ embeds: [embed] }); + + } catch (error) { + console.error('Goodbye error:', error.message); + } + } +}; diff --git a/aethex-bot/listeners/welcome.js b/aethex-bot/listeners/welcome.js new file mode 100644 index 0000000..34a0556 --- /dev/null +++ b/aethex-bot/listeners/welcome.js @@ -0,0 +1,47 @@ +const { EmbedBuilder } = require('discord.js'); + +module.exports = { + name: 'guildMemberAdd', + + async execute(member, client, supabase) { + if (!supabase) return; + + try { + const { data: config } = await supabase + .from('server_config') + .select('welcome_channel, auto_role') + .eq('guild_id', member.guild.id) + .single(); + + if (!config) return; + + if (config.auto_role) { + await member.roles.add(config.auto_role).catch(err => { + console.warn(`Failed to add auto-role: ${err.message}`); + }); + } + + if (config.welcome_channel) { + const channel = await client.channels.fetch(config.welcome_channel).catch(() => null); + if (!channel) return; + + const embed = new EmbedBuilder() + .setColor(0x7c3aed) + .setTitle('Welcome!') + .setDescription(`Welcome to **${member.guild.name}**, ${member}!`) + .setThumbnail(member.displayAvatarURL({ size: 256 })) + .addFields( + { name: 'Member #', value: `${member.guild.memberCount}`, inline: true }, + { name: 'Account Created', value: ``, inline: true } + ) + .setFooter({ text: `ID: ${member.id}` }) + .setTimestamp(); + + await channel.send({ embeds: [embed] }); + } + + } catch (error) { + console.error('Welcome error:', error.message); + } + } +}; diff --git a/aethex-bot/listeners/xpTracker.js b/aethex-bot/listeners/xpTracker.js new file mode 100644 index 0000000..42a8475 --- /dev/null +++ b/aethex-bot/listeners/xpTracker.js @@ -0,0 +1,95 @@ +const XP_PER_MESSAGE = 5; +const XP_COOLDOWN_MS = 60000; +const xpCooldowns = new Map(); + +module.exports = { + name: 'messageCreate', + + async execute(message, client, supabase) { + if (!supabase) return; + if (message.author.bot) return; + if (!message.guild) return; + + const discordUserId = message.author.id; + const now = Date.now(); + const lastXp = xpCooldowns.get(discordUserId) || 0; + + if (now - lastXp < XP_COOLDOWN_MS) return; + + try { + const { data: link, error: linkError } = await supabase + .from('discord_links') + .select('user_id') + .eq('discord_id', discordUserId) + .maybeSingle(); + + if (linkError || !link) return; + + const { data: profile, error: profileError } = await supabase + .from('user_profiles') + .select('xp') + .eq('id', link.user_id) + .maybeSingle(); + + if (profileError || !profile) return; + + const currentXp = profile.xp || 0; + const newXp = currentXp + XP_PER_MESSAGE; + const oldLevel = Math.floor(Math.sqrt(currentXp / 100)); + const newLevel = Math.floor(Math.sqrt(newXp / 100)); + + const { error: updateError } = await supabase + .from('user_profiles') + .update({ xp: newXp }) + .eq('id', link.user_id); + + if (updateError) return; + + xpCooldowns.set(discordUserId, now); + + if (newLevel > oldLevel) { + const config = client.serverConfigs?.get(message.guildId); + const levelUpChannelId = config?.level_up_channel; + + const levelUpMessage = `šŸŽ‰ Congratulations ${message.author}! You reached **Level ${newLevel}**!`; + + if (levelUpChannelId) { + const channel = await client.channels.fetch(levelUpChannelId).catch(() => null); + if (channel) { + await channel.send(levelUpMessage); + } + } else { + await message.channel.send(levelUpMessage).catch(() => {}); + } + + await checkLevelRoles(message.member, newLevel, supabase, message.guildId); + } + + } catch (error) { + console.error('XP tracking error:', error.message); + } + } +}; + +async function checkLevelRoles(member, level, supabase, guildId) { + if (!member || !supabase) return; + + try { + const { data: levelRoles, error } = await supabase + .from('level_roles') + .select('role_id, level_required') + .eq('guild_id', guildId) + .lte('level_required', level) + .order('level_required', { ascending: true }); + + if (error || !levelRoles || levelRoles.length === 0) return; + + for (const lr of levelRoles) { + if (!member.roles.cache.has(lr.role_id)) { + await member.roles.add(lr.role_id).catch(() => {}); + } + } + } catch (e) { + // Table may not exist yet - silently ignore + } +} diff --git a/replit.md b/replit.md index 1ebfc7d..2fb536d 100644 --- a/replit.md +++ b/replit.md @@ -1,52 +1,73 @@ # AeThex Unified Bot -A complete Discord bot combining AeThex community features and Sentinel enterprise security in one instance. +A complete Discord bot combining AeThex community features, Sentinel enterprise security, and multi-purpose server management in one instance. ## Overview -AeThex Unified Bot handles both community features AND security: +AeThex Unified Bot handles community features, security, AND general server management: - **Community Features**: User verification, profile linking, realm selection, leaderboards, community posts - **Sentinel Security**: Anti-nuke protection with RAM-based heat tracking - **Federation Sync**: Cross-server role synchronization across 5 realms - **Ticket System**: Support tickets with automatic channel creation -- **Admin Monitoring**: Real-time status, threat monitoring, server overview +- **Moderation**: Full moderation suite (warn, kick, ban, timeout) +- **Leveling System**: Unified XP across Discord and AeThex platform +- **Cross-Platform**: Integration with AeThex.studio and AeThex.foundation ## Tech Stack - **Runtime**: Node.js 20 - **Framework**: discord.js v14 -- **Database**: Supabase (optional - for user verification and community features) +- **Database**: Supabase (for verification, XP, moderation logs) - **Health Endpoint**: HTTP server on port 8080 ## Project Structure ``` aethex-bot/ -ā”œā”€ā”€ bot.js # Main entry point (merged: original + Sentinel) +ā”œā”€ā”€ bot.js # Main entry point ā”œā”€ā”€ package.json -ā”œā”€ā”€ Dockerfile # Docker deployment config -ā”œā”€ā”€ discloud.config # DisCloud hosting config -ā”œā”€ā”€ DEPLOYMENT_GUIDE.md # Deployment documentation +ā”œā”€ā”€ public/ +│ └── dashboard.html # Web dashboard ā”œā”€ā”€ commands/ │ ā”œā”€ā”€ admin.js # /admin status|heat|servers|threats|federation +│ ā”œā”€ā”€ announce.js # /announce - cross-server announcements +│ ā”œā”€ā”€ auditlog.js # /auditlog - admin action history +│ ā”œā”€ā”€ avatar.js # /avatar - get user avatar +│ ā”œā”€ā”€ badges.js # /badges - view earned badges +│ ā”œā”€ā”€ ban.js # /ban - ban users +│ ā”œā”€ā”€ config.js # /config - server settings +│ ā”œā”€ā”€ daily.js # /daily - claim daily XP │ ā”œā”€ā”€ federation.js # /federation link|unlink|list +│ ā”œā”€ā”€ foundation.js # /foundation - foundation stats │ ā”œā”€ā”€ help.js # /help - command list +│ ā”œā”€ā”€ kick.js # /kick - kick users │ ā”œā”€ā”€ leaderboard.js # /leaderboard - top contributors +│ ā”œā”€ā”€ modlog.js # /modlog - user mod history +│ ā”œā”€ā”€ poll.js # /poll - community polls │ ā”œā”€ā”€ post.js # /post - community feed posts │ ā”œā”€ā”€ profile.js # /profile - view linked profile +│ ā”œā”€ā”€ rank.js # /rank - view level and XP │ ā”œā”€ā”€ refresh-roles.js # /refresh-roles - sync roles +│ ā”œā”€ā”€ serverinfo.js # /serverinfo - server stats │ ā”œā”€ā”€ set-realm.js # /set-realm - choose primary realm │ ā”œā”€ā”€ stats.js # /stats - user statistics │ ā”œā”€ā”€ status.js # /status - network overview +│ ā”œā”€ā”€ studio.js # /studio - studio profile │ ā”œā”€ā”€ ticket.js # /ticket create|close +│ ā”œā”€ā”€ timeout.js # /timeout - timeout users │ ā”œā”€ā”€ unlink.js # /unlink - disconnect account +│ ā”œā”€ā”€ userinfo.js # /userinfo - user details │ ā”œā”€ā”€ verify-role.js # /verify-role - check roles -│ └── verify.js # /verify - link account +│ ā”œā”€ā”€ verify.js # /verify - link account +│ └── warn.js # /warn - warn users ā”œā”€ā”€ events/ │ └── messageCreate.js # Message event handler ā”œā”€ā”€ listeners/ │ ā”œā”€ā”€ feedSync.js # Community feed sync +│ ā”œā”€ā”€ welcome.js # Welcome messages + auto-role +│ ā”œā”€ā”€ goodbye.js # Goodbye messages +│ ā”œā”€ā”€ xpTracker.js # XP tracking on messages │ └── sentinel/ │ ā”œā”€ā”€ antiNuke.js # Channel delete monitor │ ā”œā”€ā”€ roleDelete.js # Role delete monitor @@ -56,9 +77,9 @@ aethex-bot/ └── register-commands.js # Slash command registration ``` -## Commands (14 Total) +## Commands (29 Total) -### Community Commands (10) +### Community Commands | Command | Description | |---------|-------------| | `/verify` | Link your Discord account to AeThex | @@ -72,14 +93,56 @@ aethex-bot/ | `/post` | Create a community feed post | | `/help` | View all bot commands | -### Sentinel Commands (4) +### Leveling & Engagement +| Command | Description | +|---------|-------------| +| `/rank` | View your level and unified XP | +| `/daily` | Claim daily XP bonus | +| `/badges` | View earned badges across platforms | + +### Moderation +| Command | Description | +|---------|-------------| +| `/warn @user [reason]` | Warn a user | +| `/kick @user [reason]` | Kick a user | +| `/ban @user [reason]` | Ban a user | +| `/timeout @user [minutes] [reason]` | Timeout a user | +| `/modlog @user` | View moderation history | + +### Utility +| Command | Description | +|---------|-------------| +| `/userinfo [@user]` | View user information | +| `/serverinfo` | View server statistics | +| `/avatar [@user]` | Get user's avatar | + +### Admin & Config +| Command | Description | +|---------|-------------| +| `/config view` | View server configuration | +| `/config welcome #channel` | Set welcome channel | +| `/config goodbye #channel` | Set goodbye channel | +| `/config modlog #channel` | Set mod log channel | +| `/config levelup #channel` | Set level-up announcement channel | +| `/config autorole @role` | Set auto-role for new members | +| `/config levelrole @role [level]` | Add level-based role reward | +| `/announce [title] [message]` | Send cross-server announcement | +| `/poll [question] [options]` | Create community poll | +| `/auditlog` | View admin action history | + +### Cross-Platform +| Command | Description | +|---------|-------------| +| `/studio [@user]` | View AeThex Studio profile | +| `/foundation [@user]` | View Foundation contributions | + +### Sentinel Security | Command | Description | |---------|-------------| | `/admin status` | View bot status and statistics | | `/admin heat @user` | Check heat level of a user | | `/admin servers` | View all connected servers | | `/admin threats` | View active threat monitor | -| `/admin federation` | View federation role mappings | | `/federation link @role` | Link a role for cross-server sync | | `/federation unlink @role` | Remove a role from sync | | `/federation list` | List all linked roles | @@ -87,63 +150,94 @@ aethex-bot/ | `/ticket close` | Close the current ticket | | `/status` | View network status | -## Sentinel Security System +## Unified XP System -Anti-nuke system using RAM-based heat tracking for instant response: +XP is earned across all platforms and stored in a single profile: -- **Heat Threshold**: 3 dangerous actions in 10 seconds triggers auto-ban -- **Monitored Actions**: Channel delete, role delete, member ban, member kick -- **Alerts**: Sends to configured alert channel and DMs server owner -- **Whitelist**: Set `WHITELISTED_USERS` env var for trusted users +- **Discord Messages**: +5 XP per message (60s cooldown) +- **Daily Claims**: +50 XP base + streak bonus (up to +100) +- **Platform Activity**: Posts, likes, comments on AeThex sites + +Level formula: `level = floor(sqrt(xp / 100))` + +## Supabase Tables Required + +```sql +-- Server configuration +CREATE TABLE server_config ( + guild_id TEXT PRIMARY KEY, + welcome_channel TEXT, + goodbye_channel TEXT, + modlog_channel TEXT, + level_up_channel TEXT, + auto_role TEXT, + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +-- Warnings +CREATE TABLE warnings ( + id SERIAL PRIMARY KEY, + guild_id TEXT NOT NULL, + user_id TEXT NOT NULL, + user_tag TEXT, + moderator_id TEXT NOT NULL, + moderator_tag TEXT, + reason TEXT, + created_at TIMESTAMPTZ DEFAULT NOW() +); + +-- Moderation actions +CREATE TABLE mod_actions ( + id SERIAL PRIMARY KEY, + guild_id TEXT NOT NULL, + action TEXT NOT NULL, + user_id TEXT NOT NULL, + user_tag TEXT, + moderator_id TEXT NOT NULL, + moderator_tag TEXT, + reason TEXT, + duration_minutes INTEGER, + created_at TIMESTAMPTZ DEFAULT NOW() +); + +-- Level roles +CREATE TABLE level_roles ( + guild_id TEXT NOT NULL, + role_id TEXT NOT NULL, + level_required INTEGER NOT NULL, + PRIMARY KEY (guild_id, role_id) +); + +-- Add to user_profiles (if not exists) +ALTER TABLE user_profiles ADD COLUMN IF NOT EXISTS xp INTEGER DEFAULT 0; +ALTER TABLE user_profiles ADD COLUMN IF NOT EXISTS daily_streak INTEGER DEFAULT 0; +ALTER TABLE user_profiles ADD COLUMN IF NOT EXISTS last_daily TIMESTAMPTZ; +ALTER TABLE user_profiles ADD COLUMN IF NOT EXISTS badges JSONB DEFAULT '[]'; +``` ## Environment Variables ### Required - `DISCORD_BOT_TOKEN` - Bot token from Discord Developer Portal -- `DISCORD_CLIENT_ID` - Application ID (e.g., 578971245454950421) +- `DISCORD_CLIENT_ID` - Application ID -### Optional - Supabase (for community features) +### Optional - Supabase - `SUPABASE_URL` - Supabase project URL - `SUPABASE_SERVICE_ROLE` - Supabase service role key ### Optional - Federation -- `HUB_GUILD_ID` - Main hub server -- `LABS_GUILD_ID`, `GAMEFORGE_GUILD_ID`, `CORP_GUILD_ID`, `FOUNDATION_GUILD_ID` +- `HUB_GUILD_ID`, `LABS_GUILD_ID`, `GAMEFORGE_GUILD_ID`, `CORP_GUILD_ID`, `FOUNDATION_GUILD_ID` ### Optional - Security - `WHITELISTED_USERS` - Comma-separated user IDs to skip heat tracking - `ALERT_CHANNEL_ID` - Channel for security alerts - -### Optional - Feed Sync -- `DISCORD_FEED_CHANNEL_ID` - Channel for community feed -- `DISCORD_FEED_GUILD_ID` - Guild for community feed -- `DISCORD_MAIN_CHAT_CHANNELS` - Comma-separated channel IDs +- `EXTRA_WHITELISTED_GUILDS` - Additional whitelisted server IDs ## Health Endpoints -**GET /health** (port 8080) -```json -{ - "status": "online", - "guilds": 8, - "commands": 14, - "uptime": 3600, - "heatMapSize": 0, - "supabaseConnected": false, - "timestamp": "2025-12-07T23:00:00.000Z" -} -``` - -**GET /stats** (port 8080) -```json -{ - "guilds": [...], - "totalMembers": 500, - "uptime": 3600, - "activeTickets": 0, - "heatEvents": 0 -} -``` +**GET /health** - Bot health status +**GET /stats** - Server statistics +**GET /dashboard** - Web dashboard ## Running the Bot @@ -153,21 +247,18 @@ npm install npm start ``` -Commands are registered automatically on startup or via POST to `/register-commands`. - ## Current Status - Bot running as AeThex#9389 -- Connected to 8 servers -- 14 commands loaded -- 4 Sentinel listeners active -- Health endpoint on port 8080 -- Supabase optional (community features limited when not configured) +- 29 commands loaded +- Unified XP system active +- Welcome/goodbye system active +- Moderation suite active +- Cross-platform integration ready ## Workflow - **Name**: AeThex Unified Bot - **Command**: `cd aethex-bot && npm start` -- **Runtime**: Node.js 20 (no Python required) -- **Deployment**: VM target with `sh -c "cd aethex-bot && npm start"` +- **Runtime**: Node.js 20 - **Status**: Running