Add reaction-based XP rewards to the bot and update documentation
Introduce a new listener for message reaction events to grant XP to users, alongside updates to the XP settings command, database schema, and documentation to support this feature. Replit-Commit-Author: Agent Replit-Commit-Session-Id: aed2e46d-25bb-4b73-81a1-bb9e8437c261 Replit-Commit-Checkpoint-Type: intermediate_checkpoint Replit-Commit-Event-Id: 03f44698-7f39-483b-b5d1-803ddc2519c6 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/3bdfff67-975a-46ad-9845-fbb6b4a4c4b5/aed2e46d-25bb-4b73-81a1-bb9e8437c261/ZjyNKqu Replit-Helium-Checkpoint-Created: true
This commit is contained in:
parent
78647044bb
commit
f96043dc21
4 changed files with 278 additions and 7 deletions
|
|
@ -49,6 +49,7 @@ const client = new Client({
|
|||
GatewayIntentBits.GuildMessages,
|
||||
GatewayIntentBits.MessageContent,
|
||||
GatewayIntentBits.DirectMessages,
|
||||
GatewayIntentBits.GuildMessageReactions,
|
||||
],
|
||||
});
|
||||
|
||||
|
|
@ -678,7 +679,7 @@ if (fs.existsSync(sentinelPath)) {
|
|||
// =============================================================================
|
||||
|
||||
const listenersPath = path.join(__dirname, "listeners");
|
||||
const generalListenerFiles = ['welcome.js', 'goodbye.js', 'xpTracker.js'];
|
||||
const generalListenerFiles = ['welcome.js', 'goodbye.js', 'xpTracker.js', 'reactionXp.js'];
|
||||
for (const file of generalListenerFiles) {
|
||||
const filePath = path.join(listenersPath, file);
|
||||
if (fs.existsSync(filePath)) {
|
||||
|
|
|
|||
|
|
@ -70,7 +70,38 @@ module.exports = {
|
|||
{ name: 'Easy - Faster leveling', value: 'easy' },
|
||||
{ name: 'Normal - Standard leveling', value: 'normal' },
|
||||
{ name: 'Hard - Slower leveling', value: 'hard' }
|
||||
))),
|
||||
)))
|
||||
.addSubcommand(sub =>
|
||||
sub.setName('reaction-xp')
|
||||
.setDescription('Configure XP for reactions')
|
||||
.addIntegerOption(opt =>
|
||||
opt.setName('received')
|
||||
.setDescription('XP for receiving a reaction (0-20)')
|
||||
.setRequired(true)
|
||||
.setMinValue(0)
|
||||
.setMaxValue(20))
|
||||
.addIntegerOption(opt =>
|
||||
opt.setName('given')
|
||||
.setDescription('XP for giving a reaction (0-10)')
|
||||
.setRequired(true)
|
||||
.setMinValue(0)
|
||||
.setMaxValue(10)))
|
||||
.addSubcommand(sub =>
|
||||
sub.setName('reaction-cooldown')
|
||||
.setDescription('Set cooldown between reaction XP gains')
|
||||
.addIntegerOption(opt =>
|
||||
opt.setName('seconds')
|
||||
.setDescription('Cooldown in seconds (5-120)')
|
||||
.setRequired(true)
|
||||
.setMinValue(5)
|
||||
.setMaxValue(120)))
|
||||
.addSubcommand(sub =>
|
||||
sub.setName('reaction-toggle')
|
||||
.setDescription('Enable or disable reaction XP')
|
||||
.addBooleanOption(opt =>
|
||||
opt.setName('enabled')
|
||||
.setDescription('Enable or disable reaction XP')
|
||||
.setRequired(true))),
|
||||
|
||||
async execute(interaction, client, supabase) {
|
||||
if (!supabase) {
|
||||
|
|
@ -101,6 +132,12 @@ module.exports = {
|
|||
return handleToggle(interaction, supabase, guildId, config);
|
||||
case 'level-curve':
|
||||
return handleLevelCurve(interaction, supabase, guildId, config);
|
||||
case 'reaction-xp':
|
||||
return handleReactionXp(interaction, supabase, guildId, config);
|
||||
case 'reaction-cooldown':
|
||||
return handleReactionCooldown(interaction, supabase, guildId, config);
|
||||
case 'reaction-toggle':
|
||||
return handleReactionToggle(interaction, supabase, guildId, config);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -124,7 +161,11 @@ async function getXpConfig(supabase, guildId) {
|
|||
multiplier_roles: [],
|
||||
bonus_channels: [],
|
||||
level_curve: 'normal',
|
||||
xp_enabled: true
|
||||
xp_enabled: true,
|
||||
reaction_xp_enabled: true,
|
||||
reaction_xp_received: 3,
|
||||
reaction_xp_given: 1,
|
||||
reaction_cooldown: 30
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -138,7 +179,11 @@ async function getXpConfig(supabase, guildId) {
|
|||
multiplier_roles: [],
|
||||
bonus_channels: [],
|
||||
level_curve: 'normal',
|
||||
xp_enabled: true
|
||||
xp_enabled: true,
|
||||
reaction_xp_enabled: true,
|
||||
reaction_xp_received: 3,
|
||||
reaction_xp_given: 1,
|
||||
reaction_cooldown: 30
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -155,6 +200,10 @@ async function saveXpConfig(supabase, guildId, config) {
|
|||
bonus_channels: config.bonus_channels,
|
||||
level_curve: config.level_curve,
|
||||
xp_enabled: config.xp_enabled,
|
||||
reaction_xp_enabled: config.reaction_xp_enabled,
|
||||
reaction_xp_received: config.reaction_xp_received,
|
||||
reaction_xp_given: config.reaction_xp_given,
|
||||
reaction_cooldown: config.reaction_cooldown,
|
||||
updated_at: new Date().toISOString()
|
||||
});
|
||||
|
||||
|
|
@ -184,6 +233,11 @@ async function handleView(interaction, config) {
|
|||
hard: 'Hard (200 XP per level²)'
|
||||
};
|
||||
|
||||
const reactionEnabled = config.reaction_xp_enabled !== false;
|
||||
const reactionReceived = config.reaction_xp_received ?? 3;
|
||||
const reactionGiven = config.reaction_xp_given ?? 1;
|
||||
const reactionCooldown = config.reaction_cooldown ?? 30;
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle('⚙️ XP Settings')
|
||||
.setColor(config.xp_enabled ? 0x00ff88 : 0xff4444)
|
||||
|
|
@ -193,7 +247,11 @@ async function handleView(interaction, config) {
|
|||
{ name: '⏱️ Cooldown', value: `${config.message_cooldown}s`, inline: true },
|
||||
{ name: '📈 Level Curve', value: curveInfo[config.level_curve] || 'Normal', inline: false },
|
||||
{ name: '🎭 Multiplier Roles', value: rolesText, inline: false },
|
||||
{ name: '📢 Bonus Channels', value: channelsText, inline: false }
|
||||
{ name: '📢 Bonus Channels', value: channelsText, inline: false },
|
||||
{ name: '😀 Reaction XP', value: reactionEnabled ? '✅ Enabled' : '❌ Disabled', inline: true },
|
||||
{ name: '📥 Receive Reaction', value: `${reactionReceived} XP`, inline: true },
|
||||
{ name: '📤 Give Reaction', value: `${reactionGiven} XP`, inline: true },
|
||||
{ name: '⏳ Reaction Cooldown', value: `${reactionCooldown}s`, inline: true }
|
||||
)
|
||||
.setFooter({ text: 'Use /xp-settings subcommands to modify' })
|
||||
.setTimestamp();
|
||||
|
|
@ -376,3 +434,62 @@ async function handleLevelCurve(interaction, supabase, guildId, config) {
|
|||
ephemeral: true
|
||||
});
|
||||
}
|
||||
|
||||
async function handleReactionXp(interaction, supabase, guildId, config) {
|
||||
const received = interaction.options.getInteger('received');
|
||||
const given = interaction.options.getInteger('given');
|
||||
|
||||
config.reaction_xp_received = received;
|
||||
config.reaction_xp_given = given;
|
||||
|
||||
const saved = await saveXpConfig(supabase, guildId, config);
|
||||
if (!saved) {
|
||||
return interaction.reply({
|
||||
content: '❌ Failed to save settings. Please try again.',
|
||||
ephemeral: true
|
||||
});
|
||||
}
|
||||
|
||||
return interaction.reply({
|
||||
content: `✅ Reaction XP set: **${received} XP** for receiving, **${given} XP** for giving.`,
|
||||
ephemeral: true
|
||||
});
|
||||
}
|
||||
|
||||
async function handleReactionCooldown(interaction, supabase, guildId, config) {
|
||||
const seconds = interaction.options.getInteger('seconds');
|
||||
config.reaction_cooldown = seconds;
|
||||
|
||||
const saved = await saveXpConfig(supabase, guildId, config);
|
||||
if (!saved) {
|
||||
return interaction.reply({
|
||||
content: '❌ Failed to save settings. Please try again.',
|
||||
ephemeral: true
|
||||
});
|
||||
}
|
||||
|
||||
return interaction.reply({
|
||||
content: `✅ Reaction XP cooldown set to **${seconds} seconds**.`,
|
||||
ephemeral: true
|
||||
});
|
||||
}
|
||||
|
||||
async function handleReactionToggle(interaction, supabase, guildId, config) {
|
||||
const enabled = interaction.options.getBoolean('enabled');
|
||||
config.reaction_xp_enabled = enabled;
|
||||
|
||||
const saved = await saveXpConfig(supabase, guildId, config);
|
||||
if (!saved) {
|
||||
return interaction.reply({
|
||||
content: '❌ Failed to save settings. Please try again.',
|
||||
ephemeral: true
|
||||
});
|
||||
}
|
||||
|
||||
return interaction.reply({
|
||||
content: enabled
|
||||
? '✅ Reaction XP is now **enabled**.'
|
||||
: '❌ Reaction XP is now **disabled**.',
|
||||
ephemeral: true
|
||||
});
|
||||
}
|
||||
|
|
|
|||
140
aethex-bot/listeners/reactionXp.js
Normal file
140
aethex-bot/listeners/reactionXp.js
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
const reactionCooldowns = new Map();
|
||||
const xpConfigCache = new Map();
|
||||
const CACHE_TTL = 60000;
|
||||
|
||||
module.exports = {
|
||||
name: 'messageReactionAdd',
|
||||
|
||||
async execute(reaction, user, client, supabase) {
|
||||
if (!supabase) return;
|
||||
if (user.bot) return;
|
||||
if (!reaction.message.guild) return;
|
||||
|
||||
if (reaction.partial) {
|
||||
try {
|
||||
await reaction.fetch();
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch reaction:', error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const messageAuthor = reaction.message.author;
|
||||
if (!messageAuthor || messageAuthor.bot) return;
|
||||
if (messageAuthor.id === user.id) return;
|
||||
|
||||
const guildId = reaction.message.guild.id;
|
||||
const now = Date.now();
|
||||
|
||||
const config = await getXpConfig(supabase, guildId);
|
||||
|
||||
if (!config.xp_enabled) return;
|
||||
if (!config.reaction_xp_enabled) return;
|
||||
|
||||
const receiverXp = config.reaction_xp_received || 3;
|
||||
const giverXp = config.reaction_xp_given || 1;
|
||||
const cooldownMs = (config.reaction_cooldown || 30) * 1000;
|
||||
|
||||
const cooldownKeyGiver = `reaction:giver:${guildId}:${user.id}`;
|
||||
const cooldownKeyReceiver = `reaction:receiver:${guildId}:${messageAuthor.id}`;
|
||||
const cooldownKeyMessage = `reaction:message:${guildId}:${reaction.message.id}`;
|
||||
|
||||
const lastGiverXp = reactionCooldowns.get(cooldownKeyGiver) || 0;
|
||||
const lastReceiverXp = reactionCooldowns.get(cooldownKeyReceiver) || 0;
|
||||
const lastMessageXp = reactionCooldowns.get(cooldownKeyMessage) || 0;
|
||||
|
||||
const giverOnCooldown = now - lastGiverXp < cooldownMs;
|
||||
const receiverOnCooldown = now - lastReceiverXp < cooldownMs || now - lastMessageXp < cooldownMs;
|
||||
|
||||
try {
|
||||
if (!giverOnCooldown && giverXp > 0) {
|
||||
await grantXp(supabase, user.id, giverXp, client);
|
||||
reactionCooldowns.set(cooldownKeyGiver, now);
|
||||
}
|
||||
|
||||
if (!receiverOnCooldown && receiverXp > 0) {
|
||||
await grantXp(supabase, messageAuthor.id, receiverXp, client);
|
||||
reactionCooldowns.set(cooldownKeyReceiver, now);
|
||||
reactionCooldowns.set(cooldownKeyMessage, now);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Reaction XP tracking error:', error.message);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
async function grantXp(supabase, discordUserId, xpAmount, client) {
|
||||
const { data: link, error: linkError } = await supabase
|
||||
.from('discord_links')
|
||||
.select('user_id')
|
||||
.eq('discord_id', discordUserId)
|
||||
.maybeSingle();
|
||||
|
||||
if (linkError || !link) return;
|
||||
|
||||
const { data: profile, error: profileError } = await supabase
|
||||
.from('user_profiles')
|
||||
.select('xp')
|
||||
.eq('id', link.user_id)
|
||||
.maybeSingle();
|
||||
|
||||
if (profileError || !profile) return;
|
||||
|
||||
const currentXp = profile.xp || 0;
|
||||
const newXp = currentXp + xpAmount;
|
||||
|
||||
const { error: updateError } = await supabase
|
||||
.from('user_profiles')
|
||||
.update({ xp: newXp })
|
||||
.eq('id', link.user_id);
|
||||
|
||||
if (updateError) return;
|
||||
|
||||
if (client.trackXP) {
|
||||
client.trackXP(xpAmount);
|
||||
}
|
||||
}
|
||||
|
||||
async function getXpConfig(supabase, guildId) {
|
||||
const now = Date.now();
|
||||
const cached = xpConfigCache.get(guildId);
|
||||
|
||||
if (cached && (now - cached.timestamp < CACHE_TTL)) {
|
||||
return cached.config;
|
||||
}
|
||||
|
||||
try {
|
||||
const { data, error } = await supabase
|
||||
.from('xp_config')
|
||||
.select('*')
|
||||
.eq('guild_id', guildId)
|
||||
.maybeSingle();
|
||||
|
||||
if (error) {
|
||||
const defaults = getDefaultConfig();
|
||||
xpConfigCache.set(guildId, { config: defaults, timestamp: now });
|
||||
return defaults;
|
||||
}
|
||||
|
||||
const config = data || getDefaultConfig();
|
||||
xpConfigCache.set(guildId, { config, timestamp: now });
|
||||
return config;
|
||||
} catch (e) {
|
||||
return getDefaultConfig();
|
||||
}
|
||||
}
|
||||
|
||||
function getDefaultConfig() {
|
||||
return {
|
||||
message_xp: 5,
|
||||
message_cooldown: 60,
|
||||
multiplier_roles: [],
|
||||
bonus_channels: [],
|
||||
level_curve: 'normal',
|
||||
xp_enabled: true,
|
||||
reaction_xp_enabled: true,
|
||||
reaction_xp_received: 3,
|
||||
reaction_xp_given: 1,
|
||||
reaction_cooldown: 30
|
||||
};
|
||||
}
|
||||
17
replit.md
17
replit.md
|
|
@ -79,6 +79,7 @@ aethex-bot/
|
|||
│ ├── welcome.js # Rich welcome messages + auto-role
|
||||
│ ├── goodbye.js # Rich goodbye messages
|
||||
│ ├── xpTracker.js # XP tracking on messages
|
||||
│ ├── reactionXp.js # XP tracking for reactions
|
||||
│ └── sentinel/
|
||||
│ ├── antiNuke.js # Channel delete monitor
|
||||
│ ├── roleDelete.js # Role delete monitor
|
||||
|
|
@ -170,11 +171,23 @@ aethex-bot/
|
|||
|
||||
XP is earned across all platforms and stored in a single profile:
|
||||
|
||||
- **Discord Messages**: +5 XP per message (60s cooldown)
|
||||
- **Discord Messages**: +5 XP per message (60s cooldown, configurable)
|
||||
- **Reaction XP**: +3 XP for receiving reactions, +1 XP for giving (30s cooldown, configurable)
|
||||
- **Daily Claims**: +50 XP base + streak bonus (up to +100)
|
||||
- **Platform Activity**: Posts, likes, comments on AeThex sites
|
||||
|
||||
Level formula: `level = floor(sqrt(xp / 100))`
|
||||
Level formula: `level = floor(sqrt(xp / base))` where base is 50 (easy), 100 (normal), or 200 (hard)
|
||||
|
||||
### XP Configuration (/xp-settings)
|
||||
- `message-xp` - Set XP per message (1-50)
|
||||
- `cooldown` - Set message XP cooldown (10-300s)
|
||||
- `reaction-xp` - Set XP for giving/receiving reactions
|
||||
- `reaction-cooldown` - Set reaction XP cooldown (5-120s)
|
||||
- `reaction-toggle` - Enable/disable reaction XP
|
||||
- `multiplier-role` - Add role-based XP multipliers
|
||||
- `bonus-channel` - Add channel-based XP bonuses
|
||||
- `level-curve` - Set leveling difficulty (easy/normal/hard)
|
||||
- `toggle` - Enable/disable XP system
|
||||
|
||||
## Supabase Tables Required
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue