Add server mode configuration and dynamic status updates

Introduces a new server mode configuration system (Federation/Standalone) with associated command changes, dynamic status rotation for the bot, and adds new commands and features.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: aed2e46d-25bb-4b73-81a1-bb9e8437c261
Replit-Commit-Checkpoint-Type: intermediate_checkpoint
Replit-Commit-Event-Id: b08e6ba5-7498-4b9f-b1c9-7dc11b362ddd
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/3bdfff67-975a-46ad-9845-fbb6b4a4c4b5/aed2e46d-25bb-4b73-81a1-bb9e8437c261/R9PkDi8
Replit-Helium-Checkpoint-Created: true
This commit is contained in:
sirpiglr 2025-12-09 23:26:33 +00:00
parent d7d8da51af
commit c2a34f398e
42 changed files with 4960 additions and 746 deletions

View file

@ -21,10 +21,6 @@ externalPort = 80
localPort = 8080
externalPort = 8080
[[ports]]
localPort = 34949
externalPort = 3001
[workflows]
runButton = "Project"

View file

@ -718,7 +718,7 @@ if (fs.existsSync(sentinelPath)) {
// =============================================================================
const listenersPath = path.join(__dirname, "listeners");
const generalListenerFiles = ['welcome.js', 'goodbye.js', 'xpTracker.js', 'reactionXp.js', 'voiceXp.js'];
const generalListenerFiles = ['welcome.js', 'goodbye.js', 'xpTracker.js', 'reactionXp.js', 'voiceXp.js', 'starboard.js'];
for (const file of generalListenerFiles) {
const filePath = path.join(listenersPath, file);
if (fs.existsSync(filePath)) {
@ -2499,6 +2499,36 @@ client.login(token).catch((error) => {
process.exit(1);
});
// =============================================================================
// DYNAMIC STATUS ROTATION
// =============================================================================
function startDynamicStatus(client) {
const statuses = [
() => ({ name: `${client.guilds.cache.size} servers`, type: 3 }), // Watching
() => ({ name: `${client.guilds.cache.reduce((sum, g) => sum + g.memberCount, 0).toLocaleString()} members`, type: 3 }), // Watching
() => ({ name: '/help | aethex.studio', type: 0 }), // Playing
() => ({ name: '🛡️ Guarding the Federation', type: 4 }), // Custom
() => ({ name: 'for threats', type: 3 }), // Watching
() => ({ name: '⚔️ Warden • Free Forever', type: 4 }), // Custom
];
let currentIndex = 0;
const updateStatus = () => {
try {
const status = statuses[currentIndex]();
client.user.setActivity(status.name, { type: status.type });
currentIndex = (currentIndex + 1) % statuses.length;
} catch (e) {
console.error('[Status] Error updating status:', e.message);
}
};
updateStatus();
setInterval(updateStatus, 30000); // Rotate every 30 seconds
}
client.once("clientReady", async () => {
console.log(`Bot logged in as ${client.user.tag}`);
console.log(`Bot ID: ${client.user.id}`);
@ -2520,13 +2550,14 @@ client.once("clientReady", async () => {
console.error("Failed to register commands:", regResult.error);
}
client.user.setActivity("Protecting the Federation", { type: 3 });
// Dynamic rotating status
startDynamicStatus(client);
if (setupFeedListener && supabase) {
setupFeedListener(client);
}
sendAlert(`AeThex Bot is now online! Watching ${client.guilds.cache.size} servers.`);
sendAlert(`Warden is now online! Watching ${client.guilds.cache.size} servers.`);
});
// =============================================================================

View file

@ -0,0 +1,61 @@
const { SlashCommandBuilder, EmbedBuilder } = require('discord.js');
const { getServerMode, getEmbedColor } = require('../utils/modeHelper');
const RESPONSES = [
{ text: 'It is certain.', positive: true },
{ text: 'It is decidedly so.', positive: true },
{ text: 'Without a doubt.', positive: true },
{ text: 'Yes, definitely.', positive: true },
{ text: 'You may rely on it.', positive: true },
{ text: 'As I see it, yes.', positive: true },
{ text: 'Most likely.', positive: true },
{ text: 'Outlook good.', positive: true },
{ text: 'Yes.', positive: true },
{ text: 'Signs point to yes.', positive: true },
{ text: 'Reply hazy, try again.', positive: null },
{ text: 'Ask again later.', positive: null },
{ text: 'Better not tell you now.', positive: null },
{ text: 'Cannot predict now.', positive: null },
{ text: 'Concentrate and ask again.', positive: null },
{ text: "Don't count on it.", positive: false },
{ text: 'My reply is no.', positive: false },
{ text: 'My sources say no.', positive: false },
{ text: 'Outlook not so good.', positive: false },
{ text: 'Very doubtful.', positive: false },
];
module.exports = {
data: new SlashCommandBuilder()
.setName('8ball')
.setDescription('Ask the magic 8-ball a question')
.addStringOption(option =>
option.setName('question')
.setDescription('Your question for the 8-ball')
.setRequired(true)
.setMaxLength(256)
),
async execute(interaction, supabase, client) {
const question = interaction.options.getString('question');
const response = RESPONSES[Math.floor(Math.random() * RESPONSES.length)];
const mode = await getServerMode(supabase, interaction.guildId);
let color;
if (response.positive === true) color = 0x22C55E;
else if (response.positive === false) color = 0xEF4444;
else color = 0xF59E0B;
const embed = new EmbedBuilder()
.setColor(color)
.setTitle('🎱 Magic 8-Ball')
.addFields(
{ name: '❓ Question', value: question },
{ name: '🔮 Answer', value: response.text }
)
.setFooter({ text: `Asked by ${interaction.user.username}` })
.setTimestamp();
await interaction.reply({ embeds: [embed] });
},
};

View file

@ -0,0 +1,96 @@
const { SlashCommandBuilder, EmbedBuilder } = require('discord.js');
const { getServerMode, getEmbedColor } = require('../utils/modeHelper');
const afkUsers = new Map();
module.exports = {
data: new SlashCommandBuilder()
.setName('afk')
.setDescription('Set your AFK status')
.addStringOption(option =>
option.setName('reason')
.setDescription('Reason for being AFK')
.setRequired(false)
.setMaxLength(200)
),
afkUsers,
async execute(interaction, supabase, client) {
const reason = interaction.options.getString('reason') || 'AFK';
const userId = interaction.user.id;
const guildId = interaction.guildId;
const mode = await getServerMode(supabase, interaction.guildId);
if (afkUsers.has(`${guildId}-${userId}`)) {
afkUsers.delete(`${guildId}-${userId}`);
const embed = new EmbedBuilder()
.setColor(0x22C55E)
.setTitle('👋 Welcome Back!')
.setDescription('Your AFK status has been removed.')
.setTimestamp();
return interaction.reply({ embeds: [embed], ephemeral: true });
}
afkUsers.set(`${guildId}-${userId}`, {
reason,
timestamp: Date.now(),
username: interaction.user.username
});
const embed = new EmbedBuilder()
.setColor(getEmbedColor(mode))
.setTitle('💤 AFK Status Set')
.setDescription(`You are now AFK: **${reason}**`)
.addFields({
name: '💡 Tip',
value: 'Use `/afk` again or send a message to remove your AFK status.'
})
.setTimestamp();
await interaction.reply({ embeds: [embed] });
},
checkAfk(message) {
const guildId = message.guildId;
const userId = message.author.id;
const key = `${guildId}-${userId}`;
if (afkUsers.has(key)) {
const afkData = afkUsers.get(key);
afkUsers.delete(key);
const duration = Math.floor((Date.now() - afkData.timestamp) / 1000);
let timeStr;
if (duration < 60) timeStr = `${duration} seconds`;
else if (duration < 3600) timeStr = `${Math.floor(duration / 60)} minutes`;
else timeStr = `${Math.floor(duration / 3600)} hours`;
message.reply({
content: `👋 Welcome back! You were AFK for ${timeStr}.`,
allowedMentions: { repliedUser: false }
}).catch(() => {});
}
const mentions = message.mentions.users;
mentions.forEach(user => {
const mentionKey = `${guildId}-${user.id}`;
if (afkUsers.has(mentionKey)) {
const afkData = afkUsers.get(mentionKey);
const ago = Math.floor((Date.now() - afkData.timestamp) / 1000);
let timeStr;
if (ago < 60) timeStr = `${ago} seconds ago`;
else if (ago < 3600) timeStr = `${Math.floor(ago / 60)} minutes ago`;
else timeStr = `${Math.floor(ago / 3600)} hours ago`;
message.reply({
content: `💤 **${afkData.username}** is AFK: ${afkData.reason} (set ${timeStr})`,
allowedMentions: { repliedUser: false }
}).catch(() => {});
}
});
}
};

View file

@ -0,0 +1,187 @@
const { SlashCommandBuilder, EmbedBuilder } = require('discord.js');
const { getServerMode, getEmbedColor, EMBED_COLORS } = require('../utils/modeHelper');
module.exports = {
data: new SlashCommandBuilder()
.setName('birthday')
.setDescription('Set or view birthdays')
.addSubcommand(subcommand =>
subcommand
.setName('set')
.setDescription('Set your birthday')
.addIntegerOption(option =>
option.setName('month')
.setDescription('Birth month (1-12)')
.setRequired(true)
.setMinValue(1)
.setMaxValue(12)
)
.addIntegerOption(option =>
option.setName('day')
.setDescription('Birth day (1-31)')
.setRequired(true)
.setMinValue(1)
.setMaxValue(31)
)
)
.addSubcommand(subcommand =>
subcommand
.setName('view')
.setDescription('View someone\'s birthday')
.addUserOption(option =>
option.setName('user')
.setDescription('The user to check')
.setRequired(false)
)
)
.addSubcommand(subcommand =>
subcommand
.setName('upcoming')
.setDescription('View upcoming birthdays in this server')
)
.addSubcommand(subcommand =>
subcommand
.setName('remove')
.setDescription('Remove your birthday')
),
async execute(interaction, supabase, client) {
const subcommand = interaction.options.getSubcommand();
const mode = await getServerMode(supabase, interaction.guildId);
const guildId = interaction.guildId;
const userId = interaction.user.id;
if (subcommand === 'set') {
const month = interaction.options.getInteger('month');
const day = interaction.options.getInteger('day');
const daysInMonth = [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
if (day > daysInMonth[month - 1]) {
return interaction.reply({
content: `Invalid day for month ${month}. Maximum is ${daysInMonth[month - 1]}.`,
ephemeral: true
});
}
if (supabase) {
try {
await supabase.from('birthdays').upsert({
guild_id: guildId,
user_id: userId,
username: interaction.user.username,
month: month,
day: day,
updated_at: new Date().toISOString()
}, { onConflict: 'guild_id,user_id' });
} catch (e) {}
}
const monthNames = ['January', 'February', 'March', 'April', 'May', 'June',
'July', 'August', 'September', 'October', 'November', 'December'];
const embed = new EmbedBuilder()
.setColor(EMBED_COLORS.success)
.setTitle('🎂 Birthday Set!')
.setDescription(`Your birthday is now set to **${monthNames[month - 1]} ${day}**!`)
.setTimestamp();
await interaction.reply({ embeds: [embed], ephemeral: true });
}
else if (subcommand === 'view') {
const targetUser = interaction.options.getUser('user') || interaction.user;
if (!supabase) {
return interaction.reply({ content: 'Birthday system unavailable.', ephemeral: true });
}
const { data } = await supabase
.from('birthdays')
.select('month, day')
.eq('guild_id', guildId)
.eq('user_id', targetUser.id)
.maybeSingle();
if (!data) {
return interaction.reply({
content: targetUser.id === userId
? 'You haven\'t set your birthday yet! Use `/birthday set`.'
: `${targetUser.username} hasn't set their birthday.`,
ephemeral: true
});
}
const monthNames = ['January', 'February', 'March', 'April', 'May', 'June',
'July', 'August', 'September', 'October', 'November', 'December'];
const embed = new EmbedBuilder()
.setColor(getEmbedColor(mode))
.setTitle('🎂 Birthday')
.setDescription(`**${targetUser.username}**'s birthday is on **${monthNames[data.month - 1]} ${data.day}**!`)
.setThumbnail(targetUser.displayAvatarURL({ size: 128 }))
.setTimestamp();
await interaction.reply({ embeds: [embed] });
}
else if (subcommand === 'upcoming') {
if (!supabase) {
return interaction.reply({ content: 'Birthday system unavailable.', ephemeral: true });
}
const { data } = await supabase
.from('birthdays')
.select('user_id, username, month, day')
.eq('guild_id', guildId);
if (!data || data.length === 0) {
return interaction.reply({ content: 'No birthdays set in this server yet!', ephemeral: true });
}
const now = new Date();
const currentMonth = now.getMonth() + 1;
const currentDay = now.getDate();
const sorted = data.map(b => {
let daysUntil = (b.month - currentMonth) * 30 + (b.day - currentDay);
if (daysUntil < 0) daysUntil += 365;
return { ...b, daysUntil };
}).sort((a, b) => a.daysUntil - b.daysUntil).slice(0, 10);
const monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
const list = sorted.map((b, i) => {
const emoji = b.daysUntil === 0 ? '🎉' : '🎂';
const status = b.daysUntil === 0 ? '**TODAY!**' : `in ${b.daysUntil} days`;
return `${i + 1}. ${emoji} **${b.username}** - ${monthNames[b.month - 1]} ${b.day} (${status})`;
}).join('\n');
const embed = new EmbedBuilder()
.setColor(getEmbedColor(mode))
.setTitle('🎂 Upcoming Birthdays')
.setDescription(list || 'No upcoming birthdays.')
.setTimestamp();
await interaction.reply({ embeds: [embed] });
}
else if (subcommand === 'remove') {
if (supabase) {
await supabase
.from('birthdays')
.delete()
.eq('guild_id', guildId)
.eq('user_id', userId);
}
const embed = new EmbedBuilder()
.setColor(EMBED_COLORS.success)
.setTitle('🗑️ Birthday Removed')
.setDescription('Your birthday has been removed from this server.')
.setTimestamp();
await interaction.reply({ embeds: [embed], ephemeral: true });
}
},
};

View file

@ -0,0 +1,44 @@
const { SlashCommandBuilder, EmbedBuilder } = require('discord.js');
const { getServerMode, getEmbedColor } = require('../utils/modeHelper');
module.exports = {
data: new SlashCommandBuilder()
.setName('coinflip')
.setDescription('Flip a coin')
.addStringOption(option =>
option.setName('call')
.setDescription('Call heads or tails before the flip')
.setRequired(false)
.addChoices(
{ name: 'Heads', value: 'heads' },
{ name: 'Tails', value: 'tails' }
)
),
async execute(interaction, supabase, client) {
const call = interaction.options.getString('call');
const result = Math.random() < 0.5 ? 'heads' : 'tails';
const emoji = result === 'heads' ? '🪙' : '💿';
const mode = await getServerMode(supabase, interaction.guildId);
const embed = new EmbedBuilder()
.setColor(getEmbedColor(mode))
.setTitle(`${emoji} Coin Flip`)
.setDescription(`The coin landed on **${result.toUpperCase()}**!`)
.setTimestamp();
if (call) {
const won = call === result;
embed.addFields({
name: won ? '🎉 Result' : '😔 Result',
value: won ? `You called it! You win!` : `You called ${call}, better luck next time!`
});
embed.setColor(won ? 0x22C55E : 0xEF4444);
}
embed.setFooter({ text: `Flipped by ${interaction.user.username}` });
await interaction.reply({ embeds: [embed] });
},
};

View file

@ -0,0 +1,122 @@
const { SlashCommandBuilder, EmbedBuilder, AttachmentBuilder } = require('discord.js');
const { getServerMode } = require('../utils/modeHelper');
function hexToRgb(hex) {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result ? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16)
} : null;
}
function rgbToHsl(r, g, b) {
r /= 255; g /= 255; b /= 255;
const max = Math.max(r, g, b), min = Math.min(r, g, b);
let h, s, l = (max + min) / 2;
if (max === min) {
h = s = 0;
} else {
const d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
switch (max) {
case r: h = ((g - b) / d + (g < b ? 6 : 0)) / 6; break;
case g: h = ((b - r) / d + 2) / 6; break;
case b: h = ((r - g) / d + 4) / 6; break;
}
}
return {
h: Math.round(h * 360),
s: Math.round(s * 100),
l: Math.round(l * 100)
};
}
module.exports = {
data: new SlashCommandBuilder()
.setName('color')
.setDescription('View color information')
.addStringOption(option =>
option.setName('hex')
.setDescription('Hex color code (e.g., #FF5733 or FF5733)')
.setRequired(false)
)
.addIntegerOption(option =>
option.setName('red')
.setDescription('Red value (0-255)')
.setRequired(false)
.setMinValue(0)
.setMaxValue(255)
)
.addIntegerOption(option =>
option.setName('green')
.setDescription('Green value (0-255)')
.setRequired(false)
.setMinValue(0)
.setMaxValue(255)
)
.addIntegerOption(option =>
option.setName('blue')
.setDescription('Blue value (0-255)')
.setRequired(false)
.setMinValue(0)
.setMaxValue(255)
),
async execute(interaction, supabase, client) {
const hexInput = interaction.options.getString('hex');
const red = interaction.options.getInteger('red');
const green = interaction.options.getInteger('green');
const blue = interaction.options.getInteger('blue');
let r, g, b, hexColor;
if (hexInput) {
const rgb = hexToRgb(hexInput);
if (!rgb) {
return interaction.reply({
content: 'Invalid hex color. Use format like #FF5733 or FF5733.',
ephemeral: true
});
}
r = rgb.r;
g = rgb.g;
b = rgb.b;
hexColor = hexInput.replace('#', '').toUpperCase();
} else if (red !== null && green !== null && blue !== null) {
r = red;
g = green;
b = blue;
hexColor = ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1).toUpperCase();
} else if (red !== null || green !== null || blue !== null) {
return interaction.reply({
content: 'Please provide all RGB values (red, green, blue) or use a hex code.',
ephemeral: true
});
} else {
r = Math.floor(Math.random() * 256);
g = Math.floor(Math.random() * 256);
b = Math.floor(Math.random() * 256);
hexColor = ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1).toUpperCase();
}
const hsl = rgbToHsl(r, g, b);
const colorInt = parseInt(hexColor, 16);
const embed = new EmbedBuilder()
.setColor(colorInt)
.setTitle(`🎨 Color: #${hexColor}`)
.addFields(
{ name: 'Hex', value: `\`#${hexColor}\``, inline: true },
{ name: 'RGB', value: `\`rgb(${r}, ${g}, ${b})\``, inline: true },
{ name: 'HSL', value: `\`hsl(${hsl.h}, ${hsl.s}%, ${hsl.l}%)\``, inline: true },
{ name: 'Integer', value: `\`${colorInt}\``, inline: true }
)
.setThumbnail(`https://singlecolorimage.com/get/${hexColor}/100x100`)
.setTimestamp();
await interaction.reply({ embeds: [embed] });
},
};

View file

@ -1,4 +1,5 @@
const { SlashCommandBuilder, EmbedBuilder, PermissionFlagsBits, ChannelType } = require('discord.js');
const { SlashCommandBuilder, EmbedBuilder, PermissionFlagsBits, ChannelType, ActionRowBuilder, ButtonBuilder, ButtonStyle } = require('discord.js');
const { getServerMode, setServerMode, getEmbedColor, getModeDisplayName, getModeEmoji, EMBED_COLORS } = require('../utils/modeHelper');
module.exports = {
data: new SlashCommandBuilder()
@ -73,6 +74,10 @@ module.exports = {
.setMinValue(1)
.setMaxValue(100)
)
)
.addSubcommand(sub =>
sub.setName('mode')
.setDescription('Switch between Federation and Standalone mode')
),
async execute(interaction, supabase, client) {
@ -84,6 +89,86 @@ module.exports = {
await interaction.deferReply({ ephemeral: true });
try {
if (subcommand === 'mode') {
const currentMode = await getServerMode(supabase, interaction.guildId);
const embed = new EmbedBuilder()
.setColor(getEmbedColor(currentMode))
.setTitle('Server Mode Configuration')
.setDescription(
`${getModeEmoji(currentMode)} Currently running in **${getModeDisplayName(currentMode)}** mode\n\n` +
'Choose your server mode:'
)
.addFields(
{
name: '🌐 Federation Mode',
value: '• Unified XP across all AeThex servers\n• Cross-server profiles and leaderboards\n• Realm selection and role sync',
inline: true
},
{
name: '🏠 Standalone Mode',
value: '• Isolated XP system for this server\n• Local leaderboards only\n• Full moderation features',
inline: true
}
)
.setFooter({ text: 'This affects how XP and profiles work in your server' });
const row = new ActionRowBuilder()
.addComponents(
new ButtonBuilder()
.setCustomId('config_mode_federated')
.setLabel('Federation')
.setStyle(currentMode === 'federated' ? ButtonStyle.Primary : ButtonStyle.Secondary)
.setEmoji('🌐')
.setDisabled(currentMode === 'federated'),
new ButtonBuilder()
.setCustomId('config_mode_standalone')
.setLabel('Standalone')
.setStyle(currentMode === 'standalone' ? ButtonStyle.Primary : ButtonStyle.Secondary)
.setEmoji('🏠')
.setDisabled(currentMode === 'standalone')
);
const response = await interaction.editReply({ embeds: [embed], components: [row] });
const collector = response.createMessageComponentCollector({ time: 60000 });
collector.on('collect', async (i) => {
if (i.user.id !== interaction.user.id) {
return i.reply({ content: 'Only the command user can change this.', ephemeral: true });
}
const newMode = i.customId === 'config_mode_federated' ? 'federated' : 'standalone';
const success = await setServerMode(supabase, interaction.guildId, newMode);
if (success) {
const confirmEmbed = new EmbedBuilder()
.setColor(getEmbedColor(newMode))
.setTitle(`${getModeEmoji(newMode)} Mode Changed!`)
.setDescription(
`Server is now running in **${getModeDisplayName(newMode)}** mode.\n\n` +
(newMode === 'federated'
? 'XP will now count globally across the AeThex ecosystem.'
: 'XP is now tracked locally for this server only.')
)
.setTimestamp();
await i.update({ embeds: [confirmEmbed], components: [] });
collector.stop();
} else {
await i.reply({ content: 'Failed to change mode. Please try again.', ephemeral: true });
}
});
collector.on('end', async (collected, reason) => {
if (reason === 'time') {
await interaction.editReply({ components: [] }).catch(() => {});
}
});
return;
}
if (subcommand === 'view') {
const { data: config } = await supabase
.from('server_config')
@ -91,10 +176,13 @@ module.exports = {
.eq('guild_id', interaction.guildId)
.single();
const currentMode = config?.mode || 'federated';
const embed = new EmbedBuilder()
.setColor(0x7c3aed)
.setColor(getEmbedColor(currentMode))
.setTitle('Server Configuration')
.addFields(
{ name: 'Server Mode', value: `${getModeEmoji(currentMode)} ${getModeDisplayName(currentMode)}`, inline: true },
{ 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 },

View file

@ -1,6 +1,8 @@
const { SlashCommandBuilder, EmbedBuilder } = require('discord.js');
const { checkAchievements } = require('./achievements');
const { getUserStats, calculateLevel, updateQuestProgress } = require('../listeners/xpTracker');
const { getServerMode, EMBED_COLORS } = require('../utils/modeHelper');
const { claimStandaloneDaily, getStandaloneXp, calculateLevel: calcStandaloneLevel } = require('../utils/standaloneXp');
const DAILY_XP = 50;
const STREAK_BONUS = 10;
@ -19,6 +21,59 @@ module.exports = {
await interaction.deferReply();
try {
const mode = await getServerMode(supabase, interaction.guildId);
if (mode === 'standalone') {
return handleStandaloneDaily(interaction, supabase);
} else {
return handleFederatedDaily(interaction, supabase, client);
}
} catch (error) {
console.error('Daily error:', error);
await interaction.editReply({ content: 'Failed to claim daily reward.' });
}
},
};
async function handleStandaloneDaily(interaction, supabase) {
const result = await claimStandaloneDaily(
supabase,
interaction.user.id,
interaction.guildId,
interaction.user.username
);
if (!result.success) {
return interaction.editReply({
embeds: [
new EmbedBuilder()
.setColor(EMBED_COLORS.warning)
.setTitle('Already Claimed!')
.setDescription(result.message)
]
});
}
const level = calcStandaloneLevel(result.totalXp, 'normal');
const embed = new EmbedBuilder()
.setColor(EMBED_COLORS.success)
.setTitle('Daily Reward Claimed!')
.setDescription(`You received **+${result.xpGained} XP**!`)
.addFields(
{ name: 'Base XP', value: `+50`, inline: true },
{ name: 'Streak Bonus', value: `+${Math.min((result.streak - 1) * 5, 100)}`, inline: true },
{ name: 'Current Streak', value: `${result.streak} days`, inline: true },
{ name: 'Total XP', value: result.totalXp.toLocaleString(), inline: true },
{ name: 'Level', value: `${level}`, inline: true }
)
.setFooter({ text: `🏠 Standalone Mode • Come back tomorrow!` })
.setTimestamp();
await interaction.editReply({ embeds: [embed] });
}
async function handleFederatedDaily(interaction, supabase, client) {
const { data: link } = await supabase
.from('discord_links')
.select('user_id')
@ -58,7 +113,7 @@ module.exports = {
.setColor(0xfbbf24)
.setTitle('Already Claimed!')
.setDescription(`You've already claimed your daily XP.\nNext claim: <t:${Math.floor(nextClaim.getTime() / 1000)}:R>`)
.addFields({ name: 'Current Streak', value: `🔥 ${streak} days` })
.addFields({ name: 'Current Streak', value: `${streak} days` })
]
});
}
@ -71,13 +126,10 @@ module.exports = {
streak += 1;
const streakBonus = Math.min(streak * STREAK_BONUS, MAX_STREAK_BONUS);
// Prestige level 4+ gets bonus daily XP (+25)
const prestigeDailyBonus = prestige >= 4 ? 25 : 0;
// Base total before prestige multiplier
let totalXp = DAILY_XP + streakBonus + prestigeDailyBonus;
// Apply prestige XP bonus (+5% per prestige level)
if (prestige > 0) {
const prestigeMultiplier = 1 + (prestige * 0.05);
totalXp = Math.floor(totalXp * prestigeMultiplier);
@ -106,7 +158,7 @@ module.exports = {
.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: 'Current Streak', value: `${streak} days`, inline: true },
{ name: 'Total XP', value: newXp.toLocaleString(), inline: true },
{ name: 'Level', value: `${newLevel}`, inline: true }
);
@ -115,16 +167,15 @@ module.exports = {
embed.addFields({ name: 'Prestige Bonus', value: `+${prestige * 5}% XP${prestigeDailyBonus > 0 ? ` + ${prestigeDailyBonus} daily bonus` : ''}`, inline: true });
}
embed.setFooter({ text: 'Come back tomorrow to keep your streak!' })
embed.setFooter({ text: '🌐 Federation • Come back tomorrow to keep your streak!' })
.setTimestamp();
if (newLevel > oldLevel) {
embed.addFields({ name: '🎉 Level Up!', value: `You reached level ${newLevel}!` });
embed.addFields({ name: 'Level Up!', value: `You reached level ${newLevel}!` });
}
await interaction.editReply({ embeds: [embed] });
// Check achievements with updated stats
const guildId = interaction.guildId;
const stats = await getUserStats(supabase, link.user_id, guildId);
stats.level = newLevel;
@ -134,20 +185,13 @@ module.exports = {
await checkAchievements(link.user_id, interaction.member, stats, supabase, guildId, client);
// Track quest progress for daily claims and XP earned
await updateQuestProgress(supabase, link.user_id, guildId, 'daily_claims', 1);
await updateQuestProgress(supabase, link.user_id, guildId, 'xp_earned', totalXp);
if (newLevel > oldLevel) {
await updateQuestProgress(supabase, link.user_id, guildId, 'level_ups', 1);
}
} catch (error) {
console.error('Daily error:', error);
await interaction.editReply({ content: 'Failed to claim daily reward.' });
}
},
};
}
function getPrestigeColor(level) {
const colors = [0x6b7280, 0xcd7f32, 0xc0c0c0, 0xffd700, 0xe5e4e2, 0xb9f2ff, 0xff4500, 0x9400d3, 0xffd700, 0xff69b4, 0x7c3aed];

View file

@ -0,0 +1,76 @@
const { SlashCommandBuilder, EmbedBuilder } = require('discord.js');
const { getServerMode, getEmbedColor, EMBED_COLORS } = require('../utils/modeHelper');
module.exports = {
data: new SlashCommandBuilder()
.setName('define')
.setDescription('Look up the definition of a word')
.addStringOption(option =>
option.setName('word')
.setDescription('The word to define')
.setRequired(true)
.setMaxLength(100)
),
async execute(interaction, supabase, client) {
const word = interaction.options.getString('word').toLowerCase().trim();
const mode = await getServerMode(supabase, interaction.guildId);
await interaction.deferReply();
try {
const response = await fetch(`https://api.dictionaryapi.dev/api/v2/entries/en/${encodeURIComponent(word)}`);
if (!response.ok) {
const embed = new EmbedBuilder()
.setColor(EMBED_COLORS.error)
.setTitle('📖 Word Not Found')
.setDescription(`Could not find a definition for "**${word}**".`)
.setTimestamp();
return interaction.editReply({ embeds: [embed] });
}
const data = await response.json();
const entry = data[0];
const embed = new EmbedBuilder()
.setColor(getEmbedColor(mode))
.setTitle(`📖 ${entry.word}`)
.setTimestamp();
if (entry.phonetic) {
embed.setDescription(`*${entry.phonetic}*`);
}
const meanings = entry.meanings.slice(0, 3);
for (const meaning of meanings) {
const definitions = meaning.definitions.slice(0, 2);
const defText = definitions.map((d, i) => {
let text = `${i + 1}. ${d.definition}`;
if (d.example) {
text += `\n *"${d.example}"*`;
}
return text;
}).join('\n');
embed.addFields({
name: `${meaning.partOfSpeech}`,
value: defText.substring(0, 1024)
});
}
if (entry.sourceUrls && entry.sourceUrls[0]) {
embed.setFooter({ text: 'Source: Wiktionary' });
}
await interaction.editReply({ embeds: [embed] });
} catch (e) {
const embed = new EmbedBuilder()
.setColor(EMBED_COLORS.error)
.setTitle('📖 Error')
.setDescription('Failed to fetch definition. Please try again.')
.setTimestamp();
await interaction.editReply({ embeds: [embed] });
}
},
};

176
aethex-bot/commands/duel.js Normal file
View file

@ -0,0 +1,176 @@
const { SlashCommandBuilder, EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } = require('discord.js');
const { getServerMode, getEmbedColor, EMBED_COLORS } = require('../utils/modeHelper');
const { updateStandaloneXp } = require('../utils/standaloneXp');
const activeDuels = new Map();
module.exports = {
data: new SlashCommandBuilder()
.setName('duel')
.setDescription('Challenge someone to a duel!')
.addUserOption(option =>
option.setName('opponent')
.setDescription('The user to challenge')
.setRequired(true)
)
.addIntegerOption(option =>
option.setName('bet')
.setDescription('XP to bet (0-100)')
.setRequired(false)
.setMinValue(0)
.setMaxValue(100)
),
async execute(interaction, supabase, client) {
const opponent = interaction.options.getUser('opponent');
const bet = interaction.options.getInteger('bet') || 0;
const challenger = interaction.user;
const mode = await getServerMode(supabase, interaction.guildId);
const guildId = interaction.guildId;
if (opponent.id === challenger.id) {
return interaction.reply({ content: "You can't duel yourself!", ephemeral: true });
}
if (opponent.bot) {
return interaction.reply({ content: "You can't duel bots!", ephemeral: true });
}
const duelKey = `${guildId}-${challenger.id}`;
if (activeDuels.has(duelKey)) {
return interaction.reply({ content: 'You already have an active duel!', ephemeral: true });
}
const embed = new EmbedBuilder()
.setColor(getEmbedColor(mode))
.setTitle('⚔️ Duel Challenge!')
.setDescription(`${challenger} challenges ${opponent} to a duel!`)
.addFields(
{ name: '💰 Bet', value: bet > 0 ? `${bet} XP` : 'No bet', inline: true },
{ name: '⏱️ Expires', value: 'In 60 seconds', inline: true }
)
.setTimestamp();
const row = new ActionRowBuilder()
.addComponents(
new ButtonBuilder()
.setCustomId(`duel_accept_${challenger.id}_${opponent.id}`)
.setLabel('Accept')
.setStyle(ButtonStyle.Success),
new ButtonBuilder()
.setCustomId(`duel_decline_${challenger.id}_${opponent.id}`)
.setLabel('Decline')
.setStyle(ButtonStyle.Danger)
);
const message = await interaction.reply({
content: `${opponent}`,
embeds: [embed],
components: [row],
fetchReply: true
});
activeDuels.set(duelKey, { opponent: opponent.id, bet });
const collector = message.createMessageComponentCollector({
filter: i => i.user.id === opponent.id && i.customId.includes(challenger.id),
time: 60000,
max: 1
});
collector.on('collect', async (i) => {
activeDuels.delete(duelKey);
if (i.customId.startsWith('duel_decline')) {
const declineEmbed = new EmbedBuilder()
.setColor(EMBED_COLORS.error)
.setTitle('⚔️ Duel Declined')
.setDescription(`${opponent} declined the duel challenge.`)
.setTimestamp();
await i.update({ embeds: [declineEmbed], components: [] });
return;
}
const challengerRoll = Math.floor(Math.random() * 100) + 1;
const opponentRoll = Math.floor(Math.random() * 100) + 1;
let winner, loser, winnerRoll, loserRoll;
if (challengerRoll > opponentRoll) {
winner = challenger;
loser = opponent;
winnerRoll = challengerRoll;
loserRoll = opponentRoll;
} else if (opponentRoll > challengerRoll) {
winner = opponent;
loser = challenger;
winnerRoll = opponentRoll;
loserRoll = challengerRoll;
} else {
const tieEmbed = new EmbedBuilder()
.setColor(EMBED_COLORS.warning)
.setTitle('⚔️ It\'s a Tie!')
.setDescription(`Both rolled **${challengerRoll}**! Nobody wins.`)
.addFields(
{ name: `${challenger.username}`, value: `🎲 ${challengerRoll}`, inline: true },
{ name: `${opponent.username}`, value: `🎲 ${opponentRoll}`, inline: true }
)
.setTimestamp();
await i.update({ embeds: [tieEmbed], components: [] });
return;
}
if (bet > 0) {
if (mode === 'standalone') {
await updateStandaloneXp(supabase, winner.id, guildId, bet, winner.username);
} else if (supabase) {
try {
const { data: winnerProfile } = await supabase
.from('user_profiles')
.select('xp')
.eq('discord_id', winner.id)
.maybeSingle();
if (winnerProfile) {
await supabase
.from('user_profiles')
.update({ xp: (winnerProfile.xp || 0) + bet })
.eq('discord_id', winner.id);
}
} catch (e) {}
}
}
const resultEmbed = new EmbedBuilder()
.setColor(EMBED_COLORS.success)
.setTitle('⚔️ Duel Results!')
.setDescription(`🏆 **${winner.username}** wins the duel!`)
.addFields(
{ name: `${challenger.username}`, value: `🎲 Rolled: **${challengerRoll}**`, inline: true },
{ name: `${opponent.username}`, value: `🎲 Rolled: **${opponentRoll}**`, inline: true }
)
.setTimestamp();
if (bet > 0) {
resultEmbed.addFields({ name: '💰 Reward', value: `${winner.username} wins **${bet} XP**!` });
}
await i.update({ embeds: [resultEmbed], components: [] });
});
collector.on('end', async (collected) => {
if (collected.size === 0) {
activeDuels.delete(duelKey);
const timeoutEmbed = new EmbedBuilder()
.setColor(EMBED_COLORS.warning)
.setTitle('⚔️ Duel Expired')
.setDescription(`${opponent} didn't respond in time.`)
.setTimestamp();
await interaction.editReply({ embeds: [timeoutEmbed], components: [] });
}
});
},
};

View file

@ -1,4 +1,5 @@
const { SlashCommandBuilder, EmbedBuilder, PermissionFlagsBits } = require('discord.js');
const { getServerMode, EMBED_COLORS } = require('../utils/modeHelper');
module.exports = {
data: new SlashCommandBuilder()
@ -32,6 +33,16 @@ module.exports = {
),
async execute(interaction, supabase, client) {
const mode = await getServerMode(supabase, interaction.guildId);
if (mode === 'standalone') {
const embed = new EmbedBuilder()
.setColor(EMBED_COLORS.standalone)
.setTitle('🏠 Standalone Mode')
.setDescription('Federation features are disabled in standalone mode.\n\nThis server operates independently and does not sync roles across the AeThex network.\n\nUse `/config mode` to switch to federated mode.');
return interaction.reply({ embeds: [embed], ephemeral: true });
}
const subcommand = interaction.options.getSubcommand();
if (subcommand === 'link') {

View file

@ -0,0 +1,98 @@
const { SlashCommandBuilder, EmbedBuilder } = require('discord.js');
const { getServerMode, getEmbedColor, EMBED_COLORS } = require('../utils/modeHelper');
const { updateStandaloneXp, getStandaloneXp } = require('../utils/standaloneXp');
module.exports = {
data: new SlashCommandBuilder()
.setName('gift')
.setDescription('Gift XP to another user')
.addUserOption(option =>
option.setName('user')
.setDescription('User to gift XP to')
.setRequired(true)
)
.addIntegerOption(option =>
option.setName('amount')
.setDescription('Amount of XP to gift')
.setRequired(true)
.setMinValue(1)
.setMaxValue(1000)
),
async execute(interaction, supabase, client) {
const recipient = interaction.options.getUser('user');
const amount = interaction.options.getInteger('amount');
const sender = interaction.user;
const mode = await getServerMode(supabase, interaction.guildId);
const guildId = interaction.guildId;
if (recipient.id === sender.id) {
return interaction.reply({ content: "You can't gift XP to yourself!", ephemeral: true });
}
if (recipient.bot) {
return interaction.reply({ content: "You can't gift XP to bots!", ephemeral: true });
}
if (!supabase) {
return interaction.reply({ content: 'Gift system unavailable.', ephemeral: true });
}
try {
let senderXp = 0;
if (mode === 'standalone') {
const senderData = await getStandaloneXp(supabase, sender.id, guildId);
senderXp = senderData?.xp || 0;
} else {
const { data } = await supabase
.from('user_profiles')
.select('xp')
.eq('discord_id', sender.id)
.maybeSingle();
senderXp = data?.xp || 0;
}
if (senderXp < amount) {
return interaction.reply({
content: `You don't have enough XP! You have ${senderXp} XP.`,
ephemeral: true
});
}
if (mode === 'standalone') {
await updateStandaloneXp(supabase, sender.id, guildId, -amount, sender.username);
await updateStandaloneXp(supabase, recipient.id, guildId, amount, recipient.username);
} else {
await supabase
.from('user_profiles')
.update({ xp: senderXp - amount })
.eq('discord_id', sender.id);
const { data: recipientData } = await supabase
.from('user_profiles')
.select('xp')
.eq('discord_id', recipient.id)
.maybeSingle();
if (recipientData) {
await supabase
.from('user_profiles')
.update({ xp: (recipientData.xp || 0) + amount })
.eq('discord_id', recipient.id);
}
}
const embed = new EmbedBuilder()
.setColor(EMBED_COLORS.success)
.setTitle('🎁 Gift Sent!')
.setDescription(`${sender} gifted **${amount} XP** to ${recipient}!`)
.setThumbnail(recipient.displayAvatarURL({ size: 128 }))
.setTimestamp();
await interaction.reply({ embeds: [embed] });
} catch (e) {
await interaction.reply({ content: 'Failed to send gift.', ephemeral: true });
}
},
};

View file

@ -0,0 +1,189 @@
const { SlashCommandBuilder, EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } = require('discord.js');
const { getServerMode, getEmbedColor, EMBED_COLORS } = require('../utils/modeHelper');
const { updateStandaloneXp } = require('../utils/standaloneXp');
const activeHeists = new Map();
const TARGETS = [
{ name: 'Corner Store', emoji: '🏪', difficulty: 0.7, minReward: 20, maxReward: 50 },
{ name: 'Gas Station', emoji: '⛽', difficulty: 0.6, minReward: 30, maxReward: 70 },
{ name: 'Jewelry Store', emoji: '💎', difficulty: 0.5, minReward: 50, maxReward: 120 },
{ name: 'Bank', emoji: '🏦', difficulty: 0.4, minReward: 80, maxReward: 200 },
{ name: 'Casino', emoji: '🎰', difficulty: 0.3, minReward: 100, maxReward: 300 },
{ name: 'Federal Reserve', emoji: '🏛️', difficulty: 0.2, minReward: 150, maxReward: 500 },
];
module.exports = {
data: new SlashCommandBuilder()
.setName('heist')
.setDescription('Start a group heist!')
.addStringOption(option =>
option.setName('target')
.setDescription('Choose your heist target')
.setRequired(true)
.addChoices(
{ name: '🏪 Corner Store (Easy)', value: '0' },
{ name: '⛽ Gas Station', value: '1' },
{ name: '💎 Jewelry Store', value: '2' },
{ name: '🏦 Bank (Medium)', value: '3' },
{ name: '🎰 Casino', value: '4' },
{ name: '🏛️ Federal Reserve (Hard)', value: '5' }
)
),
async execute(interaction, supabase, client) {
const targetIndex = parseInt(interaction.options.getString('target'));
const target = TARGETS[targetIndex];
const leader = interaction.user;
const mode = await getServerMode(supabase, interaction.guildId);
const guildId = interaction.guildId;
const heistKey = `${guildId}-heist`;
if (activeHeists.has(heistKey)) {
return interaction.reply({
content: 'There\'s already an active heist in this server! Wait for it to finish.',
ephemeral: true
});
}
const participants = new Set([leader.id]);
activeHeists.set(heistKey, {
target,
leader: leader.id,
participants,
usernames: { [leader.id]: leader.username }
});
const embed = new EmbedBuilder()
.setColor(getEmbedColor(mode))
.setTitle(`${target.emoji} Heist: ${target.name}`)
.setDescription(`${leader} is planning a heist!\n\nClick **Join Heist** to participate.\nMore participants = higher success chance!`)
.addFields(
{ name: '🎯 Target', value: target.name, inline: true },
{ name: '💰 Reward', value: `${target.minReward}-${target.maxReward} XP each`, inline: true },
{ name: '📊 Base Success', value: `${Math.round(target.difficulty * 100)}%`, inline: true },
{ name: '👥 Participants', value: `1. ${leader.username}` }
)
.setFooter({ text: 'Heist starts in 60 seconds!' })
.setTimestamp();
const row = new ActionRowBuilder()
.addComponents(
new ButtonBuilder()
.setCustomId(`heist_join_${guildId}`)
.setLabel('Join Heist')
.setStyle(ButtonStyle.Primary)
.setEmoji('🔫'),
new ButtonBuilder()
.setCustomId(`heist_start_${guildId}`)
.setLabel('Start Now')
.setStyle(ButtonStyle.Success)
.setEmoji('🚀')
);
const message = await interaction.reply({ embeds: [embed], components: [row], fetchReply: true });
const collector = message.createMessageComponentCollector({
filter: i => i.customId.startsWith('heist_'),
time: 60000
});
collector.on('collect', async (i) => {
const heistData = activeHeists.get(heistKey);
if (!heistData) return;
if (i.customId.startsWith('heist_join')) {
if (heistData.participants.has(i.user.id)) {
return i.reply({ content: 'You\'re already in this heist!', ephemeral: true });
}
heistData.participants.add(i.user.id);
heistData.usernames[i.user.id] = i.user.username;
const participantList = Array.from(heistData.participants)
.map((id, idx) => `${idx + 1}. ${heistData.usernames[id]}`)
.join('\n');
const bonusChance = (heistData.participants.size - 1) * 5;
const totalChance = Math.min(95, Math.round(target.difficulty * 100) + bonusChance);
embed.spliceFields(3, 1, { name: '👥 Participants', value: participantList });
embed.spliceFields(2, 1, { name: '📊 Success Chance', value: `${totalChance}%`, inline: true });
await i.update({ embeds: [embed] });
}
if (i.customId.startsWith('heist_start') && i.user.id === heistData.leader) {
collector.stop('started');
}
});
collector.on('end', async (collected, reason) => {
const heistData = activeHeists.get(heistKey);
if (!heistData) return;
activeHeists.delete(heistKey);
const participantCount = heistData.participants.size;
const bonusChance = (participantCount - 1) * 5;
const successChance = Math.min(95, target.difficulty * 100 + bonusChance) / 100;
const success = Math.random() < successChance;
if (success) {
const baseReward = Math.floor(Math.random() * (target.maxReward - target.minReward + 1)) + target.minReward;
const teamBonus = Math.floor(baseReward * (participantCount - 1) * 0.1);
const totalReward = baseReward + teamBonus;
for (const participantId of heistData.participants) {
if (mode === 'standalone') {
await updateStandaloneXp(supabase, participantId, guildId, totalReward, heistData.usernames[participantId]);
} else if (supabase) {
try {
const { data: profile } = await supabase
.from('user_profiles')
.select('xp')
.eq('discord_id', participantId)
.maybeSingle();
if (profile) {
await supabase
.from('user_profiles')
.update({ xp: (profile.xp || 0) + totalReward })
.eq('discord_id', participantId);
}
} catch (e) {}
}
}
const participantMentions = Array.from(heistData.participants)
.map(id => `<@${id}>`)
.join(', ');
const successEmbed = new EmbedBuilder()
.setColor(EMBED_COLORS.success)
.setTitle(`${target.emoji} Heist Successful!`)
.setDescription(`The crew pulled off the ${target.name} heist!`)
.addFields(
{ name: '👥 Crew', value: participantMentions },
{ name: '💰 Each Earned', value: `${totalReward} XP` }
)
.setTimestamp();
await interaction.editReply({ embeds: [successEmbed], components: [] });
} else {
const failEmbed = new EmbedBuilder()
.setColor(EMBED_COLORS.error)
.setTitle(`${target.emoji} Heist Failed!`)
.setDescription(`The ${target.name} heist went wrong! The crew escaped empty-handed.`)
.addFields({
name: '👥 Crew',
value: Array.from(heistData.participants).map(id => heistData.usernames[id]).join(', ')
})
.setTimestamp();
await interaction.editReply({ embeds: [failEmbed], components: [] });
}
});
},
};

View file

@ -1,4 +1,5 @@
const { SlashCommandBuilder, EmbedBuilder, ActionRowBuilder, StringSelectMenuBuilder } = require("discord.js");
const { getServerMode, getEmbedColor, getModeEmoji, getModeDisplayName } = require('../utils/modeHelper');
module.exports = {
data: new SlashCommandBuilder()
@ -13,27 +14,33 @@ module.exports = {
{ name: '⚔️ Realms', value: 'realms' },
{ name: '📊 Community', value: 'community' },
{ name: '⭐ Leveling', value: 'leveling' },
{ name: '🎮 Fun & Games', value: 'fun' },
{ name: '💰 Economy', value: 'economy' },
{ name: '👥 Social', value: 'social' },
{ name: '🛡️ Moderation', value: 'moderation' },
{ name: '🔧 Utility', value: 'utility' },
{ name: '⚙️ Admin', value: 'admin' }
)
),
async execute(interaction) {
async execute(interaction, supabase) {
const category = interaction.options.getString('category');
const mode = await getServerMode(supabase, interaction.guildId);
if (category) {
const embed = getCategoryEmbed(category);
const embed = getCategoryEmbed(category, mode);
return interaction.reply({ embeds: [embed], ephemeral: true });
}
const modeIndicator = `${getModeEmoji(mode)} ${getModeDisplayName(mode)} Mode`;
const mainEmbed = new EmbedBuilder()
.setColor(0x7c3aed)
.setColor(getEmbedColor(mode))
.setAuthor({
name: 'AeThex Bot Help',
iconURL: interaction.client.user.displayAvatarURL()
})
.setDescription('Welcome to AeThex Bot! Select a category below to view commands.')
.setDescription(`Welcome to AeThex Bot! Select a category below to view commands.\n\n**Current Mode:** ${modeIndicator}`)
.addFields(
{
name: "🔗 Account",
@ -42,52 +49,52 @@ module.exports = {
},
{
name: "⚔️ Realms",
value: "`/set-realm` `/verify-role` `/refresh-roles`",
value: "`/set-realm` `/federation` `/refresh-roles`",
inline: true,
},
{
name: "📊 Community",
value: "`/stats` `/leaderboard` `/post` `/poll`",
value: "`/stats` `/leaderboard` `/poll` `/post`",
inline: true,
},
{
name: "⭐ Leveling",
value: "`/rank` `/daily` `/badges`",
value: "`/rank` `/daily` `/prestige` `/badges`",
inline: true,
},
{
name: "🎮 Fun & Games",
value: "`/8ball` `/roll` `/trivia` `/duel` `/slots`",
inline: true,
},
{
name: "💰 Economy",
value: "`/work` `/heist` `/gift` `/shop`",
inline: true,
},
{
name: "👥 Social",
value: "`/rep` `/hug` `/birthday` `/remind`",
inline: true,
},
{
name: "🛡️ Moderation",
value: "`/warn` `/kick` `/ban` `/timeout` `/modlog`",
value: "`/warn` `/kick` `/ban` `/timeout`",
inline: true,
},
{
name: "🔧 Utility",
value: "`/userinfo` `/serverinfo` `/avatar`",
value: "`/translate` `/define` `/math` `/qr`",
inline: true,
},
{
name: "⚙️ Admin",
value: "`/config` `/announce` `/embed` `/rolepanel`",
inline: true,
},
{
name: "🎉 Fun",
value: "`/giveaway` `/schedule`",
inline: true,
},
{
name: "🛡️ Auto-Mod",
value: "`/automod`",
value: "`/config` `/starboard` `/automod`",
inline: true,
}
)
.addFields({
name: "🔗 Quick Links",
value: "[AeThex Platform](https://aethex.dev) • [Creator Directory](https://aethex.dev/creators) • [Community Feed](https://aethex.dev/community/feed)",
inline: false,
})
.setFooter({
text: "Use /help [category] for detailed commands • AeThex Bot",
text: "Use /help [category] for detailed commands",
iconURL: interaction.client.user.displayAvatarURL()
})
.setTimestamp();
@ -100,6 +107,9 @@ module.exports = {
{ label: 'Realms', description: 'Realm selection and roles', emoji: '⚔️', value: 'realms' },
{ label: 'Community', description: 'Community features', emoji: '📊', value: 'community' },
{ label: 'Leveling', description: 'XP and leveling system', emoji: '⭐', value: 'leveling' },
{ label: 'Fun & Games', description: 'Fun commands and minigames', emoji: '🎮', value: 'fun' },
{ label: 'Economy', description: 'Earn and spend XP', emoji: '💰', value: 'economy' },
{ label: 'Social', description: 'Social interactions', emoji: '👥', value: 'social' },
{ label: 'Moderation', description: 'Moderation tools', emoji: '🛡️', value: 'moderation' },
{ label: 'Utility', description: 'Utility commands', emoji: '🔧', value: 'utility' },
{ label: 'Admin', description: 'Admin and config', emoji: '⚙️', value: 'admin' },
@ -111,7 +121,7 @@ module.exports = {
},
};
function getCategoryEmbed(category) {
function getCategoryEmbed(category, mode = 'federated') {
const categories = {
account: {
title: '🔗 Account Commands',
@ -119,16 +129,17 @@ function getCategoryEmbed(category) {
commands: [
{ name: '/verify', desc: 'Link your Discord account to AeThex' },
{ name: '/unlink', desc: 'Disconnect your Discord from AeThex' },
{ name: '/profile [@user]', desc: 'View your or another user\'s AeThex profile' },
{ name: '/profile [@user]', desc: 'View your or another user\'s profile' },
]
},
realms: {
title: '⚔️ Realm Commands',
color: 0xf97316,
commands: [
{ name: '/set-realm', desc: 'Choose your primary realm (Labs, GameForge, Corp, Foundation, Dev-Link)' },
{ name: '/verify-role', desc: 'Check your assigned Discord roles' },
{ name: '/set-realm', desc: 'Choose your primary realm (Federation mode only)' },
{ name: '/federation', desc: 'Manage cross-server role sync' },
{ name: '/refresh-roles', desc: 'Sync your roles based on AeThex profile' },
{ name: '/verify-role', desc: 'Check your assigned Discord roles' },
]
},
community: {
@ -147,9 +158,47 @@ function getCategoryEmbed(category) {
title: '⭐ Leveling Commands',
color: 0xfbbf24,
commands: [
{ name: '/rank [@user]', desc: 'View your level and unified XP' },
{ name: '/rank [@user]', desc: 'View your level and XP' },
{ name: '/daily', desc: 'Claim your daily XP bonus (+50 base + streak)' },
{ name: '/badges', desc: 'View earned badges across platforms' },
{ name: '/prestige', desc: 'Prestige at Level 50 for permanent bonuses' },
{ name: '/badges', desc: 'View earned badges' },
{ name: '/achievements', desc: 'View available achievements' },
{ name: '/quests', desc: 'View and track your quests' },
]
},
fun: {
title: '🎮 Fun & Games Commands',
color: 0xec4899,
commands: [
{ name: '/8ball [question]', desc: 'Ask the magic 8-ball a question' },
{ name: '/coinflip [call]', desc: 'Flip a coin' },
{ name: '/roll [dice]', desc: 'Roll dice (e.g., 2d6, d20, 3d8+5)' },
{ name: '/trivia [category]', desc: 'Answer trivia questions for XP' },
{ name: '/duel @user [bet]', desc: 'Challenge someone to a duel' },
{ name: '/slots [bet]', desc: 'Try your luck at the slot machine' },
{ name: '/afk [reason]', desc: 'Set your AFK status' },
]
},
economy: {
title: '💰 Economy Commands',
color: 0x10b981,
commands: [
{ name: '/work', desc: 'Work to earn XP (hourly)' },
{ name: '/heist [target]', desc: 'Start a group heist' },
{ name: '/gift @user [amount]', desc: 'Gift XP to another user' },
{ name: '/shop', desc: 'Browse and purchase items' },
{ name: '/inventory [@user]', desc: 'View your inventory' },
{ name: '/trade @user [offer] [request]', desc: 'Trade items with another user' },
]
},
social: {
title: '👥 Social Commands',
color: 0x8b5cf6,
commands: [
{ name: '/rep @user [reason]', desc: 'Give reputation to someone' },
{ name: '/hug @user', desc: 'Give someone a virtual hug' },
{ name: '/birthday set/view/upcoming', desc: 'Manage birthdays' },
{ name: '/remind set/list/cancel', desc: 'Set personal reminders' },
]
},
moderation: {
@ -166,28 +215,35 @@ function getCategoryEmbed(category) {
},
utility: {
title: '🔧 Utility Commands',
color: 0x8b5cf6,
color: 0x6366f1,
commands: [
{ name: '/translate [text] [to]', desc: 'Translate text to another language' },
{ name: '/define [word]', desc: 'Look up word definitions' },
{ name: '/math [expression]', desc: 'Calculate math expressions' },
{ name: '/color [hex/rgb]', desc: 'View color information' },
{ name: '/qr [text]', desc: 'Generate a QR code' },
{ name: '/userinfo [@user]', desc: 'View detailed user information' },
{ name: '/serverinfo', desc: 'View server statistics and info' },
{ name: '/serverinfo', desc: 'View server statistics' },
{ name: '/avatar [@user]', desc: 'Get a user\'s avatar' },
{ name: '/status', desc: 'View network status' },
{ name: '/status', desc: 'View bot status' },
]
},
admin: {
title: '⚙️ Admin Commands',
color: 0x6b7280,
commands: [
{ name: '/config', desc: 'View and edit server configuration' },
{ name: '/announce', desc: 'Send cross-server announcements' },
{ name: '/config', desc: 'View and edit server configuration (including mode)' },
{ name: '/starboard setup/disable/status', desc: 'Configure the starboard' },
{ name: '/announce', desc: 'Send announcements' },
{ name: '/embed', desc: 'Create custom embed messages' },
{ name: '/rolepanel', desc: 'Create role button panels' },
{ name: '/giveaway', desc: 'Create and manage giveaways' },
{ name: '/schedule', desc: 'Schedule messages for later' },
{ name: '/automod', desc: 'Configure auto-moderation' },
{ name: '/admin', desc: 'Bot administration commands' },
{ name: '/federation', desc: 'Manage cross-server role sync' },
{ name: '/ticket', desc: 'Manage support tickets' },
{ name: '/xp-settings', desc: 'Configure XP system' },
{ name: '/level-roles', desc: 'Set up level-up role rewards' },
{ name: '/quests-manage', desc: 'Manage quests' },
{ name: '/shop-manage', desc: 'Manage shop items' },
]
}
};

View file

@ -0,0 +1,47 @@
const { SlashCommandBuilder, EmbedBuilder } = require('discord.js');
const { getServerMode, getEmbedColor } = require('../utils/modeHelper');
const HUG_GIFS = [
'https://media.giphy.com/media/l2QDM9Jnim1YVILXa/giphy.gif',
'https://media.giphy.com/media/od5H3PmEG5EVq/giphy.gif',
'https://media.giphy.com/media/ZQN9jsRWp1M76/giphy.gif',
'https://media.giphy.com/media/lrr9rHuoJOE0w/giphy.gif',
'https://media.giphy.com/media/3M4NpbLCTxBqU/giphy.gif',
];
const HUG_MESSAGES = [
'{user} gives {target} a warm hug!',
'{user} wraps {target} in a cozy embrace!',
'{user} hugs {target} tightly!',
'{user} sends virtual hugs to {target}!',
'{user} gives {target} the biggest hug ever!',
];
module.exports = {
data: new SlashCommandBuilder()
.setName('hug')
.setDescription('Give someone a virtual hug')
.addUserOption(option =>
option.setName('user')
.setDescription('The user to hug')
.setRequired(true)
),
async execute(interaction, supabase, client) {
const targetUser = interaction.options.getUser('user');
const mode = await getServerMode(supabase, interaction.guildId);
const gif = HUG_GIFS[Math.floor(Math.random() * HUG_GIFS.length)];
const message = HUG_MESSAGES[Math.floor(Math.random() * HUG_MESSAGES.length)]
.replace('{user}', interaction.user.toString())
.replace('{target}', targetUser.toString());
const embed = new EmbedBuilder()
.setColor(0xFFB6C1)
.setDescription(`🤗 ${message}`)
.setImage(gif)
.setTimestamp();
await interaction.reply({ embeds: [embed] });
},
};

View file

@ -0,0 +1,59 @@
const { SlashCommandBuilder, EmbedBuilder } = require('discord.js');
const { getServerMode, getEmbedColor, EMBED_COLORS } = require('../utils/modeHelper');
module.exports = {
data: new SlashCommandBuilder()
.setName('inventory')
.setDescription('View your inventory')
.addUserOption(option =>
option.setName('user')
.setDescription('User to view inventory of')
.setRequired(false)
),
async execute(interaction, supabase, client) {
const targetUser = interaction.options.getUser('user') || interaction.user;
const mode = await getServerMode(supabase, interaction.guildId);
const guildId = interaction.guildId;
if (!supabase) {
return interaction.reply({ content: 'Inventory system unavailable.', ephemeral: true });
}
try {
const { data: items } = await supabase
.from('user_inventory')
.select('*, shop_items(*)')
.eq('guild_id', guildId)
.eq('user_id', targetUser.id);
if (!items || items.length === 0) {
const embed = new EmbedBuilder()
.setColor(getEmbedColor(mode))
.setTitle(`🎒 ${targetUser.username}'s Inventory`)
.setDescription('Inventory is empty. Buy items from the `/shop`!')
.setThumbnail(targetUser.displayAvatarURL({ size: 128 }))
.setTimestamp();
return interaction.reply({ embeds: [embed] });
}
const itemList = items.map(inv => {
const item = inv.shop_items;
if (!item) return null;
return `${item.emoji || '📦'} **${item.name}** x${inv.quantity}`;
}).filter(Boolean).join('\n');
const embed = new EmbedBuilder()
.setColor(getEmbedColor(mode))
.setTitle(`🎒 ${targetUser.username}'s Inventory`)
.setDescription(itemList || 'No items')
.setThumbnail(targetUser.displayAvatarURL({ size: 128 }))
.setFooter({ text: `${items.length} unique item(s)` })
.setTimestamp();
await interaction.reply({ embeds: [embed] });
} catch (e) {
await interaction.reply({ content: 'Failed to fetch inventory.', ephemeral: true });
}
},
};

View file

@ -1,21 +1,23 @@
const { SlashCommandBuilder, EmbedBuilder } = require("discord.js");
const { getServerMode, EMBED_COLORS } = require("../utils/modeHelper");
const { getStandaloneLeaderboard, calculateLevel } = require("../utils/standaloneXp");
module.exports = {
data: new SlashCommandBuilder()
.setName("leaderboard")
.setDescription("View the top AeThex contributors")
.setDescription("View the top contributors")
.addStringOption((option) =>
option
.setName("category")
.setDescription("Leaderboard category")
.setRequired(false)
.addChoices(
{ name: "XP Leaders (All-Time)", value: "xp" },
{ name: "📅 This Week", value: "weekly" },
{ name: "📆 This Month", value: "monthly" },
{ name: "🔥 Most Active (Posts)", value: "posts" },
{ name: "❤️ Most Liked", value: "likes" },
{ name: "🎨 Top Creators", value: "creators" }
{ name: "XP Leaders (All-Time)", value: "xp" },
{ name: "This Week", value: "weekly" },
{ name: "This Month", value: "monthly" },
{ name: "Most Active (Posts)", value: "posts" },
{ name: "Most Liked", value: "likes" },
{ name: "Top Creators", value: "creators" }
)
),
@ -26,7 +28,150 @@ module.exports = {
await interaction.deferReply();
try {
const mode = await getServerMode(supabase, interaction.guildId);
const category = interaction.options.getString("category") || "xp";
if (mode === 'standalone') {
return handleStandaloneLeaderboard(interaction, supabase, category);
} else {
return handleFederatedLeaderboard(interaction, supabase, category);
}
} catch (error) {
console.error("Leaderboard command error:", error);
const embed = new EmbedBuilder()
.setColor(0xff0000)
.setTitle("Error")
.setDescription("Failed to fetch leaderboard. Please try again.");
await interaction.editReply({ embeds: [embed] });
}
},
};
async function handleStandaloneLeaderboard(interaction, supabase, category) {
const guildId = interaction.guildId;
if (category !== 'xp' && category !== 'weekly' && category !== 'monthly') {
return interaction.editReply({
embeds: [
new EmbedBuilder()
.setColor(EMBED_COLORS.standalone)
.setTitle("🏠 Standalone Mode")
.setDescription("This leaderboard category is only available in Federation mode.\n\nIn Standalone mode, only XP, Weekly, and Monthly leaderboards are available.")
]
});
}
let leaderboardData = [];
let title = "";
let color = EMBED_COLORS.standalone;
if (category === 'xp') {
title = "Server XP Leaderboard";
const data = await getStandaloneLeaderboard(supabase, guildId, 10);
for (const entry of data) {
const level = calculateLevel(entry.xp || 0, 'normal');
let displayName = entry.username || 'Unknown';
try {
const member = await interaction.guild.members.fetch(entry.discord_id).catch(() => null);
if (member) displayName = member.displayName;
} catch (e) {}
leaderboardData.push({
name: displayName,
value: `Level ${level}${(entry.xp || 0).toLocaleString()} XP`,
xp: entry.xp || 0
});
}
} else if (category === 'weekly' || category === 'monthly') {
const periodType = category === 'weekly' ? 'week' : 'month';
title = category === 'weekly' ? 'Weekly XP Leaderboard' : 'Monthly XP Leaderboard';
const now = new Date();
let periodStart;
if (category === 'weekly') {
const dayOfWeek = now.getDay();
const diffToMonday = dayOfWeek === 0 ? 6 : dayOfWeek - 1;
periodStart = new Date(now);
periodStart.setDate(now.getDate() - diffToMonday);
periodStart.setHours(0, 0, 0, 0);
} else {
periodStart = new Date(now.getFullYear(), now.getMonth(), 1);
}
const periodStartStr = periodStart.toISOString().split('T')[0];
const { data: periodicData } = await supabase
.from("periodic_xp")
.select("discord_id, weekly_xp, monthly_xp")
.eq("guild_id", guildId)
.eq("period_type", periodType)
.eq("period_start", periodStartStr);
const aggregated = {};
for (const entry of periodicData || []) {
if (!aggregated[entry.discord_id]) {
aggregated[entry.discord_id] = 0;
}
aggregated[entry.discord_id] += category === 'weekly' ? (entry.weekly_xp || 0) : (entry.monthly_xp || 0);
}
const sortedUsers = Object.entries(aggregated)
.sort(([, a], [, b]) => b - a)
.slice(0, 10);
for (const [discordId, xp] of sortedUsers) {
let displayName = 'Unknown User';
try {
const member = await interaction.guild.members.fetch(discordId).catch(() => null);
if (member) displayName = member.displayName;
} catch (e) {}
leaderboardData.push({
name: displayName,
value: `${xp.toLocaleString()} XP`,
xp: xp
});
}
}
const medals = ['1.', '2.', '3.'];
const description = leaderboardData.length > 0
? leaderboardData
.map((user, index) => {
const medal = index < 3 ? medals[index] : `${index + 1}.`;
return `**${medal}** ${user.name}\n ${user.value}`;
})
.join("\n\n")
: "No data available yet. Be the first to contribute!";
const embed = new EmbedBuilder()
.setColor(color)
.setTitle(`🏠 ${title}`)
.setDescription(description)
.setThumbnail(interaction.guild.iconURL({ size: 128 }))
.setFooter({
text: `${interaction.guild.name} • Standalone Mode`,
iconURL: interaction.guild.iconURL({ size: 32 })
})
.setTimestamp();
if (leaderboardData.length > 0) {
embed.addFields({
name: 'Showing',
value: `Top ${leaderboardData.length} contributors`,
inline: true
});
}
await interaction.editReply({ embeds: [embed] });
}
async function handleFederatedLeaderboard(interaction, supabase, category) {
const guildId = interaction.guildId;
let leaderboardData = [];
@ -37,7 +182,7 @@ module.exports = {
if (category === "weekly") {
title = "Weekly XP Leaderboard";
emoji = "📅";
emoji = "";
color = 0x22c55e;
const now = new Date();
@ -52,7 +197,6 @@ module.exports = {
weekEnd.setDate(weekStart.getDate() + 6);
periodInfo = `${weekStart.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })} - ${weekEnd.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}`;
// Fetch weekly records using period_type
const { data: periodicData } = await supabase
.from("periodic_xp")
.select("discord_id, weekly_xp, weekly_messages")
@ -60,7 +204,6 @@ module.exports = {
.eq("period_type", "week")
.eq("period_start", weekStartStr);
// Aggregate per user (handles multiple records if they exist)
const aggregated = {};
for (const entry of periodicData || []) {
if (!aggregated[entry.discord_id]) {
@ -70,7 +213,6 @@ module.exports = {
aggregated[entry.discord_id].messages += entry.weekly_messages || 0;
}
// Sort and limit after aggregation
const sortedUsers = Object.entries(aggregated)
.sort(([, a], [, b]) => b.xp - a.xp)
.slice(0, 10);
@ -90,7 +232,7 @@ module.exports = {
}
} else if (category === "monthly") {
title = "Monthly XP Leaderboard";
emoji = "📆";
emoji = "";
color = 0x3b82f6;
const now = new Date();
@ -99,7 +241,6 @@ module.exports = {
periodInfo = monthStart.toLocaleDateString('en-US', { month: 'long', year: 'numeric' });
// Fetch monthly records using period_type
const { data: periodicData } = await supabase
.from("periodic_xp")
.select("discord_id, monthly_xp, monthly_messages")
@ -107,7 +248,6 @@ module.exports = {
.eq("period_type", "month")
.eq("period_start", monthStartStr);
// Aggregate all entries per user first
const aggregated = {};
for (const entry of periodicData || []) {
if (!aggregated[entry.discord_id]) {
@ -117,7 +257,6 @@ module.exports = {
aggregated[entry.discord_id].messages += entry.monthly_messages || 0;
}
// Sort and limit AFTER aggregation
const sortedUsers = Object.entries(aggregated)
.sort(([, a], [, b]) => b.xp - a.xp)
.slice(0, 10);
@ -137,7 +276,7 @@ module.exports = {
}
} else if (category === "xp") {
title = "XP Leaderboard (All-Time)";
emoji = "";
emoji = "";
color = 0xfbbf24;
const { data: profiles } = await supabase
@ -158,7 +297,7 @@ module.exports = {
}
} else if (category === "posts") {
title = "Most Active Posters";
emoji = "🔥";
emoji = "";
color = 0xef4444;
const { data: posts } = await supabase
@ -192,7 +331,7 @@ module.exports = {
}
} else if (category === "likes") {
title = "Most Liked Users";
emoji = "❤️";
emoji = "";
color = 0xec4899;
const { data: posts } = await supabase
@ -228,7 +367,7 @@ module.exports = {
}
} else if (category === "creators") {
title = "Top Creators";
emoji = "🎨";
emoji = "";
color = 0x8b5cf6;
const { data: creators } = await supabase
@ -246,8 +385,8 @@ module.exports = {
if (profile) {
const badges = [];
if (creator.verified) badges.push("");
if (creator.featured) badges.push("");
if (creator.verified) badges.push("Verified");
if (creator.featured) badges.push("Featured");
leaderboardData.push({
name: profile.full_name || profile.username || "Anonymous",
@ -258,31 +397,31 @@ module.exports = {
}
}
const medals = ['🥇', '🥈', '🥉'];
const medals = ['1.', '2.', '3.'];
const description = leaderboardData.length > 0
? leaderboardData
.map((user, index) => {
const medal = index < 3 ? medals[index] : `\`${index + 1}.\``;
return `${medal} **${user.name}**\n ${user.value}`;
const medal = index < 3 ? medals[index] : `${index + 1}.`;
return `**${medal}** ${user.name}\n ${user.value}`;
})
.join("\n\n")
: "No data available yet. Be the first to contribute!";
const embed = new EmbedBuilder()
.setColor(color)
.setTitle(`${emoji} ${title}`)
.setTitle(`🌐 ${title}`)
.setDescription(description)
.setThumbnail(interaction.guild.iconURL({ size: 128 }))
.setFooter({
text: `${interaction.guild.name}Updated in real-time`,
text: `${interaction.guild.name}Federation Mode`,
iconURL: interaction.guild.iconURL({ size: 32 })
})
.setTimestamp();
if (periodInfo) {
embed.addFields({
name: '📊 Period',
name: 'Period',
value: periodInfo,
inline: true
});
@ -290,7 +429,7 @@ module.exports = {
if (leaderboardData.length > 0) {
embed.addFields({
name: '👥 Showing',
name: 'Showing',
value: `Top ${leaderboardData.length} contributors`,
inline: true
});
@ -298,21 +437,11 @@ module.exports = {
if (category === "weekly" || category === "monthly") {
embed.addFields({
name: '💡 Tip',
name: 'Tip',
value: 'Leaderboards reset automatically at the start of each period!',
inline: false
});
}
await interaction.editReply({ embeds: [embed] });
} catch (error) {
console.error("Leaderboard command error:", error);
const embed = new EmbedBuilder()
.setColor(0xff0000)
.setTitle("❌ Error")
.setDescription("Failed to fetch leaderboard. Please try again.");
await interaction.editReply({ embeds: [embed] });
}
},
};
}

View file

@ -0,0 +1,67 @@
const { SlashCommandBuilder, EmbedBuilder } = require('discord.js');
const { getServerMode, getEmbedColor, EMBED_COLORS } = require('../utils/modeHelper');
function safeEval(expression) {
const sanitized = expression.replace(/[^0-9+\-*/().%^sqrt\s]/gi, '');
if (!sanitized || sanitized.trim() === '') {
return { error: 'Invalid expression' };
}
try {
let processed = sanitized
.replace(/\^/g, '**')
.replace(/sqrt\(([^)]+)\)/gi, 'Math.sqrt($1)');
const result = Function('"use strict"; return (' + processed + ')')();
if (typeof result !== 'number' || isNaN(result) || !isFinite(result)) {
return { error: 'Result is not a valid number' };
}
return { result };
} catch (e) {
return { error: 'Invalid expression' };
}
}
module.exports = {
data: new SlashCommandBuilder()
.setName('math')
.setDescription('Calculate a math expression')
.addStringOption(option =>
option.setName('expression')
.setDescription('The math expression (e.g., 2+2, sqrt(16), 5^2)')
.setRequired(true)
.setMaxLength(200)
),
async execute(interaction, supabase, client) {
const expression = interaction.options.getString('expression');
const mode = await getServerMode(supabase, interaction.guildId);
const { result, error } = safeEval(expression);
if (error) {
const embed = new EmbedBuilder()
.setColor(EMBED_COLORS.error)
.setTitle('🧮 Math Error')
.setDescription(error)
.setTimestamp();
return interaction.reply({ embeds: [embed], ephemeral: true });
}
const formattedResult = Number.isInteger(result) ? result.toString() : result.toFixed(6).replace(/\.?0+$/, '');
const embed = new EmbedBuilder()
.setColor(getEmbedColor(mode))
.setTitle('🧮 Calculator')
.addFields(
{ name: '📥 Expression', value: `\`${expression}\`` },
{ name: '📤 Result', value: `\`${formattedResult}\`` }
)
.setTimestamp();
await interaction.reply({ embeds: [embed] });
},
};

View file

@ -1,4 +1,6 @@
const { SlashCommandBuilder, EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } = require('discord.js');
const { getServerMode, EMBED_COLORS } = require('../utils/modeHelper');
const { getStandaloneXp, prestigeStandalone, calculateLevel } = require('../utils/standaloneXp');
module.exports = {
data: new SlashCommandBuilder()
@ -28,22 +30,70 @@ module.exports = {
}
const sub = interaction.options.getSubcommand();
const mode = await getServerMode(supabase, interaction.guildId);
if (sub === 'view') {
return viewPrestige(interaction, supabase);
return viewPrestige(interaction, supabase, mode);
} else if (sub === 'up') {
return prestigeUp(interaction, supabase, client);
return prestigeUp(interaction, supabase, client, mode);
} else if (sub === 'rewards') {
return viewRewards(interaction);
return viewRewards(interaction, mode);
}
},
};
async function viewPrestige(interaction, supabase) {
async function viewPrestige(interaction, supabase, mode) {
const target = interaction.options.getUser('user') || interaction.user;
await interaction.deferReply();
try {
if (mode === 'standalone') {
const data = await getStandaloneXp(supabase, target.id, interaction.guildId);
if (!data) {
return interaction.editReply({
embeds: [
new EmbedBuilder()
.setColor(EMBED_COLORS.standalone)
.setDescription(`${target.id === interaction.user.id ? 'You have' : `${target.tag} has`} no XP data yet.`)
]
});
}
const prestige = data.prestige_level || 0;
const currentXp = data.xp || 0;
const totalXpEarned = data.total_xp_earned || currentXp;
const level = calculateLevel(currentXp, 'normal');
const prestigeInfo = getPrestigeInfo(prestige);
const canPrestige = level >= 50;
const embed = new EmbedBuilder()
.setColor(prestigeInfo.color)
.setTitle(`${prestigeInfo.icon} ${target.tag}'s Prestige`)
.setThumbnail(target.displayAvatarURL({ size: 256 }))
.addFields(
{ name: 'Prestige Level', value: `**${prestigeInfo.name}** (${prestige})`, inline: true },
{ name: 'XP Bonus', value: `+${prestige * 5}%`, inline: true },
{ name: 'Current Level', value: `${level}`, inline: true },
{ name: 'Total XP Earned', value: totalXpEarned.toLocaleString(), inline: true },
{ name: 'Current XP', value: currentXp.toLocaleString(), inline: true },
{ name: 'Can Prestige?', value: canPrestige ? 'Yes (Level 50+)' : `Need Level 50 (${level}/50)`, inline: true }
)
.setFooter({ text: `🏠 Standalone Mode • ${interaction.guild.name}` })
.setTimestamp();
if (prestige > 0) {
embed.addFields({
name: 'Prestige Rewards Unlocked',
value: getUnlockedRewards(prestige).join('\n') || 'None yet',
inline: false
});
}
return interaction.editReply({ embeds: [embed] });
}
const { data: link } = await supabase
.from('discord_links')
.select('user_id')
@ -72,7 +122,6 @@ async function viewPrestige(interaction, supabase) {
const level = Math.floor(Math.sqrt(currentXp / 100));
const prestigeInfo = getPrestigeInfo(prestige);
const nextPrestigeReq = getPrestigeRequirement(prestige + 1);
const canPrestige = level >= 50;
const embed = new EmbedBuilder()
@ -85,14 +134,14 @@ async function viewPrestige(interaction, supabase) {
{ name: 'Current Level', value: `${level}`, inline: true },
{ name: 'Total XP Earned', value: totalXpEarned.toLocaleString(), inline: true },
{ name: 'Current XP', value: currentXp.toLocaleString(), inline: true },
{ name: 'Can Prestige?', value: canPrestige ? 'Yes (Level 50+)' : `Need Level 50 (${level}/50)`, inline: true }
{ name: 'Can Prestige?', value: canPrestige ? 'Yes (Level 50+)' : `Need Level 50 (${level}/50)`, inline: true }
)
.setFooter({ text: `Next prestige requirement: Level 50` })
.setFooter({ text: `🌐 Federation • Next prestige requirement: Level 50` })
.setTimestamp();
if (prestige > 0) {
embed.addFields({
name: '🏆 Prestige Rewards Unlocked',
name: 'Prestige Rewards Unlocked',
value: getUnlockedRewards(prestige).join('\n') || 'None yet',
inline: false
});
@ -105,10 +154,127 @@ async function viewPrestige(interaction, supabase) {
}
}
async function prestigeUp(interaction, supabase, client) {
async function prestigeUp(interaction, supabase, client, mode) {
await interaction.deferReply();
try {
if (mode === 'standalone') {
const data = await getStandaloneXp(supabase, interaction.user.id, interaction.guildId);
if (!data) {
return interaction.editReply({
embeds: [
new EmbedBuilder()
.setColor(EMBED_COLORS.error)
.setDescription('You have no XP data yet. Start chatting to earn XP!')
]
});
}
const currentXp = data.xp || 0;
const level = calculateLevel(currentXp, 'normal');
const prestige = data.prestige_level || 0;
if (level < 50) {
return interaction.editReply({
embeds: [
new EmbedBuilder()
.setColor(EMBED_COLORS.error)
.setTitle('Cannot Prestige')
.setDescription(`You need to reach **Level 50** to prestige.\nCurrent level: **${level}**/50`)
]
});
}
if (prestige >= 10) {
return interaction.editReply({
embeds: [
new EmbedBuilder()
.setColor(0xffd700)
.setTitle('Maximum Prestige!')
.setDescription('You have reached the maximum prestige level!')
]
});
}
const newPrestige = prestige + 1;
const newPrestigeInfo = getPrestigeInfo(newPrestige);
const confirmEmbed = new EmbedBuilder()
.setColor(EMBED_COLORS.warning)
.setTitle('Confirm Prestige')
.setDescription(`Are you sure you want to prestige?\n\n**What will happen:**\n• Your XP will reset to **0**\n• Your level will reset to **0**\n• You gain **Prestige ${newPrestige}** (${newPrestigeInfo.name})\n• You get a permanent **+${newPrestige * 5}%** XP bonus\n\n**Current Stats:**\n• Level: ${level}\n• XP: ${currentXp.toLocaleString()}`)
.setFooter({ text: 'This action cannot be undone!' });
const row = new ActionRowBuilder()
.addComponents(
new ButtonBuilder()
.setCustomId('prestige_confirm_standalone')
.setLabel('Prestige Up!')
.setStyle(ButtonStyle.Success),
new ButtonBuilder()
.setCustomId('prestige_cancel_standalone')
.setLabel('Cancel')
.setStyle(ButtonStyle.Secondary)
);
const response = await interaction.editReply({ embeds: [confirmEmbed], components: [row] });
try {
const confirmation = await response.awaitMessageComponent({
filter: i => i.user.id === interaction.user.id,
time: 60000
});
if (confirmation.customId === 'prestige_confirm_standalone') {
const result = await prestigeStandalone(supabase, interaction.user.id, interaction.guildId);
if (!result.success) {
return confirmation.update({
embeds: [
new EmbedBuilder()
.setColor(EMBED_COLORS.error)
.setDescription(result.message)
],
components: []
});
}
const successEmbed = new EmbedBuilder()
.setColor(newPrestigeInfo.color)
.setTitle(`${newPrestigeInfo.icon} Prestige ${result.newPrestige} Achieved!`)
.setDescription(`Congratulations! You are now **${newPrestigeInfo.name}**!`)
.addFields(
{ name: 'XP Bonus', value: `+${result.bonus}%`, inline: true }
)
.setThumbnail(interaction.user.displayAvatarURL({ size: 256 }))
.setFooter({ text: '🏠 Standalone Mode' })
.setTimestamp();
await confirmation.update({ embeds: [successEmbed], components: [] });
} else {
await confirmation.update({
embeds: [
new EmbedBuilder()
.setColor(EMBED_COLORS.standalone)
.setDescription('Prestige cancelled.')
],
components: []
});
}
} catch (e) {
await interaction.editReply({
embeds: [
new EmbedBuilder()
.setColor(EMBED_COLORS.standalone)
.setDescription('Prestige request timed out.')
],
components: []
});
}
return;
}
const { data: link } = await supabase
.from('discord_links')
.select('user_id')
@ -141,7 +307,7 @@ async function prestigeUp(interaction, supabase, client) {
embeds: [
new EmbedBuilder()
.setColor(0xff6b6b)
.setTitle('Cannot Prestige')
.setTitle('Cannot Prestige')
.setDescription(`You need to reach **Level 50** to prestige.\nCurrent level: **${level}**/50`)
]
});
@ -152,7 +318,7 @@ async function prestigeUp(interaction, supabase, client) {
embeds: [
new EmbedBuilder()
.setColor(0xffd700)
.setTitle('👑 Maximum Prestige!')
.setTitle('Maximum Prestige!')
.setDescription('You have reached the maximum prestige level! You are a true legend.')
]
});
@ -164,7 +330,7 @@ async function prestigeUp(interaction, supabase, client) {
const confirmEmbed = new EmbedBuilder()
.setColor(0xf59e0b)
.setTitle('⚠️ Confirm Prestige')
.setTitle('Confirm Prestige')
.setDescription(`Are you sure you want to prestige?\n\n**What will happen:**\n• Your XP will reset to **0**\n• Your level will reset to **0**\n• You gain **Prestige ${newPrestige}** (${newPrestigeInfo.name})\n• You get a permanent **+${xpBonus}%** XP bonus\n• You unlock new **prestige rewards**\n\n**Current Stats:**\n• Level: ${level}\n• XP: ${currentXp.toLocaleString()}`)
.setFooter({ text: 'This action cannot be undone!' });
@ -172,7 +338,7 @@ async function prestigeUp(interaction, supabase, client) {
.addComponents(
new ButtonBuilder()
.setCustomId('prestige_confirm')
.setLabel('Prestige Up!')
.setLabel('Prestige Up!')
.setStyle(ButtonStyle.Success),
new ButtonBuilder()
.setCustomId('prestige_cancel')
@ -218,6 +384,7 @@ async function prestigeUp(interaction, supabase, client) {
{ name: 'Total XP Earned (All Time)', value: totalEarned.toLocaleString(), inline: true }
)
.setThumbnail(interaction.user.displayAvatarURL({ size: 256 }))
.setFooter({ text: '🌐 Federation' })
.setTimestamp();
await confirmation.update({ embeds: [successEmbed], components: [] });
@ -247,24 +414,24 @@ async function prestigeUp(interaction, supabase, client) {
}
}
async function viewRewards(interaction) {
async function viewRewards(interaction, mode) {
const embed = new EmbedBuilder()
.setColor(0x7c3aed)
.setTitle('Prestige Rewards')
.setColor(mode === 'standalone' ? EMBED_COLORS.standalone : 0x7c3aed)
.setTitle('Prestige Rewards')
.setDescription('Each prestige level grants permanent rewards!\n\n**Requirements:** Level 50 to prestige')
.addFields(
{ name: 'Prestige 1 - Bronze', value: '+5% XP bonus\n🏷️ Bronze Prestige badge', inline: true },
{ name: 'Prestige 2 - Silver', value: '+10% XP bonus\n🏷️ Silver Prestige badge', inline: true },
{ name: 'Prestige 3 - Gold', value: '+15% XP bonus\n🏷️ Gold Prestige badge', inline: true },
{ name: '💎 Prestige 4 - Platinum', value: '+20% XP bonus\n🏷️ Platinum badge\n🎁 Bonus daily XP', inline: true },
{ name: '💎 Prestige 5 - Diamond', value: '+25% XP bonus\n🏷️ Diamond badge\n⏱️ Reduced cooldowns', inline: true },
{ name: '🔥 Prestige 6 - Master', value: '+30% XP bonus\n🏷️ Master badge\n🎯 XP milestone rewards', inline: true },
{ name: '🔥 Prestige 7 - Grandmaster', value: '+35% XP bonus\n🏷️ Grandmaster badge\n💫 Special profile effects', inline: true },
{ name: '👑 Prestige 8 - Champion', value: '+40% XP bonus\n🏷️ Champion badge\n🏆 Leaderboard priority', inline: true },
{ name: '👑 Prestige 9 - Legend', value: '+45% XP bonus\n🏷️ Legend badge\nLegendary profile aura', inline: true },
{ name: '🌟 Prestige 10 - Mythic', value: '+50% XP bonus\n🏷️ Mythic badge\n🌈 All prestige perks!', inline: true }
{ name: 'Prestige 1 - Bronze', value: '+5% XP bonus\nBronze Prestige badge', inline: true },
{ name: 'Prestige 2 - Silver', value: '+10% XP bonus\nSilver Prestige badge', inline: true },
{ name: 'Prestige 3 - Gold', value: '+15% XP bonus\nGold Prestige badge', inline: true },
{ name: 'Prestige 4 - Platinum', value: '+20% XP bonus\nPlatinum badge\nBonus daily XP', inline: true },
{ name: 'Prestige 5 - Diamond', value: '+25% XP bonus\nDiamond badge\nReduced cooldowns', inline: true },
{ name: 'Prestige 6 - Master', value: '+30% XP bonus\nMaster badge\nXP milestone rewards', inline: true },
{ name: 'Prestige 7 - Grandmaster', value: '+35% XP bonus\nGrandmaster badge\nSpecial profile effects', inline: true },
{ name: 'Prestige 8 - Champion', value: '+40% XP bonus\nChampion badge\nLeaderboard priority', inline: true },
{ name: 'Prestige 9 - Legend', value: '+45% XP bonus\nLegend badge\nLegendary profile aura', inline: true },
{ name: 'Prestige 10 - Mythic', value: '+50% XP bonus\nMythic badge\nAll prestige perks!', inline: true }
)
.setFooter({ text: 'Use /prestige up when you reach Level 50!' })
.setFooter({ text: `${mode === 'standalone' ? '🏠 Standalone' : '🌐 Federation'} • Use /prestige up when you reach Level 50!` })
.setTimestamp();
await interaction.reply({ embeds: [embed] });
@ -272,52 +439,48 @@ async function viewRewards(interaction) {
function getPrestigeInfo(level) {
const prestiges = [
{ name: 'Unprestiged', icon: '', color: 0x6b7280 },
{ name: 'Bronze', icon: '🥉', color: 0xcd7f32 },
{ name: 'Silver', icon: '🥈', color: 0xc0c0c0 },
{ name: 'Gold', icon: '🥇', color: 0xffd700 },
{ name: 'Platinum', icon: '💎', color: 0xe5e4e2 },
{ name: 'Diamond', icon: '💠', color: 0xb9f2ff },
{ name: 'Master', icon: '🔥', color: 0xff4500 },
{ name: 'Grandmaster', icon: '⚔️', color: 0x9400d3 },
{ name: 'Champion', icon: '👑', color: 0xffd700 },
{ name: 'Legend', icon: '🌟', color: 0xff69b4 },
{ name: 'Mythic', icon: '🌈', color: 0x7c3aed }
{ name: 'Unprestiged', icon: '', color: 0x6b7280 },
{ name: 'Bronze', icon: '', color: 0xcd7f32 },
{ name: 'Silver', icon: '', color: 0xc0c0c0 },
{ name: 'Gold', icon: '', color: 0xffd700 },
{ name: 'Platinum', icon: '', color: 0xe5e4e2 },
{ name: 'Diamond', icon: '', color: 0xb9f2ff },
{ name: 'Master', icon: '', color: 0xff4500 },
{ name: 'Grandmaster', icon: '', color: 0x9400d3 },
{ name: 'Champion', icon: '', color: 0xffd700 },
{ name: 'Legend', icon: '', color: 0xff69b4 },
{ name: 'Mythic', icon: '', color: 0x7c3aed }
];
return prestiges[Math.min(level, 10)] || prestiges[0];
}
function getPrestigeRequirement(level) {
return 50;
}
function getUnlockedRewards(prestige) {
const rewards = [];
if (prestige >= 1) rewards.push('🥉 Bronze Prestige badge');
if (prestige >= 2) rewards.push('🥈 Silver Prestige badge');
if (prestige >= 3) rewards.push('🥇 Gold Prestige badge');
if (prestige >= 4) rewards.push('💎 Platinum badge + Bonus daily XP');
if (prestige >= 5) rewards.push('💠 Diamond badge + Reduced cooldowns');
if (prestige >= 6) rewards.push('🔥 Master badge + XP milestones');
if (prestige >= 7) rewards.push('⚔️ Grandmaster badge + Profile effects');
if (prestige >= 8) rewards.push('👑 Champion badge + Leaderboard priority');
if (prestige >= 9) rewards.push('🌟 Legend badge + Legendary aura');
if (prestige >= 10) rewards.push('🌈 Mythic badge + All perks unlocked');
if (prestige >= 1) rewards.push('Bronze Prestige badge');
if (prestige >= 2) rewards.push('Silver Prestige badge');
if (prestige >= 3) rewards.push('Gold Prestige badge');
if (prestige >= 4) rewards.push('Platinum badge + Bonus daily XP');
if (prestige >= 5) rewards.push('Diamond badge + Reduced cooldowns');
if (prestige >= 6) rewards.push('Master badge + XP milestones');
if (prestige >= 7) rewards.push('Grandmaster badge + Profile effects');
if (prestige >= 8) rewards.push('Champion badge + Leaderboard priority');
if (prestige >= 9) rewards.push('Legend badge + Legendary aura');
if (prestige >= 10) rewards.push('Mythic badge + All perks unlocked');
return rewards;
}
function getNewRewards(prestige) {
const rewardMap = {
1: ['🥉 Bronze Prestige badge', '+5% XP bonus on all XP gains'],
2: ['🥈 Silver Prestige badge', '+10% XP bonus on all XP gains'],
3: ['🥇 Gold Prestige badge', '+15% XP bonus on all XP gains'],
4: ['💎 Platinum badge', '+20% XP bonus', '🎁 +25 bonus daily XP'],
5: ['💠 Diamond badge', '+25% XP bonus', '⏱️ 10% reduced XP cooldowns'],
6: ['🔥 Master badge', '+30% XP bonus', '🎯 XP milestone rewards'],
7: ['⚔️ Grandmaster badge', '+35% XP bonus', '💫 Special profile effects'],
8: ['👑 Champion badge', '+40% XP bonus', '🏆 Leaderboard priority display'],
9: ['🌟 Legend badge', '+45% XP bonus', 'Legendary profile aura'],
10: ['🌈 Mythic badge', '+50% XP bonus', '🌟 All prestige perks unlocked!']
1: ['Bronze Prestige badge', '+5% XP bonus on all XP gains'],
2: ['Silver Prestige badge', '+10% XP bonus on all XP gains'],
3: ['Gold Prestige badge', '+15% XP bonus on all XP gains'],
4: ['Platinum badge', '+20% XP bonus', '+25 bonus daily XP'],
5: ['Diamond badge', '+25% XP bonus', '10% reduced XP cooldowns'],
6: ['Master badge', '+30% XP bonus', 'XP milestone rewards'],
7: ['Grandmaster badge', '+35% XP bonus', 'Special profile effects'],
8: ['Champion badge', '+40% XP bonus', 'Leaderboard priority display'],
9: ['Legend badge', '+45% XP bonus', 'Legendary profile aura'],
10: ['Mythic badge', '+50% XP bonus', 'All prestige perks unlocked!']
};
return rewardMap[prestige] || [];
}

View file

@ -1,9 +1,11 @@
const { SlashCommandBuilder, EmbedBuilder } = require("discord.js");
const { getServerMode, EMBED_COLORS } = require("../utils/modeHelper");
const { getStandaloneXp, calculateLevel } = require("../utils/standaloneXp");
module.exports = {
data: new SlashCommandBuilder()
.setName("profile")
.setDescription("View your AeThex profile in Discord")
.setDescription("View your profile")
.addUserOption(option =>
option.setName('user')
.setDescription('User to view profile of')
@ -19,6 +21,88 @@ module.exports = {
const targetUser = interaction.options.getUser('user') || interaction.user;
try {
const mode = await getServerMode(supabase, interaction.guildId);
if (mode === 'standalone') {
return handleStandaloneProfile(interaction, supabase, targetUser);
} else {
return handleFederatedProfile(interaction, supabase, targetUser);
}
} catch (error) {
console.error("Profile command error:", error);
const embed = new EmbedBuilder()
.setColor(0xff0000)
.setTitle("Error")
.setDescription("Failed to fetch profile. Please try again.");
await interaction.editReply({ embeds: [embed] });
}
},
};
async function handleStandaloneProfile(interaction, supabase, targetUser) {
const data = await getStandaloneXp(supabase, targetUser.id, interaction.guildId);
if (!data) {
const embed = new EmbedBuilder()
.setColor(EMBED_COLORS.standalone)
.setTitle("No Profile Found")
.setThumbnail(targetUser.displayAvatarURL({ size: 256 }))
.setDescription(
targetUser.id === interaction.user.id
? "You don't have any XP yet. Start chatting to build your profile!"
: `${targetUser.tag} hasn't earned any XP yet in this server.`
);
return await interaction.editReply({ embeds: [embed] });
}
const xp = data.xp || 0;
const prestige = data.prestige_level || 0;
const totalXpEarned = data.total_xp_earned || xp;
const level = calculateLevel(xp, 'normal');
const dailyStreak = data.daily_streak || 0;
const currentLevelXp = level * level * 100;
const nextLevelXp = (level + 1) * (level + 1) * 100;
const progressXp = xp - currentLevelXp;
const neededXp = nextLevelXp - currentLevelXp;
const progressPercent = Math.min(100, Math.floor((progressXp / neededXp) * 100));
const progressBar = createProgressBar(progressPercent);
const prestigeInfo = getPrestigeInfo(prestige);
const { count: rankPosition } = await supabase
.from('guild_user_xp')
.select('*', { count: 'exact', head: true })
.eq('guild_id', interaction.guildId)
.gt('xp', xp);
const embed = new EmbedBuilder()
.setColor(EMBED_COLORS.standalone)
.setAuthor({
name: targetUser.tag,
iconURL: targetUser.displayAvatarURL({ size: 64 })
})
.setThumbnail(targetUser.displayAvatarURL({ size: 256 }))
.addFields(
{ name: "Username", value: `\`${data.username || targetUser.username}\``, inline: true },
{ name: "Server Rank", value: `#${(rankPosition || 0) + 1}`, inline: true },
{ name: "Daily Streak", value: `${dailyStreak} days`, inline: true },
{ name: `${prestigeInfo.icon || ''} Prestige`, value: prestige > 0 ? `**${prestigeInfo.name}** (P${prestige}) +${prestige * 5}% XP` : 'Not prestiged', inline: true },
{ name: `Level ${level}`, value: `${progressBar}\n\`${xp.toLocaleString()}\` / \`${nextLevelXp.toLocaleString()}\` XP`, inline: false },
{ name: "Total XP Earned", value: totalXpEarned.toLocaleString(), inline: true }
)
.setFooter({
text: `🏠 Standalone Mode • ${interaction.guild.name}`,
iconURL: interaction.guild.iconURL({ size: 32 })
})
.setTimestamp();
await interaction.editReply({ embeds: [embed] });
}
async function handleFederatedProfile(interaction, supabase, targetUser) {
const { data: link } = await supabase
.from("discord_links")
.select("user_id, primary_arm")
@ -28,7 +112,7 @@ module.exports = {
if (!link) {
const embed = new EmbedBuilder()
.setColor(0xff6b6b)
.setTitle("Not Linked")
.setTitle("Not Linked")
.setThumbnail(targetUser.displayAvatarURL({ size: 256 }))
.setDescription(
targetUser.id === interaction.user.id
@ -48,18 +132,18 @@ module.exports = {
if (!profile) {
const embed = new EmbedBuilder()
.setColor(0xff6b6b)
.setTitle("Profile Not Found")
.setTitle("Profile Not Found")
.setDescription("The AeThex profile could not be found.");
return await interaction.editReply({ embeds: [embed] });
}
const armEmojis = {
labs: "🧪",
gameforge: "🎮",
corp: "💼",
foundation: "🤝",
devlink: "💻",
labs: "",
gameforge: "",
corp: "",
foundation: "",
devlink: "",
};
const armColors = {
@ -87,7 +171,6 @@ module.exports = {
? badges.map(b => getBadgeEmoji(b)).join(' ')
: 'No badges yet';
// Validate avatar URL - must be http/https, not base64
let avatarUrl = targetUser.displayAvatarURL({ size: 256 });
if (profile.avatar_url && profile.avatar_url.startsWith('http')) {
avatarUrl = profile.avatar_url;
@ -102,43 +185,19 @@ module.exports = {
.setThumbnail(avatarUrl)
.setDescription(profile.bio || '*No bio set*')
.addFields(
{
name: "👤 Username",
value: `\`${profile.username || 'N/A'}\``,
inline: true,
},
{
name: `${armEmojis[link.primary_arm] || "⚔️"} Realm`,
value: capitalizeFirst(link.primary_arm) || "Not set",
inline: true,
},
{
name: "📊 Role",
value: formatRole(profile.user_type),
inline: true,
},
{
name: `${prestigeInfo.icon} Prestige`,
value: prestige > 0 ? `**${prestigeInfo.name}** (P${prestige}) +${prestige * 5}% XP` : 'Not prestiged',
inline: true,
},
{
name: `📈 Level ${level}`,
value: `${progressBar}\n\`${xp.toLocaleString()}\` / \`${nextLevelXp.toLocaleString()}\` XP`,
inline: false,
},
{
name: "🏆 Badges",
value: badgeDisplay,
inline: false,
}
{ name: "Username", value: `\`${profile.username || 'N/A'}\``, inline: true },
{ name: `${armEmojis[link.primary_arm] || ""} Realm`, value: capitalizeFirst(link.primary_arm) || "Not set", inline: true },
{ name: "Role", value: formatRole(profile.user_type), inline: true },
{ name: `${prestigeInfo.icon || ''} Prestige`, value: prestige > 0 ? `**${prestigeInfo.name}** (P${prestige}) +${prestige * 5}% XP` : 'Not prestiged', inline: true },
{ name: `Level ${level}`, value: `${progressBar}\n\`${xp.toLocaleString()}\` / \`${nextLevelXp.toLocaleString()}\` XP`, inline: false },
{ name: "Badges", value: badgeDisplay, inline: false }
)
.addFields({
name: "🔗 Links",
name: "Links",
value: `[View Full Profile](https://aethex.dev/creators/${profile.username}) • [AeThex Platform](https://aethex.dev)`,
})
.setFooter({
text: `AeThex | ${targetUser.tag}`,
text: `🌐 Federation • ${targetUser.tag}`,
iconURL: 'https://aethex.dev/favicon.ico'
})
.setTimestamp();
@ -148,17 +207,7 @@ module.exports = {
}
await interaction.editReply({ embeds: [embed] });
} catch (error) {
console.error("Profile command error:", error);
const embed = new EmbedBuilder()
.setColor(0xff0000)
.setTitle("❌ Error")
.setDescription("Failed to fetch profile. Please try again.");
await interaction.editReply({ embeds: [embed] });
}
},
};
}
function createProgressBar(percent) {
const filled = Math.floor(percent / 10);
@ -178,36 +227,36 @@ function formatRole(role) {
function getBadgeEmoji(badge) {
const badgeMap = {
'verified': '',
'founder': '👑',
'early_adopter': '🌟',
'contributor': '💎',
'creator': '🎨',
'developer': '💻',
'moderator': '🛡️',
'partner': '🤝',
'premium': '💫',
'top_poster': '📝',
'helpful': '❤️',
'bug_hunter': '🐛',
'event_winner': '🏆',
'verified': 'Verified',
'founder': 'Founder',
'early_adopter': 'Early Adopter',
'contributor': 'Contributor',
'creator': 'Creator',
'developer': 'Developer',
'moderator': 'Moderator',
'partner': 'Partner',
'premium': 'Premium',
'top_poster': 'Top Poster',
'helpful': 'Helpful',
'bug_hunter': 'Bug Hunter',
'event_winner': 'Event Winner',
};
return badgeMap[badge] || `[${badge}]`;
}
function getPrestigeInfo(level) {
const prestiges = [
{ name: 'Unprestiged', icon: '', color: 0x6b7280 },
{ name: 'Bronze', icon: '🥉', color: 0xcd7f32 },
{ name: 'Silver', icon: '🥈', color: 0xc0c0c0 },
{ name: 'Gold', icon: '🥇', color: 0xffd700 },
{ name: 'Platinum', icon: '💎', color: 0xe5e4e2 },
{ name: 'Diamond', icon: '💠', color: 0xb9f2ff },
{ name: 'Master', icon: '🔥', color: 0xff4500 },
{ name: 'Grandmaster', icon: '⚔️', color: 0x9400d3 },
{ name: 'Champion', icon: '👑', color: 0xffd700 },
{ name: 'Legend', icon: '🌟', color: 0xff69b4 },
{ name: 'Mythic', icon: '🌈', color: 0x7c3aed }
{ name: 'Unprestiged', icon: '', color: 0x6b7280 },
{ name: 'Bronze', icon: '', color: 0xcd7f32 },
{ name: 'Silver', icon: '', color: 0xc0c0c0 },
{ name: 'Gold', icon: '', color: 0xffd700 },
{ name: 'Platinum', icon: '', color: 0xe5e4e2 },
{ name: 'Diamond', icon: '', color: 0xb9f2ff },
{ name: 'Master', icon: '', color: 0xff4500 },
{ name: 'Grandmaster', icon: '', color: 0x9400d3 },
{ name: 'Champion', icon: '', color: 0xffd700 },
{ name: 'Legend', icon: '', color: 0xff69b4 },
{ name: 'Mythic', icon: '', color: 0x7c3aed }
];
return prestiges[Math.min(level, 10)] || prestiges[0];
}

40
aethex-bot/commands/qr.js Normal file
View file

@ -0,0 +1,40 @@
const { SlashCommandBuilder, EmbedBuilder } = require('discord.js');
const { getServerMode, getEmbedColor } = require('../utils/modeHelper');
module.exports = {
data: new SlashCommandBuilder()
.setName('qr')
.setDescription('Generate a QR code')
.addStringOption(option =>
option.setName('text')
.setDescription('The text or URL to encode')
.setRequired(true)
.setMaxLength(500)
)
.addIntegerOption(option =>
option.setName('size')
.setDescription('QR code size (default: 200)')
.setRequired(false)
.setMinValue(100)
.setMaxValue(500)
),
async execute(interaction, supabase, client) {
const text = interaction.options.getString('text');
const size = interaction.options.getInteger('size') || 200;
const mode = await getServerMode(supabase, interaction.guildId);
const encodedText = encodeURIComponent(text);
const qrUrl = `https://api.qrserver.com/v1/create-qr-code/?size=${size}x${size}&data=${encodedText}`;
const embed = new EmbedBuilder()
.setColor(getEmbedColor(mode))
.setTitle('📱 QR Code Generated')
.setDescription(`\`${text.length > 100 ? text.substring(0, 100) + '...' : text}\``)
.setImage(qrUrl)
.setFooter({ text: `Size: ${size}x${size}` })
.setTimestamp();
await interaction.reply({ embeds: [embed] });
},
};

View file

@ -1,9 +1,11 @@
const { SlashCommandBuilder, EmbedBuilder } = require('discord.js');
const { getServerMode, getEmbedColor, EMBED_COLORS } = require('../utils/modeHelper');
const { getStandaloneXp, calculateLevel } = require('../utils/standaloneXp');
module.exports = {
data: new SlashCommandBuilder()
.setName('rank')
.setDescription('View your unified level and XP across all platforms')
.setDescription('View your level and XP')
.addUserOption(option =>
option.setName('user')
.setDescription('User to check (defaults to yourself)')
@ -19,6 +21,72 @@ module.exports = {
await interaction.deferReply();
try {
const mode = await getServerMode(supabase, interaction.guildId);
if (mode === 'standalone') {
return handleStandaloneRank(interaction, supabase, target);
} else {
return handleFederatedRank(interaction, supabase, target);
}
} catch (error) {
console.error('Rank error:', error);
await interaction.editReply({ content: 'Failed to fetch rank data.' });
}
},
};
async function handleStandaloneRank(interaction, supabase, target) {
const data = await getStandaloneXp(supabase, target.id, interaction.guildId);
if (!data) {
return interaction.editReply({
embeds: [
new EmbedBuilder()
.setColor(EMBED_COLORS.standalone)
.setDescription(`${target.id === interaction.user.id ? 'You have' : `${target.tag} has`} no XP yet. Start chatting to earn XP!`)
]
});
}
const xp = data.xp || 0;
const prestige = data.prestige_level || 0;
const totalXpEarned = data.total_xp_earned || xp;
const level = calculateLevel(xp, 'normal');
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 prestigeInfo = getPrestigeInfo(prestige);
const { count: rankPosition } = await supabase
.from('guild_user_xp')
.select('*', { count: 'exact', head: true })
.eq('guild_id', interaction.guildId)
.gt('xp', xp);
const embed = new EmbedBuilder()
.setColor(prestigeInfo.color)
.setTitle(`${prestigeInfo.icon} ${target.tag}'s Rank`)
.setThumbnail(target.displayAvatarURL())
.addFields(
{ name: 'Prestige', value: prestige > 0 ? `**${prestigeInfo.name}** (P${prestige})` : 'Not prestiged', inline: true },
{ name: 'Level', value: `**${level}**`, inline: true },
{ name: 'Server Rank', value: `#${(rankPosition || 0) + 1}`, inline: true },
{ name: 'Current XP', value: `**${xp.toLocaleString()}**`, inline: true },
{ name: 'XP Bonus', value: prestige > 0 ? `+${prestige * 5}%` : 'None', inline: true },
{ name: 'Total XP Earned', value: totalXpEarned.toLocaleString(), inline: true },
{ name: 'Progress to Next Level', value: `${progressBar}\n${progress.toLocaleString()} / ${needed.toLocaleString()} XP (${progressPercent}%)` }
)
.setFooter({ text: `🏠 Standalone Mode • ${interaction.guild.name}` })
.setTimestamp();
await interaction.editReply({ embeds: [embed] });
}
async function handleFederatedRank(interaction, supabase, target) {
const { data: link } = await supabase
.from('discord_links')
.select('user_id, primary_arm')
@ -59,7 +127,6 @@ module.exports = {
.select('*', { count: 'exact', head: true })
.gt('xp', xp);
// Validate avatar URL - must be http/https, not base64
let avatarUrl = target.displayAvatarURL();
if (profile?.avatar_url && profile.avatar_url.startsWith('http')) {
avatarUrl = profile.avatar_url;
@ -72,24 +139,18 @@ module.exports = {
.addFields(
{ name: 'Prestige', value: prestige > 0 ? `**${prestigeInfo.name}** (P${prestige})` : 'Not prestiged', inline: true },
{ name: 'Level', value: `**${level}**`, inline: true },
{ name: 'Rank', value: `#${(rankPosition || 0) + 1}`, inline: true },
{ name: 'Global Rank', value: `#${(rankPosition || 0) + 1}`, inline: true },
{ name: 'Current XP', value: `**${xp.toLocaleString()}**`, inline: true },
{ name: 'XP Bonus', value: prestige > 0 ? `+${prestige * 5}%` : 'None', inline: true },
{ name: 'Total XP Earned', value: totalXpEarned.toLocaleString(), 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: prestige >= 1 ? `Prestige ${prestige} | XP earned across Discord & AeThex platforms` : 'XP earned across Discord & AeThex platforms' })
.setFooter({ text: prestige >= 1 ? `🌐 Federation • Prestige ${prestige} | XP earned across Discord & AeThex platforms` : '🌐 Federation • 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);

View file

@ -1,5 +1,6 @@
const { SlashCommandBuilder, EmbedBuilder } = require("discord.js");
const { assignRoleByArm, getUserArm } = require("../utils/roleManager");
const { getServerMode, EMBED_COLORS } = require("../utils/modeHelper");
module.exports = {
data: new SlashCommandBuilder()
@ -12,6 +13,17 @@ module.exports = {
if (!supabase) {
return interaction.reply({ content: "This feature requires Supabase to be configured.", ephemeral: true });
}
const mode = await getServerMode(supabase, interaction.guildId);
if (mode === 'standalone') {
const embed = new EmbedBuilder()
.setColor(EMBED_COLORS.standalone)
.setTitle('🏠 Standalone Mode')
.setDescription('Role syncing is disabled in standalone mode.\n\nThis server operates independently without AeThex role federation.\n\nUse `/config mode` to switch to federated mode.');
return interaction.reply({ embeds: [embed], ephemeral: true });
}
await interaction.deferReply({ ephemeral: true });
try {

View file

@ -0,0 +1,193 @@
const { SlashCommandBuilder, EmbedBuilder } = require('discord.js');
const { getServerMode, getEmbedColor, EMBED_COLORS } = require('../utils/modeHelper');
const activeReminders = new Map();
function parseTime(timeStr) {
const regex = /^(\d+)(s|m|h|d)$/i;
const match = timeStr.match(regex);
if (!match) return null;
const value = parseInt(match[1]);
const unit = match[2].toLowerCase();
const multipliers = {
's': 1000,
'm': 60 * 1000,
'h': 60 * 60 * 1000,
'd': 24 * 60 * 60 * 1000
};
return value * multipliers[unit];
}
module.exports = {
data: new SlashCommandBuilder()
.setName('remind')
.setDescription('Set a personal reminder')
.addSubcommand(subcommand =>
subcommand
.setName('set')
.setDescription('Set a new reminder')
.addStringOption(option =>
option.setName('time')
.setDescription('When to remind (e.g., 30m, 2h, 1d)')
.setRequired(true)
)
.addStringOption(option =>
option.setName('message')
.setDescription('What to remind you about')
.setRequired(true)
.setMaxLength(500)
)
)
.addSubcommand(subcommand =>
subcommand
.setName('list')
.setDescription('List your active reminders')
)
.addSubcommand(subcommand =>
subcommand
.setName('cancel')
.setDescription('Cancel a reminder')
.addStringOption(option =>
option.setName('id')
.setDescription('The reminder ID to cancel')
.setRequired(true)
)
),
async execute(interaction, supabase, client) {
const subcommand = interaction.options.getSubcommand();
const mode = await getServerMode(supabase, interaction.guildId);
const userId = interaction.user.id;
if (subcommand === 'set') {
const timeStr = interaction.options.getString('time');
const message = interaction.options.getString('message');
const duration = parseTime(timeStr);
if (!duration) {
return interaction.reply({
content: 'Invalid time format. Use formats like: 30s, 5m, 2h, 1d',
ephemeral: true
});
}
if (duration < 10000) {
return interaction.reply({
content: 'Reminder must be at least 10 seconds in the future.',
ephemeral: true
});
}
if (duration > 30 * 24 * 60 * 60 * 1000) {
return interaction.reply({
content: 'Reminder cannot be more than 30 days in the future.',
ephemeral: true
});
}
const reminderId = `${Date.now()}-${Math.random().toString(36).substr(2, 6)}`;
const reminderTime = Date.now() + duration;
const timeout = setTimeout(async () => {
try {
const user = await client.users.fetch(userId);
const reminderEmbed = new EmbedBuilder()
.setColor(EMBED_COLORS.warning)
.setTitle('⏰ Reminder!')
.setDescription(message)
.addFields({
name: '📍 Set',
value: `<t:${Math.floor((reminderTime - duration) / 1000)}:R>`
})
.setTimestamp();
await user.send({ embeds: [reminderEmbed] });
} catch (e) {
try {
const channel = await client.channels.fetch(interaction.channelId);
await channel.send({
content: `<@${userId}> ⏰ **Reminder:** ${message}`
});
} catch (e2) {}
}
const userReminders = activeReminders.get(userId) || [];
activeReminders.set(userId, userReminders.filter(r => r.id !== reminderId));
}, duration);
const userReminders = activeReminders.get(userId) || [];
userReminders.push({
id: reminderId,
message,
time: reminderTime,
timeout,
channelId: interaction.channelId
});
activeReminders.set(userId, userReminders);
const embed = new EmbedBuilder()
.setColor(EMBED_COLORS.success)
.setTitle('⏰ Reminder Set!')
.setDescription(`I'll remind you: **${message}**`)
.addFields(
{ name: '⏱️ In', value: timeStr, inline: true },
{ name: '📅 At', value: `<t:${Math.floor(reminderTime / 1000)}:f>`, inline: true },
{ name: '🆔 ID', value: `\`${reminderId}\``, inline: true }
)
.setTimestamp();
await interaction.reply({ embeds: [embed], ephemeral: true });
}
else if (subcommand === 'list') {
const userReminders = activeReminders.get(userId) || [];
if (userReminders.length === 0) {
return interaction.reply({
content: 'You have no active reminders.',
ephemeral: true
});
}
const list = userReminders.map((r, i) => {
return `${i + 1}. **${r.message.substring(0, 50)}${r.message.length > 50 ? '...' : ''}**\n ⏰ <t:${Math.floor(r.time / 1000)}:R> | ID: \`${r.id}\``;
}).join('\n\n');
const embed = new EmbedBuilder()
.setColor(getEmbedColor(mode))
.setTitle('⏰ Your Reminders')
.setDescription(list)
.setFooter({ text: `${userReminders.length} active reminder(s)` })
.setTimestamp();
await interaction.reply({ embeds: [embed], ephemeral: true });
}
else if (subcommand === 'cancel') {
const reminderId = interaction.options.getString('id');
const userReminders = activeReminders.get(userId) || [];
const reminder = userReminders.find(r => r.id === reminderId);
if (!reminder) {
return interaction.reply({
content: 'Reminder not found. Use `/remind list` to see your reminders.',
ephemeral: true
});
}
clearTimeout(reminder.timeout);
activeReminders.set(userId, userReminders.filter(r => r.id !== reminderId));
const embed = new EmbedBuilder()
.setColor(EMBED_COLORS.success)
.setTitle('🗑️ Reminder Cancelled')
.setDescription(`Cancelled reminder: **${reminder.message.substring(0, 100)}**`)
.setTimestamp();
await interaction.reply({ embeds: [embed], ephemeral: true });
}
},
};

View file

@ -0,0 +1,83 @@
const { SlashCommandBuilder, EmbedBuilder } = require('discord.js');
const { getServerMode, getEmbedColor, EMBED_COLORS } = require('../utils/modeHelper');
const repCooldowns = new Map();
const REP_COOLDOWN = 12 * 60 * 60 * 1000;
module.exports = {
data: new SlashCommandBuilder()
.setName('rep')
.setDescription('Give a reputation point to someone')
.addUserOption(option =>
option.setName('user')
.setDescription('The user to give rep to')
.setRequired(true)
)
.addStringOption(option =>
option.setName('reason')
.setDescription('Why are you giving them rep?')
.setRequired(false)
.setMaxLength(200)
),
async execute(interaction, supabase, client) {
const targetUser = interaction.options.getUser('user');
const reason = interaction.options.getString('reason');
const userId = interaction.user.id;
const guildId = interaction.guildId;
const mode = await getServerMode(supabase, interaction.guildId);
if (targetUser.id === userId) {
return interaction.reply({
content: "You can't give rep to yourself!",
ephemeral: true
});
}
if (targetUser.bot) {
return interaction.reply({
content: "You can't give rep to bots!",
ephemeral: true
});
}
const cooldownKey = `${guildId}-${userId}`;
const lastRep = repCooldowns.get(cooldownKey);
if (lastRep && Date.now() - lastRep < REP_COOLDOWN) {
const remaining = REP_COOLDOWN - (Date.now() - lastRep);
const hours = Math.floor(remaining / (60 * 60 * 1000));
const minutes = Math.floor((remaining % (60 * 60 * 1000)) / (60 * 1000));
return interaction.reply({
content: `You can give rep again in ${hours}h ${minutes}m.`,
ephemeral: true
});
}
repCooldowns.set(cooldownKey, Date.now());
if (supabase) {
try {
await supabase.from('reputation').insert({
guild_id: guildId,
giver_id: userId,
receiver_id: targetUser.id,
reason: reason,
created_at: new Date().toISOString()
});
} catch (e) {}
}
const embed = new EmbedBuilder()
.setColor(EMBED_COLORS.success)
.setTitle('⭐ Reputation Given!')
.setDescription(`${interaction.user} gave a rep point to ${targetUser}!`)
.setThumbnail(targetUser.displayAvatarURL({ size: 128 }))
.setTimestamp();
if (reason) {
embed.addFields({ name: '💬 Reason', value: reason });
}
await interaction.reply({ embeds: [embed] });
},
};

View file

@ -0,0 +1,98 @@
const { SlashCommandBuilder, EmbedBuilder } = require('discord.js');
const { getServerMode, getEmbedColor } = require('../utils/modeHelper');
module.exports = {
data: new SlashCommandBuilder()
.setName('roll')
.setDescription('Roll dice')
.addStringOption(option =>
option.setName('dice')
.setDescription('Dice notation (e.g., 2d6, d20, 3d8+5)')
.setRequired(false)
)
.addIntegerOption(option =>
option.setName('sides')
.setDescription('Number of sides (default: 6)')
.setRequired(false)
.setMinValue(2)
.setMaxValue(1000)
)
.addIntegerOption(option =>
option.setName('count')
.setDescription('Number of dice to roll (default: 1)')
.setRequired(false)
.setMinValue(1)
.setMaxValue(100)
),
async execute(interaction, supabase, client) {
const diceNotation = interaction.options.getString('dice');
let sides = interaction.options.getInteger('sides') || 6;
let count = interaction.options.getInteger('count') || 1;
let modifier = 0;
if (diceNotation) {
const match = diceNotation.match(/^(\d*)d(\d+)([+-]\d+)?$/i);
if (match) {
count = match[1] ? parseInt(match[1]) : 1;
sides = parseInt(match[2]);
modifier = match[3] ? parseInt(match[3]) : 0;
if (count > 100) count = 100;
if (sides > 1000) sides = 1000;
} else {
return interaction.reply({
content: 'Invalid dice notation. Use format like `2d6`, `d20`, or `3d8+5`',
ephemeral: true
});
}
}
const rolls = [];
for (let i = 0; i < count; i++) {
rolls.push(Math.floor(Math.random() * sides) + 1);
}
const sum = rolls.reduce((a, b) => a + b, 0);
const total = sum + modifier;
const mode = await getServerMode(supabase, interaction.guildId);
const embed = new EmbedBuilder()
.setColor(getEmbedColor(mode))
.setTitle('🎲 Dice Roll')
.setTimestamp()
.setFooter({ text: `Rolled by ${interaction.user.username}` });
if (count === 1 && modifier === 0) {
embed.setDescription(`You rolled a **${total}**!`);
} else {
const rollsDisplay = rolls.length <= 20
? rolls.map(r => `\`${r}\``).join(' + ')
: `${rolls.slice(0, 20).map(r => `\`${r}\``).join(' + ')} ... (+${rolls.length - 20} more)`;
let formula = `${count}d${sides}`;
if (modifier > 0) formula += `+${modifier}`;
else if (modifier < 0) formula += modifier;
embed.addFields(
{ name: '🎯 Formula', value: formula, inline: true },
{ name: '📊 Total', value: `**${total}**`, inline: true }
);
if (rolls.length <= 20) {
embed.addFields({ name: '🎲 Rolls', value: rollsDisplay });
}
if (modifier !== 0) {
embed.addFields({
name: ' Calculation',
value: `${sum} ${modifier >= 0 ? '+' : ''}${modifier} = **${total}**`,
inline: true
});
}
}
await interaction.reply({ embeds: [embed] });
},
};

View file

@ -5,6 +5,7 @@ const {
ActionRowBuilder,
} = require("discord.js");
const { assignRoleByArm } = require("../utils/roleManager");
const { getServerMode, EMBED_COLORS } = require("../utils/modeHelper");
const REALMS = [
{ value: "labs", label: "🧪 Labs", description: "Research & Development" },
@ -35,6 +36,17 @@ module.exports = {
if (!supabase) {
return interaction.reply({ content: "This feature requires Supabase to be configured.", ephemeral: true });
}
const mode = await getServerMode(supabase, interaction.guildId);
if (mode === 'standalone') {
const embed = new EmbedBuilder()
.setColor(EMBED_COLORS.standalone)
.setTitle('🏠 Standalone Mode')
.setDescription('Realm selection is disabled in standalone mode.\n\nThis server operates independently without AeThex realm affiliation.\n\nUse `/config mode` to switch to federated mode.');
return interaction.reply({ embeds: [embed], ephemeral: true });
}
await interaction.deferReply({ ephemeral: true });
try {

View file

@ -0,0 +1,127 @@
const { SlashCommandBuilder, EmbedBuilder } = require('discord.js');
const { getServerMode, getEmbedColor, EMBED_COLORS } = require('../utils/modeHelper');
const { updateStandaloneXp } = require('../utils/standaloneXp');
const SYMBOLS = ['🍒', '🍋', '🍊', '🍇', '⭐', '💎', '7⃣'];
const WEIGHTS = [30, 25, 20, 15, 7, 2, 1];
const PAYOUTS = {
'🍒🍒🍒': 5,
'🍋🍋🍋': 10,
'🍊🍊🍊': 15,
'🍇🍇🍇': 20,
'⭐⭐⭐': 50,
'💎💎💎': 100,
'7⃣7⃣7⃣': 250,
};
const slotsCooldowns = new Map();
const COOLDOWN = 30000;
function spin() {
const totalWeight = WEIGHTS.reduce((a, b) => a + b, 0);
const random = Math.random() * totalWeight;
let cumulativeWeight = 0;
for (let i = 0; i < SYMBOLS.length; i++) {
cumulativeWeight += WEIGHTS[i];
if (random < cumulativeWeight) {
return SYMBOLS[i];
}
}
return SYMBOLS[0];
}
module.exports = {
data: new SlashCommandBuilder()
.setName('slots')
.setDescription('Try your luck at the slot machine!')
.addIntegerOption(option =>
option.setName('bet')
.setDescription('XP to bet (10-50)')
.setRequired(false)
.setMinValue(10)
.setMaxValue(50)
),
async execute(interaction, supabase, client) {
const bet = interaction.options.getInteger('bet') || 10;
const userId = interaction.user.id;
const guildId = interaction.guildId;
const mode = await getServerMode(supabase, interaction.guildId);
const cooldownKey = `${guildId}-${userId}`;
const lastSpin = slotsCooldowns.get(cooldownKey);
if (lastSpin && Date.now() - lastSpin < COOLDOWN) {
const remaining = Math.ceil((COOLDOWN - (Date.now() - lastSpin)) / 1000);
return interaction.reply({
content: `You can spin again in ${remaining} seconds.`,
ephemeral: true
});
}
slotsCooldowns.set(cooldownKey, Date.now());
const slot1 = spin();
const slot2 = spin();
const slot3 = spin();
const result = `${slot1}${slot2}${slot3}`;
const payout = PAYOUTS[result];
let netGain = 0;
let resultMessage = '';
let color = getEmbedColor(mode);
if (payout) {
netGain = bet * payout;
resultMessage = `🎉 JACKPOT! You won **${netGain} XP**!`;
color = EMBED_COLORS.success;
} else if (slot1 === slot2 || slot2 === slot3 || slot1 === slot3) {
netGain = Math.floor(bet * 0.5);
resultMessage = `Nice! Two matching! You won **${netGain} XP**!`;
color = 0xF59E0B;
} else {
netGain = -bet;
resultMessage = `No luck this time. You lost **${bet} XP**.`;
color = EMBED_COLORS.error;
}
if (netGain !== 0) {
if (mode === 'standalone') {
await updateStandaloneXp(supabase, userId, guildId, netGain, interaction.user.username);
} else if (supabase) {
try {
const { data: profile } = await supabase
.from('user_profiles')
.select('xp')
.eq('discord_id', userId)
.maybeSingle();
if (profile) {
const newXp = Math.max(0, (profile.xp || 0) + netGain);
await supabase
.from('user_profiles')
.update({ xp: newXp })
.eq('discord_id', userId);
}
} catch (e) {}
}
}
const embed = new EmbedBuilder()
.setColor(color)
.setTitle('🎰 Slot Machine')
.setDescription(`
${slot1} ${slot2} ${slot3}
${resultMessage}
`)
.addFields({ name: '💰 Bet', value: `${bet} XP`, inline: true })
.setFooter({ text: `${interaction.user.username}` })
.setTimestamp();
await interaction.reply({ embeds: [embed] });
},
};

View file

@ -0,0 +1,113 @@
const { SlashCommandBuilder, EmbedBuilder, PermissionFlagsBits, ChannelType } = require('discord.js');
const { EMBED_COLORS } = require('../utils/modeHelper');
module.exports = {
data: new SlashCommandBuilder()
.setName('starboard')
.setDescription('Configure the starboard system')
.setDefaultMemberPermissions(PermissionFlagsBits.ManageGuild)
.addSubcommand(subcommand =>
subcommand
.setName('setup')
.setDescription('Set up or update the starboard channel')
.addChannelOption(option =>
option.setName('channel')
.setDescription('The starboard channel')
.setRequired(true)
.addChannelTypes(ChannelType.GuildText)
)
.addIntegerOption(option =>
option.setName('threshold')
.setDescription('Minimum stars needed (default: 3)')
.setRequired(false)
.setMinValue(1)
.setMaxValue(50)
)
)
.addSubcommand(subcommand =>
subcommand
.setName('disable')
.setDescription('Disable the starboard')
)
.addSubcommand(subcommand =>
subcommand
.setName('status')
.setDescription('View current starboard settings')
),
async execute(interaction, supabase, client) {
const subcommand = interaction.options.getSubcommand();
const guildId = interaction.guildId;
if (!supabase) {
return interaction.reply({ content: 'Starboard system unavailable.', ephemeral: true });
}
if (subcommand === 'setup') {
const channel = interaction.options.getChannel('channel');
const threshold = interaction.options.getInteger('threshold') || 3;
await supabase.from('starboard_config').upsert({
guild_id: guildId,
channel_id: channel.id,
threshold: threshold,
enabled: true,
updated_at: new Date().toISOString()
}, { onConflict: 'guild_id' });
const embed = new EmbedBuilder()
.setColor(EMBED_COLORS.success)
.setTitle('⭐ Starboard Configured!')
.addFields(
{ name: '📍 Channel', value: `${channel}`, inline: true },
{ name: '🎯 Threshold', value: `${threshold} stars`, inline: true }
)
.setDescription('Messages that receive enough ⭐ reactions will be posted to the starboard!')
.setTimestamp();
await interaction.reply({ embeds: [embed] });
}
else if (subcommand === 'disable') {
await supabase
.from('starboard_config')
.update({ enabled: false })
.eq('guild_id', guildId);
const embed = new EmbedBuilder()
.setColor(EMBED_COLORS.warning)
.setTitle('⭐ Starboard Disabled')
.setDescription('The starboard has been disabled. Use `/starboard setup` to re-enable.')
.setTimestamp();
await interaction.reply({ embeds: [embed] });
}
else if (subcommand === 'status') {
const { data } = await supabase
.from('starboard_config')
.select('*')
.eq('guild_id', guildId)
.maybeSingle();
if (!data) {
return interaction.reply({
content: 'Starboard is not set up. Use `/starboard setup` to configure it.',
ephemeral: true
});
}
const embed = new EmbedBuilder()
.setColor(data.enabled ? EMBED_COLORS.success : EMBED_COLORS.warning)
.setTitle('⭐ Starboard Status')
.addFields(
{ name: '📊 Status', value: data.enabled ? '✅ Enabled' : '❌ Disabled', inline: true },
{ name: '📍 Channel', value: `<#${data.channel_id}>`, inline: true },
{ name: '🎯 Threshold', value: `${data.threshold || 3} stars`, inline: true }
)
.setTimestamp();
await interaction.reply({ embeds: [embed] });
}
},
};

View file

@ -1,4 +1,5 @@
const { SlashCommandBuilder, EmbedBuilder } = require('discord.js');
const { getServerMode, getEmbedColor, getModeDisplayName, getModeEmoji } = require('../utils/modeHelper');
module.exports = {
data: new SlashCommandBuilder()
@ -13,8 +14,29 @@ module.exports = {
const minutes = Math.floor((uptime % 3600) / 60);
const seconds = uptime % 60;
const serverMode = supabase ? await getServerMode(supabase, interaction.guildId) : 'standalone';
const embedColor = getEmbedColor(serverMode);
let federatedCount = 0;
let standaloneCount = 0;
if (supabase) {
try {
const { data: configs } = await supabase
.from('server_config')
.select('mode');
if (configs) {
federatedCount = configs.filter(c => c.mode === 'federated' || !c.mode).length;
standaloneCount = configs.filter(c => c.mode === 'standalone').length;
}
} catch (e) {
federatedCount = guildCount;
}
}
const realmStatus = [];
const REALM_GUILDS = client.REALM_GUILDS;
const REALM_GUILDS = client.REALM_GUILDS || {};
for (const [realm, guildId] of Object.entries(REALM_GUILDS)) {
if (!guildId) {
@ -37,19 +59,34 @@ module.exports = {
}));
const embed = new EmbedBuilder()
.setColor(0x7c3aed)
.setTitle('AeThex Network Status')
.setDescription('Current status of the AeThex Federation')
.setColor(embedColor)
.setTitle('⚔️ Warden Status')
.setDescription(`${getModeEmoji(serverMode)} This server is running in **${getModeDisplayName(serverMode)}** mode`)
.addFields(
{ name: 'Total Servers', value: `${guildCount}`, inline: true },
{ name: 'Total Members', value: `${memberCount.toLocaleString()}`, inline: true },
{ name: 'Uptime', value: `${hours}h ${minutes}m ${seconds}s`, inline: true },
...realmFields,
{ name: 'Sentinel Status', value: client.heatMap.size > 0 ? `⚠️ Monitoring ${client.heatMap.size} user(s)` : '🛡️ All Clear', inline: false },
{ name: 'Active Tickets', value: `${client.activeTickets.size}`, inline: true },
{ name: 'Federation Mappings', value: `${client.federationMappings.size}`, inline: true }
)
.setFooter({ text: 'AeThex Unified Bot' })
{ name: 'Federated Servers', value: `🌐 ${federatedCount}`, inline: true },
{ name: 'Standalone Servers', value: `🏠 ${standaloneCount}`, inline: true },
{ name: '\u200b', value: '\u200b', inline: true }
);
if (serverMode === 'federated' && realmFields.length > 0) {
embed.addFields(
{ name: '── Federation Realms ──', value: '\u200b', inline: false },
...realmFields
);
}
embed.addFields(
{ name: '── Security ──', value: '\u200b', inline: false },
{ name: 'Sentinel Status', value: client.heatMap?.size > 0 ? `⚠️ Monitoring ${client.heatMap.size} user(s)` : '🛡️ All Clear', inline: true },
{ name: 'Active Tickets', value: `${client.activeTickets?.size || 0}`, inline: true },
{ name: 'Federation Mappings', value: `${client.federationMappings?.size || 0}`, inline: true }
);
embed
.setFooter({ text: `Warden • ${getModeDisplayName(serverMode)} Mode` })
.setTimestamp();
await interaction.reply({ embeds: [embed] });

View file

@ -0,0 +1,218 @@
const { SlashCommandBuilder, EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } = require('discord.js');
const { getServerMode, getEmbedColor, EMBED_COLORS } = require('../utils/modeHelper');
const activeTrades = new Map();
module.exports = {
data: new SlashCommandBuilder()
.setName('trade')
.setDescription('Trade items with another user')
.addUserOption(option =>
option.setName('user')
.setDescription('User to trade with')
.setRequired(true)
)
.addStringOption(option =>
option.setName('offer')
.setDescription('What you\'re offering (item name)')
.setRequired(true)
)
.addStringOption(option =>
option.setName('request')
.setDescription('What you want in return (item name)')
.setRequired(true)
),
async execute(interaction, supabase, client) {
const partner = interaction.options.getUser('user');
const offer = interaction.options.getString('offer');
const request = interaction.options.getString('request');
const initiator = interaction.user;
const mode = await getServerMode(supabase, interaction.guildId);
const guildId = interaction.guildId;
if (partner.id === initiator.id) {
return interaction.reply({ content: "You can't trade with yourself!", ephemeral: true });
}
if (partner.bot) {
return interaction.reply({ content: "You can't trade with bots!", ephemeral: true });
}
if (!supabase) {
return interaction.reply({ content: 'Trade system unavailable.', ephemeral: true });
}
const tradeKey = `${guildId}-${initiator.id}`;
if (activeTrades.has(tradeKey)) {
return interaction.reply({ content: 'You already have an active trade!', ephemeral: true });
}
const { data: initiatorItem } = await supabase
.from('user_inventory')
.select('*, shop_items(*)')
.eq('guild_id', guildId)
.eq('user_id', initiator.id)
.ilike('shop_items.name', `%${offer}%`)
.gt('quantity', 0)
.maybeSingle();
if (!initiatorItem) {
return interaction.reply({
content: `You don't have "${offer}" in your inventory!`,
ephemeral: true
});
}
const { data: partnerItem } = await supabase
.from('user_inventory')
.select('*, shop_items(*)')
.eq('guild_id', guildId)
.eq('user_id', partner.id)
.ilike('shop_items.name', `%${request}%`)
.gt('quantity', 0)
.maybeSingle();
if (!partnerItem) {
return interaction.reply({
content: `${partner.username} doesn't have "${request}" in their inventory!`,
ephemeral: true
});
}
activeTrades.set(tradeKey, {
partner: partner.id,
offer: initiatorItem,
request: partnerItem
});
const embed = new EmbedBuilder()
.setColor(getEmbedColor(mode))
.setTitle('🔄 Trade Request')
.setDescription(`${initiator} wants to trade with ${partner}!`)
.addFields(
{
name: `${initiator.username} Offers`,
value: `${initiatorItem.shop_items?.emoji || '📦'} ${initiatorItem.shop_items?.name}`,
inline: true
},
{
name: `${partner.username} Offers`,
value: `${partnerItem.shop_items?.emoji || '📦'} ${partnerItem.shop_items?.name}`,
inline: true
}
)
.setFooter({ text: 'Trade expires in 60 seconds' })
.setTimestamp();
const row = new ActionRowBuilder()
.addComponents(
new ButtonBuilder()
.setCustomId(`trade_accept_${initiator.id}`)
.setLabel('Accept')
.setStyle(ButtonStyle.Success),
new ButtonBuilder()
.setCustomId(`trade_decline_${initiator.id}`)
.setLabel('Decline')
.setStyle(ButtonStyle.Danger)
);
const message = await interaction.reply({
content: `${partner}`,
embeds: [embed],
components: [row],
fetchReply: true
});
const collector = message.createMessageComponentCollector({
filter: i => i.user.id === partner.id && i.customId.includes(initiator.id),
time: 60000,
max: 1
});
collector.on('collect', async (i) => {
const tradeData = activeTrades.get(tradeKey);
activeTrades.delete(tradeKey);
if (!tradeData) return;
if (i.customId.startsWith('trade_decline')) {
const declineEmbed = new EmbedBuilder()
.setColor(EMBED_COLORS.error)
.setTitle('🔄 Trade Declined')
.setDescription(`${partner} declined the trade.`)
.setTimestamp();
await i.update({ embeds: [declineEmbed], components: [] });
return;
}
try {
await supabase.rpc('execute_trade', {
p_guild_id: guildId,
p_user1_id: initiator.id,
p_user2_id: partner.id,
p_item1_id: tradeData.offer.item_id,
p_item2_id: tradeData.request.item_id
});
const successEmbed = new EmbedBuilder()
.setColor(EMBED_COLORS.success)
.setTitle('🔄 Trade Complete!')
.setDescription(`${initiator} and ${partner} successfully traded items!`)
.addFields(
{ name: `${initiator.username} received`, value: `${partnerItem.shop_items?.emoji || '📦'} ${partnerItem.shop_items?.name}`, inline: true },
{ name: `${partner.username} received`, value: `${initiatorItem.shop_items?.emoji || '📦'} ${initiatorItem.shop_items?.name}`, inline: true }
)
.setTimestamp();
await i.update({ embeds: [successEmbed], components: [] });
} catch (e) {
await supabase
.from('user_inventory')
.update({ quantity: tradeData.offer.quantity - 1 })
.eq('id', tradeData.offer.id);
await supabase
.from('user_inventory')
.update({ quantity: tradeData.request.quantity - 1 })
.eq('id', tradeData.request.id);
await supabase.from('user_inventory').upsert({
guild_id: guildId,
user_id: partner.id,
item_id: tradeData.offer.item_id,
quantity: 1
}, { onConflict: 'guild_id,user_id,item_id' });
await supabase.from('user_inventory').upsert({
guild_id: guildId,
user_id: initiator.id,
item_id: tradeData.request.item_id,
quantity: 1
}, { onConflict: 'guild_id,user_id,item_id' });
const successEmbed = new EmbedBuilder()
.setColor(EMBED_COLORS.success)
.setTitle('🔄 Trade Complete!')
.setDescription(`${initiator} and ${partner} successfully traded items!`)
.setTimestamp();
await i.update({ embeds: [successEmbed], components: [] });
}
});
collector.on('end', async (collected) => {
if (collected.size === 0) {
activeTrades.delete(tradeKey);
const timeoutEmbed = new EmbedBuilder()
.setColor(EMBED_COLORS.warning)
.setTitle('🔄 Trade Expired')
.setDescription(`${partner} didn't respond in time.`)
.setTimestamp();
await interaction.editReply({ embeds: [timeoutEmbed], components: [] });
}
});
},
};

View file

@ -0,0 +1,93 @@
const { SlashCommandBuilder, EmbedBuilder } = require('discord.js');
const { getServerMode, getEmbedColor, EMBED_COLORS } = require('../utils/modeHelper');
const LANGUAGES = {
'en': 'English',
'es': 'Spanish',
'fr': 'French',
'de': 'German',
'it': 'Italian',
'pt': 'Portuguese',
'ru': 'Russian',
'ja': 'Japanese',
'ko': 'Korean',
'zh': 'Chinese',
'ar': 'Arabic',
'hi': 'Hindi',
'nl': 'Dutch',
'pl': 'Polish',
'tr': 'Turkish',
'vi': 'Vietnamese',
'th': 'Thai',
'sv': 'Swedish',
'da': 'Danish',
'fi': 'Finnish'
};
module.exports = {
data: new SlashCommandBuilder()
.setName('translate')
.setDescription('Translate text to another language')
.addStringOption(option =>
option.setName('text')
.setDescription('The text to translate')
.setRequired(true)
.setMaxLength(500)
)
.addStringOption(option =>
option.setName('to')
.setDescription('Target language')
.setRequired(true)
.addChoices(
{ name: 'English', value: 'en' },
{ name: 'Spanish', value: 'es' },
{ name: 'French', value: 'fr' },
{ name: 'German', value: 'de' },
{ name: 'Italian', value: 'it' },
{ name: 'Portuguese', value: 'pt' },
{ name: 'Russian', value: 'ru' },
{ name: 'Japanese', value: 'ja' },
{ name: 'Korean', value: 'ko' },
{ name: 'Chinese', value: 'zh' }
)
),
async execute(interaction, supabase, client) {
const text = interaction.options.getString('text');
const targetLang = interaction.options.getString('to');
const mode = await getServerMode(supabase, interaction.guildId);
await interaction.deferReply();
try {
const response = await fetch(`https://api.mymemory.translated.net/get?q=${encodeURIComponent(text)}&langpair=auto|${targetLang}`);
const data = await response.json();
if (data.responseStatus !== 200 || !data.responseData?.translatedText) {
throw new Error('Translation failed');
}
const translated = data.responseData.translatedText;
const detectedLang = data.responseData.detectedLanguage || 'auto';
const embed = new EmbedBuilder()
.setColor(getEmbedColor(mode))
.setTitle('🌐 Translation')
.addFields(
{ name: '📥 Original', value: text.substring(0, 1000) },
{ name: `📤 ${LANGUAGES[targetLang] || targetLang}`, value: translated.substring(0, 1000) }
)
.setFooter({ text: `Translated to ${LANGUAGES[targetLang]}` })
.setTimestamp();
await interaction.editReply({ embeds: [embed] });
} catch (e) {
const embed = new EmbedBuilder()
.setColor(EMBED_COLORS.error)
.setTitle('🌐 Translation Error')
.setDescription('Failed to translate text. Please try again.')
.setTimestamp();
await interaction.editReply({ embeds: [embed] });
}
},
};

View file

@ -0,0 +1,283 @@
const { SlashCommandBuilder, EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } = require('discord.js');
const { getServerMode, getEmbedColor, EMBED_COLORS } = require('../utils/modeHelper');
const { updateStandaloneXp } = require('../utils/standaloneXp');
const TRIVIA_QUESTIONS = [
{
question: "What is the capital of Japan?",
answers: ["Tokyo", "Osaka", "Kyoto", "Hiroshima"],
correct: 0,
category: "Geography"
},
{
question: "Which planet is known as the Red Planet?",
answers: ["Venus", "Mars", "Jupiter", "Saturn"],
correct: 1,
category: "Science"
},
{
question: "What year did World War II end?",
answers: ["1943", "1944", "1945", "1946"],
correct: 2,
category: "History"
},
{
question: "What is the largest mammal on Earth?",
answers: ["African Elephant", "Blue Whale", "Giraffe", "Hippopotamus"],
correct: 1,
category: "Science"
},
{
question: "Who painted the Mona Lisa?",
answers: ["Michelangelo", "Leonardo da Vinci", "Raphael", "Donatello"],
correct: 1,
category: "Art"
},
{
question: "What is the chemical symbol for gold?",
answers: ["Go", "Gd", "Au", "Ag"],
correct: 2,
category: "Science"
},
{
question: "Which programming language was created by Brendan Eich?",
answers: ["Python", "Java", "JavaScript", "Ruby"],
correct: 2,
category: "Technology"
},
{
question: "What is the smallest country in the world?",
answers: ["Monaco", "Vatican City", "San Marino", "Liechtenstein"],
correct: 1,
category: "Geography"
},
{
question: "How many bones are in the adult human body?",
answers: ["186", "206", "226", "246"],
correct: 1,
category: "Science"
},
{
question: "What year was the first iPhone released?",
answers: ["2005", "2006", "2007", "2008"],
correct: 2,
category: "Technology"
},
{
question: "Which element has the atomic number 1?",
answers: ["Helium", "Hydrogen", "Oxygen", "Carbon"],
correct: 1,
category: "Science"
},
{
question: "What is the capital of Australia?",
answers: ["Sydney", "Melbourne", "Canberra", "Perth"],
correct: 2,
category: "Geography"
},
{
question: "Who wrote 'Romeo and Juliet'?",
answers: ["Charles Dickens", "William Shakespeare", "Jane Austen", "Mark Twain"],
correct: 1,
category: "Literature"
},
{
question: "What is the speed of light in km/s (approximately)?",
answers: ["150,000", "200,000", "300,000", "400,000"],
correct: 2,
category: "Science"
},
{
question: "Which company created Discord?",
answers: ["Hammer & Chisel", "Meta", "Microsoft", "Google"],
correct: 0,
category: "Technology"
}
];
const activeTrivia = new Map();
module.exports = {
data: new SlashCommandBuilder()
.setName('trivia')
.setDescription('Answer trivia questions to earn XP!')
.addStringOption(option =>
option.setName('category')
.setDescription('Choose a category (optional)')
.setRequired(false)
.addChoices(
{ name: 'All', value: 'all' },
{ name: 'Science', value: 'Science' },
{ name: 'Geography', value: 'Geography' },
{ name: 'History', value: 'History' },
{ name: 'Technology', value: 'Technology' },
{ name: 'Art', value: 'Art' },
{ name: 'Literature', value: 'Literature' }
)
),
async execute(interaction, supabase, client) {
const category = interaction.options.getString('category') || 'all';
const mode = await getServerMode(supabase, interaction.guildId);
const userId = interaction.user.id;
const guildId = interaction.guildId;
const activeKey = `${guildId}-${userId}`;
if (activeTrivia.has(activeKey)) {
return interaction.reply({
content: 'You already have an active trivia question! Answer it first.',
ephemeral: true
});
}
let questions = TRIVIA_QUESTIONS;
if (category !== 'all') {
questions = TRIVIA_QUESTIONS.filter(q => q.category === category);
}
if (questions.length === 0) {
return interaction.reply({
content: 'No questions available for that category.',
ephemeral: true
});
}
const question = questions[Math.floor(Math.random() * questions.length)];
const shuffledIndexes = [0, 1, 2, 3].sort(() => Math.random() - 0.5);
const shuffledAnswers = shuffledIndexes.map(i => question.answers[i]);
const correctIndex = shuffledIndexes.indexOf(question.correct);
activeTrivia.set(activeKey, {
correctIndex,
question: question.question,
startTime: Date.now()
});
const embed = new EmbedBuilder()
.setColor(getEmbedColor(mode))
.setTitle('🧠 Trivia Time!')
.setDescription(`**${question.question}**`)
.addFields(
{ name: '📚 Category', value: question.category, inline: true },
{ name: '⏱️ Time Limit', value: '30 seconds', inline: true },
{ name: '🎁 Reward', value: '25-50 XP', inline: true }
)
.setFooter({ text: 'Click a button to answer!' })
.setTimestamp();
const row = new ActionRowBuilder()
.addComponents(
new ButtonBuilder()
.setCustomId(`trivia_0_${interaction.user.id}`)
.setLabel(shuffledAnswers[0])
.setStyle(ButtonStyle.Primary),
new ButtonBuilder()
.setCustomId(`trivia_1_${interaction.user.id}`)
.setLabel(shuffledAnswers[1])
.setStyle(ButtonStyle.Primary),
new ButtonBuilder()
.setCustomId(`trivia_2_${interaction.user.id}`)
.setLabel(shuffledAnswers[2])
.setStyle(ButtonStyle.Primary),
new ButtonBuilder()
.setCustomId(`trivia_3_${interaction.user.id}`)
.setLabel(shuffledAnswers[3])
.setStyle(ButtonStyle.Primary)
);
const message = await interaction.reply({ embeds: [embed], components: [row], fetchReply: true });
const collector = message.createMessageComponentCollector({
filter: i => i.customId.startsWith('trivia_') && i.customId.endsWith(`_${interaction.user.id}`),
time: 30000,
max: 1
});
collector.on('collect', async (i) => {
const triviaData = activeTrivia.get(activeKey);
if (!triviaData) return;
const selectedIndex = parseInt(i.customId.split('_')[1]);
const isCorrect = selectedIndex === triviaData.correctIndex;
const timeTaken = (Date.now() - triviaData.startTime) / 1000;
activeTrivia.delete(activeKey);
let xpReward = 0;
if (isCorrect) {
xpReward = timeTaken < 5 ? 50 : timeTaken < 15 ? 35 : 25;
if (mode === 'standalone') {
await updateStandaloneXp(supabase, userId, guildId, xpReward, interaction.user.username);
} else if (supabase) {
try {
const { data: profile } = await supabase
.from('user_profiles')
.select('xp')
.eq('discord_id', userId)
.maybeSingle();
if (profile) {
await supabase
.from('user_profiles')
.update({ xp: (profile.xp || 0) + xpReward })
.eq('discord_id', userId);
}
} catch (e) {}
}
}
const resultEmbed = new EmbedBuilder()
.setColor(isCorrect ? EMBED_COLORS.success : EMBED_COLORS.error)
.setTitle(isCorrect ? '✅ Correct!' : '❌ Incorrect!')
.setDescription(isCorrect
? `Great job! You answered in ${timeTaken.toFixed(1)} seconds.`
: `The correct answer was: **${question.answers[question.correct]}**`
)
.addFields(
{ name: '❓ Question', value: question.question }
)
.setTimestamp();
if (isCorrect) {
resultEmbed.addFields({ name: '🎁 XP Earned', value: `+${xpReward} XP`, inline: true });
}
const disabledRow = new ActionRowBuilder()
.addComponents(
...row.components.map((btn, idx) =>
ButtonBuilder.from(btn)
.setStyle(idx === triviaData.correctIndex ? ButtonStyle.Success :
(idx === selectedIndex && !isCorrect) ? ButtonStyle.Danger :
ButtonStyle.Secondary)
.setDisabled(true)
)
);
await i.update({ embeds: [resultEmbed], components: [disabledRow] });
});
collector.on('end', async (collected) => {
if (collected.size === 0) {
activeTrivia.delete(activeKey);
const timeoutEmbed = new EmbedBuilder()
.setColor(EMBED_COLORS.warning)
.setTitle('⏰ Time\'s Up!')
.setDescription(`You didn't answer in time. The correct answer was: **${question.answers[question.correct]}**`)
.setTimestamp();
const disabledRow = new ActionRowBuilder()
.addComponents(
...row.components.map(btn =>
ButtonBuilder.from(btn)
.setStyle(ButtonStyle.Secondary)
.setDisabled(true)
)
);
await interaction.editReply({ embeds: [timeoutEmbed], components: [disabledRow] });
}
});
},
};

View file

@ -0,0 +1,76 @@
const { SlashCommandBuilder, EmbedBuilder } = require('discord.js');
const { getServerMode, getEmbedColor, EMBED_COLORS } = require('../utils/modeHelper');
const { updateStandaloneXp } = require('../utils/standaloneXp');
const JOBS = [
{ name: 'Developer', emoji: '💻', minXp: 15, maxXp: 35, message: 'You wrote some clean code' },
{ name: 'Designer', emoji: '🎨', minXp: 12, maxXp: 30, message: 'You created a beautiful design' },
{ name: 'Writer', emoji: '✍️', minXp: 10, maxXp: 25, message: 'You wrote an engaging article' },
{ name: 'Streamer', emoji: '📺', minXp: 18, maxXp: 40, message: 'Your stream was a hit' },
{ name: 'Chef', emoji: '👨‍🍳', minXp: 8, maxXp: 22, message: 'You cooked a delicious meal' },
{ name: 'Teacher', emoji: '👨‍🏫', minXp: 12, maxXp: 28, message: 'You helped students learn' },
{ name: 'Artist', emoji: '🖼️', minXp: 14, maxXp: 32, message: 'You created a masterpiece' },
{ name: 'Musician', emoji: '🎵', minXp: 10, maxXp: 26, message: 'You performed an amazing song' },
{ name: 'Photographer', emoji: '📷', minXp: 11, maxXp: 24, message: 'You captured a perfect shot' },
{ name: 'YouTuber', emoji: '🎬', minXp: 16, maxXp: 38, message: 'Your video went viral' },
];
const workCooldowns = new Map();
const COOLDOWN = 60 * 60 * 1000;
module.exports = {
data: new SlashCommandBuilder()
.setName('work')
.setDescription('Work to earn some XP!'),
async execute(interaction, supabase, client) {
const userId = interaction.user.id;
const guildId = interaction.guildId;
const mode = await getServerMode(supabase, interaction.guildId);
const cooldownKey = `${guildId}-${userId}`;
const lastWork = workCooldowns.get(cooldownKey);
if (lastWork && Date.now() - lastWork < COOLDOWN) {
const remaining = COOLDOWN - (Date.now() - lastWork);
const minutes = Math.floor(remaining / 60000);
const seconds = Math.floor((remaining % 60000) / 1000);
return interaction.reply({
content: `You're tired! You can work again in ${minutes}m ${seconds}s.`,
ephemeral: true
});
}
workCooldowns.set(cooldownKey, Date.now());
const job = JOBS[Math.floor(Math.random() * JOBS.length)];
const xpEarned = Math.floor(Math.random() * (job.maxXp - job.minXp + 1)) + job.minXp;
if (mode === 'standalone') {
await updateStandaloneXp(supabase, userId, guildId, xpEarned, interaction.user.username);
} else if (supabase) {
try {
const { data: profile } = await supabase
.from('user_profiles')
.select('xp')
.eq('discord_id', userId)
.maybeSingle();
if (profile) {
await supabase
.from('user_profiles')
.update({ xp: (profile.xp || 0) + xpEarned })
.eq('discord_id', userId);
}
} catch (e) {}
}
const embed = new EmbedBuilder()
.setColor(EMBED_COLORS.success)
.setTitle(`${job.emoji} ${job.name}`)
.setDescription(`${job.message} and earned **${xpEarned} XP**!`)
.setFooter({ text: `${interaction.user.username} | Work again in 1 hour` })
.setTimestamp();
await interaction.reply({ embeds: [embed] });
},
};

View file

@ -0,0 +1,183 @@
const { EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle, PermissionFlagsBits } = require('discord.js');
const { setServerMode, EMBED_COLORS } = require('../utils/modeHelper');
module.exports = {
name: 'guildCreate',
async execute(guild, client, supabase) {
if (!supabase) return;
const { data: existingConfig } = await supabase
.from('server_config')
.select('mode')
.eq('guild_id', guild.id)
.maybeSingle();
if (existingConfig?.mode) {
return;
}
let targetChannel = null;
if (guild.systemChannel && guild.systemChannel.permissionsFor(guild.members.me)?.has(PermissionFlagsBits.SendMessages)) {
targetChannel = guild.systemChannel;
}
if (!targetChannel) {
const channels = guild.channels.cache
.filter(c =>
c.type === 0 &&
c.permissionsFor(guild.members.me)?.has(PermissionFlagsBits.SendMessages)
)
.sort((a, b) => a.position - b.position);
targetChannel = channels.first();
}
if (!targetChannel) {
try {
const owner = await guild.fetchOwner();
await sendSetupDM(owner, guild, client, supabase);
} catch (e) {
console.log(`[Setup] Could not send setup prompt to ${guild.name}`);
}
return;
}
await sendSetupEmbed(targetChannel, guild, client, supabase);
}
};
async function sendSetupEmbed(channel, guild, client, supabase) {
const embed = new EmbedBuilder()
.setColor(EMBED_COLORS.federated)
.setTitle('⚔️ Welcome to Warden!')
.setDescription(
`Thanks for adding **Warden** to **${guild.name}**!\n\n` +
`Choose how you want to run Warden:`
)
.addFields(
{
name: '🌐 Join the Federation',
value:
'• Unified XP across all AeThex servers\n' +
'• Cross-server profiles and leaderboards\n' +
'• Realm selection and role sync\n' +
'• Part of the AeThex ecosystem',
inline: true
},
{
name: '🏠 Run Standalone',
value:
'• Isolated XP system for this server only\n' +
'• Local leaderboards and profiles\n' +
'• Full moderation and anti-nuke\n' +
'• Independent from other servers',
inline: true
}
)
.setFooter({ text: 'You can change this later with /config mode' })
.setTimestamp();
const row = new ActionRowBuilder()
.addComponents(
new ButtonBuilder()
.setCustomId('setup_federated')
.setLabel('Join Federation')
.setStyle(ButtonStyle.Primary)
.setEmoji('🌐'),
new ButtonBuilder()
.setCustomId('setup_standalone')
.setLabel('Run Standalone')
.setStyle(ButtonStyle.Secondary)
.setEmoji('🏠')
);
try {
const message = await channel.send({ embeds: [embed], components: [row] });
const collector = message.createMessageComponentCollector({
filter: (i) => {
return i.member.permissions.has(PermissionFlagsBits.Administrator) ||
i.member.id === guild.ownerId;
},
time: 86400000
});
collector.on('collect', async (interaction) => {
const mode = interaction.customId === 'setup_federated' ? 'federated' : 'standalone';
const success = await setServerMode(supabase, guild.id, mode);
if (success) {
const confirmEmbed = new EmbedBuilder()
.setColor(mode === 'federated' ? EMBED_COLORS.federated : EMBED_COLORS.standalone)
.setTitle(mode === 'federated' ? '🌐 Federation Mode Activated!' : '🏠 Standalone Mode Activated!')
.setDescription(
mode === 'federated'
? `**${guild.name}** is now part of the AeThex Federation!\n\n` +
'• XP earned here counts globally\n' +
'• Use `/verify` to link your AeThex account\n' +
'• Use `/set-realm` to choose your realm\n' +
'• Use `/help` to see all commands'
: `**${guild.name}** is now running in Standalone mode!\n\n` +
'• XP is tracked locally for this server\n' +
'• Use `/rank` to check your server level\n' +
'• Use `/leaderboard` to see top members\n' +
'• Use `/help` to see all commands'
)
.setFooter({ text: 'Change mode anytime with /config mode' })
.setTimestamp();
await interaction.update({ embeds: [confirmEmbed], components: [] });
collector.stop();
} else {
await interaction.reply({
content: 'Failed to set mode. Please try `/config mode` later.',
ephemeral: true
});
}
});
collector.on('end', async (collected, reason) => {
if (reason === 'time' && collected.size === 0) {
await setServerMode(supabase, guild.id, 'federated');
const timeoutEmbed = new EmbedBuilder()
.setColor(EMBED_COLORS.federated)
.setTitle('🌐 Federation Mode (Default)')
.setDescription(
`No selection was made, so **${guild.name}** has been set to Federation mode by default.\n\n` +
'Use `/config mode` to change this anytime.'
)
.setTimestamp();
await message.edit({ embeds: [timeoutEmbed], components: [] }).catch(() => {});
}
});
} catch (e) {
console.error('[Setup] Error sending setup embed:', e.message);
}
}
async function sendSetupDM(owner, guild, client, supabase) {
const embed = new EmbedBuilder()
.setColor(EMBED_COLORS.federated)
.setTitle('⚔️ Warden Setup Required')
.setDescription(
`Thanks for adding **Warden** to **${guild.name}**!\n\n` +
`I couldn't find a channel to send the setup prompt. Please use \`/config mode\` in your server to choose:\n\n` +
'**🌐 Federation** - Join the AeThex ecosystem with unified XP\n' +
'**🏠 Standalone** - Independent XP for your server only\n\n' +
'Until you choose, Federation mode is enabled by default.'
)
.setTimestamp();
try {
await owner.send({ embeds: [embed] });
await setServerMode(supabase, guild.id, 'federated');
} catch (e) {
await setServerMode(supabase, guild.id, 'federated');
}
}

View file

@ -162,10 +162,17 @@ async function syncMessageToFeed(message) {
module.exports = {
name: "messageCreate",
async execute(message, client) {
if (!supabase) return;
if (message.author.bot) return;
try {
const afkCommand = require('../commands/afk');
if (afkCommand && afkCommand.checkAfk) {
afkCommand.checkAfk(message);
}
} catch (e) {}
if (!supabase) return;
if (!message.content && message.attachments.size === 0) return;
if (!FEED_CHANNEL_ID) {

View file

@ -0,0 +1,127 @@
const { EmbedBuilder } = require('discord.js');
const { createClient } = require('@supabase/supabase-js');
let supabase = null;
if (process.env.SUPABASE_URL && process.env.SUPABASE_SERVICE_ROLE) {
supabase = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_SERVICE_ROLE);
}
const starboardCache = new Map();
const STAR_EMOJI = '⭐';
const DEFAULT_THRESHOLD = 3;
async function getStarboardConfig(guildId) {
if (!supabase) return null;
const cached = starboardCache.get(guildId);
if (cached && Date.now() - cached.timestamp < 60000) {
return cached.config;
}
try {
const { data } = await supabase
.from('starboard_config')
.select('*')
.eq('guild_id', guildId)
.maybeSingle();
starboardCache.set(guildId, { config: data, timestamp: Date.now() });
return data;
} catch (e) {
return null;
}
}
async function handleStarboard(reaction, user) {
if (reaction.emoji.name !== STAR_EMOJI) return;
if (reaction.message.author?.bot) return;
const guildId = reaction.message.guildId;
const config = await getStarboardConfig(guildId);
if (!config || !config.enabled || !config.channel_id) return;
const threshold = config.threshold || DEFAULT_THRESHOLD;
const starCount = reaction.count;
if (starCount < threshold) return;
const message = reaction.message;
const starboardChannel = message.guild.channels.cache.get(config.channel_id);
if (!starboardChannel) return;
try {
const { data: existing } = await supabase
.from('starboard_messages')
.select('starboard_message_id')
.eq('original_message_id', message.id)
.maybeSingle();
const embed = new EmbedBuilder()
.setColor(0xFFD700)
.setAuthor({
name: message.author.tag,
iconURL: message.author.displayAvatarURL({ size: 128 })
})
.setDescription(message.content || '*No text content*')
.addFields(
{ name: '📍 Source', value: `[Jump to message](${message.url})`, inline: true },
{ name: '⭐ Stars', value: `${starCount}`, inline: true }
)
.setTimestamp(message.createdAt)
.setFooter({ text: `#${message.channel.name}` });
if (message.attachments.size > 0) {
const attachment = message.attachments.first();
if (attachment.contentType?.startsWith('image/')) {
embed.setImage(attachment.url);
}
}
const starEmoji = starCount >= 10 ? '🌟' : starCount >= 5 ? '✨' : '⭐';
const content = `${starEmoji} **${starCount}** | <#${message.channelId}>`;
if (existing?.starboard_message_id) {
try {
const starboardMessage = await starboardChannel.messages.fetch(existing.starboard_message_id);
await starboardMessage.edit({ content, embeds: [embed] });
} catch (e) {}
} else {
const starboardMessage = await starboardChannel.send({ content, embeds: [embed] });
await supabase.from('starboard_messages').insert({
guild_id: guildId,
original_message_id: message.id,
starboard_message_id: starboardMessage.id,
channel_id: message.channelId,
author_id: message.author.id,
star_count: starCount
});
}
if (existing) {
await supabase
.from('starboard_messages')
.update({ star_count: starCount })
.eq('original_message_id', message.id);
}
} catch (e) {
console.error('[Starboard] Error:', e.message);
}
}
module.exports = {
name: 'messageReactionAdd',
async execute(reaction, user, client) {
if (reaction.partial) {
try {
await reaction.fetch();
} catch (e) {
return;
}
}
await handleStarboard(reaction, user);
}
};

View file

@ -1,5 +1,7 @@
const { EmbedBuilder } = require('discord.js');
const { checkAchievements } = require('../commands/achievements');
const { getServerMode, getEmbedColor } = require('../utils/modeHelper');
const { updateStandaloneXp, calculateLevel: standaloneCalcLevel } = require('../utils/standaloneXp');
const xpCooldowns = new Map();
const xpConfigCache = new Map();
@ -103,6 +105,16 @@ module.exports = {
if (now - lastXp < cooldownMs) return;
// Check server mode
const serverMode = await getServerMode(supabase, guildId);
// Handle standalone mode separately
if (serverMode === 'standalone') {
await handleStandaloneXp(message, client, supabase, config, discordUserId, guildId, channelId, now);
return;
}
// FEDERATED MODE - requires linked account
try {
const { data: link, error: linkError } = await supabase
.from('discord_links')
@ -537,6 +549,201 @@ async function upsertPeriodXp(supabase, userId, guildId, discordId, periodType,
}
}
// STANDALONE MODE XP HANDLER
async function handleStandaloneXp(message, client, supabase, config, discordUserId, guildId, channelId, now) {
try {
// Get existing standalone XP data
const { data: existingXp } = await supabase
.from('guild_user_xp')
.select('*')
.eq('discord_id', discordUserId)
.eq('guild_id', guildId)
.maybeSingle();
// Calculate base XP
let xpGain = config.message_xp || 5;
const prestige = existingXp?.prestige_level || 0;
// Apply channel bonus
const bonusChannels = config.bonus_channels || [];
const channelBonus = bonusChannels.find(c => c.channel_id === channelId);
if (channelBonus) {
xpGain = Math.floor(xpGain * channelBonus.multiplier);
}
// Apply role multipliers (use highest)
const multiplierRoles = config.multiplier_roles || [];
let highestMultiplier = 1;
for (const mr of multiplierRoles) {
if (message.member?.roles.cache.has(mr.role_id)) {
highestMultiplier = Math.max(highestMultiplier, mr.multiplier);
}
}
// Server boosters get 1.5x XP bonus automatically
if (message.member?.premiumSince) {
highestMultiplier = Math.max(highestMultiplier, 1.5);
}
xpGain = Math.floor(xpGain * highestMultiplier);
// Apply prestige bonus (+5% per prestige level)
if (prestige > 0) {
const prestigeBonus = 1 + (prestige * 0.05);
xpGain = Math.floor(xpGain * prestigeBonus);
}
const currentXp = existingXp?.xp || 0;
const newXp = currentXp + xpGain;
// Calculate levels
const oldLevel = standaloneCalcLevel(currentXp, config.level_curve);
const newLevel = standaloneCalcLevel(newXp, config.level_curve);
// Update or create standalone XP record
const result = await updateStandaloneXp(supabase, discordUserId, guildId, xpGain, message.author.username);
if (!result) return;
// Update cooldown
const cooldownKey = `${guildId}:${discordUserId}`;
xpCooldowns.set(cooldownKey, now);
// Track XP for analytics
if (client.trackXP) {
client.trackXP(xpGain);
}
// Level up announcement for standalone
if (newLevel > oldLevel) {
await sendStandaloneLevelUp(message, newLevel, newXp, config, client);
await checkMilestoneRolesStandalone(message.member, {
level: newLevel,
prestige: prestige,
totalXp: result.total_xp_earned || newXp
}, supabase, guildId);
}
} catch (error) {
console.error('Standalone XP tracking error:', error.message);
}
}
async function sendStandaloneLevelUp(message, newLevel, newXp, config, client) {
try {
const messageTemplate = config.levelup_message || '🎉 Congratulations {user}! You reached **Level {level}**!';
const channelId = config.levelup_channel_id;
const sendDm = config.levelup_dm === true;
const useEmbed = config.levelup_embed === true;
const embedColor = '#6B7280'; // Standalone gray color
const formattedMessage = messageTemplate
.replace(/{user}/g, message.author.toString())
.replace(/{username}/g, message.author.username)
.replace(/{level}/g, newLevel.toString())
.replace(/{xp}/g, newXp.toLocaleString())
.replace(/{server}/g, message.guild.name);
let messageContent;
if (useEmbed) {
const embed = new EmbedBuilder()
.setDescription(formattedMessage)
.setColor(parseInt(embedColor.replace('#', ''), 16))
.setThumbnail(message.author.displayAvatarURL({ dynamic: true }))
.setFooter({ text: '🏠 Standalone Mode' })
.setTimestamp();
messageContent = { embeds: [embed] };
} else {
messageContent = { content: formattedMessage };
}
if (sendDm) {
const dmSent = await message.author.send(messageContent).catch(() => null);
if (!dmSent) {
await message.channel.send(messageContent).catch(() => {});
}
} else if (channelId) {
const channel = await client.channels.fetch(channelId).catch(() => null);
if (channel) {
await channel.send(messageContent).catch(() => {});
} else {
await message.channel.send(messageContent).catch(() => {});
}
} else {
await message.channel.send(messageContent).catch(() => {});
}
} catch (error) {
console.error('Standalone level-up announcement error:', error.message);
}
}
async function checkMilestoneRolesStandalone(member, milestones, supabase, guildId) {
if (!member || !supabase) return;
try {
const { data: allRoles, error } = await supabase
.from('level_roles')
.select('*')
.eq('guild_id', guildId);
if (error || !allRoles || allRoles.length === 0) return;
const rolesToAdd = [];
const rolesToRemove = [];
for (const roleConfig of allRoles) {
const { role_id, milestone_type, milestone_value, stack_roles } = roleConfig;
let qualifies = false;
switch (milestone_type) {
case 'level':
qualifies = milestones.level >= milestone_value;
break;
case 'prestige':
qualifies = milestones.prestige >= milestone_value;
break;
case 'total_xp':
qualifies = milestones.totalXp >= milestone_value;
break;
}
if (qualifies && !member.roles.cache.has(role_id)) {
rolesToAdd.push({ role_id, milestone_type, milestone_value, stack_roles });
}
}
for (const roleToAdd of rolesToAdd) {
try {
await member.roles.add(roleToAdd.role_id);
if (!roleToAdd.stack_roles) {
const sameTypeRoles = allRoles.filter(r =>
r.milestone_type === roleToAdd.milestone_type &&
r.milestone_value < roleToAdd.milestone_value &&
member.roles.cache.has(r.role_id)
);
for (const oldRole of sameTypeRoles) {
if (!rolesToRemove.includes(oldRole.role_id)) {
rolesToRemove.push(oldRole.role_id);
}
}
}
} catch (e) {
// Role addition failed
}
}
for (const roleId of rolesToRemove) {
await member.roles.remove(roleId).catch(() => {});
}
} catch (e) {
// Silently ignore
}
}
// Export functions for use in other commands
module.exports.calculateLevel = calculateLevel;
module.exports.checkMilestoneRoles = checkMilestoneRoles;

View file

@ -1324,7 +1324,7 @@
<div class="stat-label">Unified Profiles</div>
</div>
<div class="stat-item">
<div class="stat-value" id="commandCount">44+</div>
<div class="stat-value" id="commandCount">60+</div>
<div class="stat-label">Commands</div>
</div>
<div class="stat-item">
@ -1597,6 +1597,126 @@
</div>
</div>
</div>
<!-- Fun & Games Features -->
<div class="feature-category">
<div class="category-header">
<div class="category-icon">🎮</div>
<h3>Fun & Games</h3>
</div>
<div class="features-grid">
<div class="feature-card">
<div class="feature-icon">🎱</div>
<h3>8-Ball & Fortune</h3>
<p>Ask the magic 8-ball questions, flip coins, and roll dice with custom notation.</p>
</div>
<div class="feature-card">
<div class="feature-icon">🧠</div>
<h3>Trivia</h3>
<p>Multiple categories, earn XP for correct answers. Test your knowledge daily.</p>
</div>
<div class="feature-card">
<div class="feature-icon">⚔️</div>
<h3>Duels</h3>
<p>Challenge others to 1v1 battles. Bet XP on the outcome for extra rewards.</p>
</div>
<div class="feature-card">
<div class="feature-icon">🎰</div>
<h3>Slot Machine</h3>
<p>Try your luck at slots. Match symbols for XP jackpots and winning streaks.</p>
</div>
</div>
</div>
<!-- Social Features -->
<div class="feature-category">
<div class="category-header">
<div class="category-icon">❤️</div>
<h3>Social & Interaction</h3>
</div>
<div class="features-grid">
<div class="feature-card">
<div class="feature-icon"></div>
<h3>Reputation</h3>
<p>Give and receive rep points. Build your community standing over time.</p>
</div>
<div class="feature-card">
<div class="feature-icon">🤗</div>
<h3>Social Actions</h3>
<p>Hugs, high-fives, and more with animated GIFs. Express yourself!</p>
</div>
<div class="feature-card">
<div class="feature-icon">🎂</div>
<h3>Birthdays</h3>
<p>Set your birthday, view upcoming celebrations, get special recognition.</p>
</div>
<div class="feature-card">
<div class="feature-icon"></div>
<h3>Reminders</h3>
<p>Set personal reminders. Never forget important events or tasks.</p>
</div>
</div>
</div>
<!-- Economy Features -->
<div class="feature-category">
<div class="category-header">
<div class="category-icon">💰</div>
<h3>Economy & Trading</h3>
</div>
<div class="features-grid">
<div class="feature-card">
<div class="feature-icon">💼</div>
<h3>Work System</h3>
<p>Work hourly for XP rewards. Different jobs with varying payouts.</p>
</div>
<div class="feature-card">
<div class="feature-icon">🏦</div>
<h3>Heists</h3>
<p>Team up for group heists. Higher risk, higher rewards. Strategy matters.</p>
</div>
<div class="feature-card">
<div class="feature-icon">🎁</div>
<h3>Gifting</h3>
<p>Gift XP to friends and community members. Spread the wealth.</p>
</div>
<div class="feature-card">
<div class="feature-icon">🔄</div>
<h3>Trading</h3>
<p>Trade items between users. Full inventory system with secure trades.</p>
</div>
</div>
</div>
<!-- Utility Features -->
<div class="feature-category">
<div class="category-header">
<div class="category-icon">🔧</div>
<h3>Utility Tools</h3>
</div>
<div class="features-grid">
<div class="feature-card">
<div class="feature-icon">🌐</div>
<h3>Translation</h3>
<p>Translate text between 100+ languages instantly. Break language barriers.</p>
</div>
<div class="feature-card">
<div class="feature-icon">📖</div>
<h3>Definitions</h3>
<p>Look up word definitions, synonyms, and usage examples.</p>
</div>
<div class="feature-card">
<div class="feature-icon">🔢</div>
<h3>Calculator</h3>
<p>Safe math expression evaluator. Complex calculations made easy.</p>
</div>
<div class="feature-card">
<div class="feature-icon">📱</div>
<h3>QR Codes</h3>
<p>Generate QR codes for any text or URL. Share links instantly.</p>
</div>
</div>
</div>
</div>
</section>

View file

@ -0,0 +1,105 @@
const { EmbedBuilder } = require('discord.js');
const EMBED_COLORS = {
federated: 0x4A90E2,
standalone: 0x6B7280,
success: 0x22C55E,
error: 0xEF4444,
warning: 0xF59E0B,
};
const modeConfigCache = new Map();
const MODE_CACHE_TTL = 60000;
async function getServerMode(supabase, guildId) {
if (!supabase) return 'standalone';
const now = Date.now();
const cached = modeConfigCache.get(guildId);
if (cached && (now - cached.timestamp < MODE_CACHE_TTL)) {
return cached.mode;
}
try {
const { data } = await supabase
.from('server_config')
.select('mode')
.eq('guild_id', guildId)
.maybeSingle();
const mode = data?.mode || 'federated';
modeConfigCache.set(guildId, { mode, timestamp: now });
return mode;
} catch (e) {
return 'federated';
}
}
async function setServerMode(supabase, guildId, mode) {
if (!supabase) return false;
try {
await supabase.from('server_config').upsert({
guild_id: guildId,
mode: mode,
mode_changed_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
}, { onConflict: 'guild_id' });
modeConfigCache.set(guildId, { mode, timestamp: Date.now() });
return true;
} catch (e) {
console.error('Failed to set server mode:', e.message);
return false;
}
}
function clearModeCache(guildId) {
if (guildId) {
modeConfigCache.delete(guildId);
} else {
modeConfigCache.clear();
}
}
function getEmbedColor(mode) {
return mode === 'standalone' ? EMBED_COLORS.standalone : EMBED_COLORS.federated;
}
function createModeEmbed(mode, title, description) {
return new EmbedBuilder()
.setColor(getEmbedColor(mode))
.setTitle(title)
.setDescription(description)
.setTimestamp();
}
function isFederated(mode) {
return mode === 'federated';
}
function isStandalone(mode) {
return mode === 'standalone';
}
function getModeDisplayName(mode) {
return mode === 'federated' ? 'Federation' : 'Standalone';
}
function getModeEmoji(mode) {
return mode === 'federated' ? '🌐' : '🏠';
}
module.exports = {
EMBED_COLORS,
getServerMode,
setServerMode,
clearModeCache,
getEmbedColor,
createModeEmbed,
isFederated,
isStandalone,
getModeDisplayName,
getModeEmoji,
};

View file

@ -0,0 +1,230 @@
const { EmbedBuilder } = require('discord.js');
const { getEmbedColor } = require('./modeHelper');
async function getStandaloneXp(supabase, discordId, guildId) {
if (!supabase) return null;
try {
const { data } = await supabase
.from('guild_user_xp')
.select('*')
.eq('discord_id', discordId)
.eq('guild_id', guildId)
.maybeSingle();
return data || null;
} catch (e) {
return null;
}
}
async function updateStandaloneXp(supabase, discordId, guildId, xpGain, username) {
if (!supabase) return null;
try {
const { data: existing } = await supabase
.from('guild_user_xp')
.select('*')
.eq('discord_id', discordId)
.eq('guild_id', guildId)
.maybeSingle();
if (existing) {
const newXp = (existing.xp || 0) + xpGain;
const totalEarned = (existing.total_xp_earned || 0) + xpGain;
const { data } = await supabase
.from('guild_user_xp')
.update({
xp: newXp,
total_xp_earned: totalEarned,
updated_at: new Date().toISOString(),
})
.eq('id', existing.id)
.select()
.single();
return data;
} else {
const { data } = await supabase
.from('guild_user_xp')
.insert({
discord_id: discordId,
guild_id: guildId,
username: username,
xp: xpGain,
total_xp_earned: xpGain,
prestige_level: 0,
daily_streak: 0,
})
.select()
.single();
return data;
}
} catch (e) {
console.error('Standalone XP update error:', e.message);
return null;
}
}
async function getStandaloneLeaderboard(supabase, guildId, limit = 10) {
if (!supabase) return [];
try {
const { data } = await supabase
.from('guild_user_xp')
.select('discord_id, username, xp, prestige_level')
.eq('guild_id', guildId)
.order('xp', { ascending: false })
.limit(limit);
return data || [];
} catch (e) {
return [];
}
}
async function claimStandaloneDaily(supabase, discordId, guildId, username) {
if (!supabase) return { success: false, message: 'Database not available' };
try {
const { data: existing } = await supabase
.from('guild_user_xp')
.select('*')
.eq('discord_id', discordId)
.eq('guild_id', guildId)
.maybeSingle();
const now = new Date();
const today = now.toISOString().split('T')[0];
if (existing) {
const lastClaim = existing.last_daily_claim ? existing.last_daily_claim.split('T')[0] : null;
if (lastClaim === today) {
return { success: false, message: 'You already claimed your daily reward today!' };
}
const yesterday = new Date(now);
yesterday.setDate(yesterday.getDate() - 1);
const yesterdayStr = yesterday.toISOString().split('T')[0];
let newStreak = 1;
if (lastClaim === yesterdayStr) {
newStreak = (existing.daily_streak || 0) + 1;
}
const baseXp = 50;
const streakBonus = Math.min(newStreak * 5, 100);
const totalXp = baseXp + streakBonus;
const newXp = (existing.xp || 0) + totalXp;
const totalEarned = (existing.total_xp_earned || 0) + totalXp;
await supabase
.from('guild_user_xp')
.update({
xp: newXp,
total_xp_earned: totalEarned,
daily_streak: newStreak,
last_daily_claim: now.toISOString(),
updated_at: now.toISOString(),
})
.eq('id', existing.id);
return {
success: true,
xpGained: totalXp,
streak: newStreak,
totalXp: newXp,
};
} else {
const baseXp = 50;
await supabase
.from('guild_user_xp')
.insert({
discord_id: discordId,
guild_id: guildId,
username: username,
xp: baseXp,
total_xp_earned: baseXp,
prestige_level: 0,
daily_streak: 1,
last_daily_claim: now.toISOString(),
});
return {
success: true,
xpGained: baseXp,
streak: 1,
totalXp: baseXp,
};
}
} catch (e) {
console.error('Standalone daily claim error:', e.message);
return { success: false, message: 'An error occurred' };
}
}
async function prestigeStandalone(supabase, discordId, guildId) {
if (!supabase) return { success: false, message: 'Database not available' };
try {
const { data: existing } = await supabase
.from('guild_user_xp')
.select('*')
.eq('discord_id', discordId)
.eq('guild_id', guildId)
.maybeSingle();
if (!existing) {
return { success: false, message: 'No XP data found' };
}
const level = calculateLevel(existing.xp || 0, 'normal');
if (level < 50) {
return { success: false, message: `You need Level 50 to prestige! (Current: Level ${level})` };
}
const newPrestige = (existing.prestige_level || 0) + 1;
await supabase
.from('guild_user_xp')
.update({
xp: 0,
prestige_level: newPrestige,
updated_at: new Date().toISOString(),
})
.eq('id', existing.id);
return {
success: true,
newPrestige: newPrestige,
bonus: newPrestige * 5,
};
} catch (e) {
console.error('Standalone prestige error:', e.message);
return { success: false, message: 'An error occurred' };
}
}
function calculateLevel(xp, curve = 'normal') {
const bases = {
easy: 50,
normal: 100,
hard: 200,
};
const base = bases[curve] || 100;
return Math.floor(Math.sqrt(xp / base));
}
module.exports = {
getStandaloneXp,
updateStandaloneXp,
getStandaloneLeaderboard,
claimStandaloneDaily,
prestigeStandalone,
calculateLevel,
};