Add customizable level-up announcements for user progression
Adds new subcommands to `xp-settings.js` for configuring level-up channel, message, DM notifications, and embed usage. Updates `xpTracker.js` to use these configurations, including placeholder support and embed customization, and introduces a `sendLevelUpAnnouncement` function. Replit-Commit-Author: Agent Replit-Commit-Session-Id: aed2e46d-25bb-4b73-81a1-bb9e8437c261 Replit-Commit-Checkpoint-Type: intermediate_checkpoint Replit-Commit-Event-Id: df3297ab-2de4-4d8a-99e3-9f5a31ff4ac6 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/3bdfff67-975a-46ad-9845-fbb6b4a4c4b5/aed2e46d-25bb-4b73-81a1-bb9e8437c261/yTaZipL Replit-Helium-Checkpoint-Created: true
This commit is contained in:
parent
2f414e5616
commit
140fe14c34
3 changed files with 265 additions and 23 deletions
4
.replit
4
.replit
|
|
@ -22,10 +22,6 @@ externalPort = 80
|
||||||
localPort = 8080
|
localPort = 8080
|
||||||
externalPort = 8080
|
externalPort = 8080
|
||||||
|
|
||||||
[[ports]]
|
|
||||||
localPort = 35953
|
|
||||||
externalPort = 3000
|
|
||||||
|
|
||||||
[workflows]
|
[workflows]
|
||||||
runButton = "Project"
|
runButton = "Project"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -126,7 +126,43 @@ module.exports = {
|
||||||
.addBooleanOption(opt =>
|
.addBooleanOption(opt =>
|
||||||
opt.setName('enabled')
|
opt.setName('enabled')
|
||||||
.setDescription('Enable or disable voice XP')
|
.setDescription('Enable or disable voice XP')
|
||||||
.setRequired(true))),
|
.setRequired(true)))
|
||||||
|
.addSubcommand(sub =>
|
||||||
|
sub.setName('levelup-channel')
|
||||||
|
.setDescription('Set a dedicated channel for level-up announcements')
|
||||||
|
.addChannelOption(opt =>
|
||||||
|
opt.setName('channel')
|
||||||
|
.setDescription('Channel for level-ups (leave empty to use current channel)')
|
||||||
|
.setRequired(false)))
|
||||||
|
.addSubcommand(sub =>
|
||||||
|
sub.setName('levelup-message')
|
||||||
|
.setDescription('Set custom level-up message (use {user}, {level}, {xp}, {server})')
|
||||||
|
.addStringOption(opt =>
|
||||||
|
opt.setName('message')
|
||||||
|
.setDescription('Custom message with placeholders')
|
||||||
|
.setRequired(true)
|
||||||
|
.setMaxLength(500)))
|
||||||
|
.addSubcommand(sub =>
|
||||||
|
sub.setName('levelup-dm')
|
||||||
|
.setDescription('Toggle DM notifications for level-ups instead of channel')
|
||||||
|
.addBooleanOption(opt =>
|
||||||
|
opt.setName('enabled')
|
||||||
|
.setDescription('Send level-up via DM')
|
||||||
|
.setRequired(true)))
|
||||||
|
.addSubcommand(sub =>
|
||||||
|
sub.setName('levelup-embed')
|
||||||
|
.setDescription('Toggle using embeds for level-up announcements')
|
||||||
|
.addBooleanOption(opt =>
|
||||||
|
opt.setName('enabled')
|
||||||
|
.setDescription('Use embed for level-ups')
|
||||||
|
.setRequired(true))
|
||||||
|
.addStringOption(opt =>
|
||||||
|
opt.setName('color')
|
||||||
|
.setDescription('Embed color (hex, e.g., #5865F2)')
|
||||||
|
.setRequired(false)))
|
||||||
|
.addSubcommand(sub =>
|
||||||
|
sub.setName('levelup-reset')
|
||||||
|
.setDescription('Reset level-up settings to defaults')),
|
||||||
|
|
||||||
async execute(interaction, client, supabase) {
|
async execute(interaction, client, supabase) {
|
||||||
if (!supabase) {
|
if (!supabase) {
|
||||||
|
|
@ -169,6 +205,16 @@ module.exports = {
|
||||||
return handleVoiceCooldown(interaction, supabase, guildId, config);
|
return handleVoiceCooldown(interaction, supabase, guildId, config);
|
||||||
case 'voice-toggle':
|
case 'voice-toggle':
|
||||||
return handleVoiceToggle(interaction, supabase, guildId, config);
|
return handleVoiceToggle(interaction, supabase, guildId, config);
|
||||||
|
case 'levelup-channel':
|
||||||
|
return handleLevelupChannel(interaction, supabase, guildId, config);
|
||||||
|
case 'levelup-message':
|
||||||
|
return handleLevelupMessage(interaction, supabase, guildId, config);
|
||||||
|
case 'levelup-dm':
|
||||||
|
return handleLevelupDm(interaction, supabase, guildId, config);
|
||||||
|
case 'levelup-embed':
|
||||||
|
return handleLevelupEmbed(interaction, supabase, guildId, config);
|
||||||
|
case 'levelup-reset':
|
||||||
|
return handleLevelupReset(interaction, supabase, guildId, config);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -199,7 +245,12 @@ async function getXpConfig(supabase, guildId) {
|
||||||
reaction_cooldown: 30,
|
reaction_cooldown: 30,
|
||||||
voice_xp_enabled: true,
|
voice_xp_enabled: true,
|
||||||
voice_xp: 2,
|
voice_xp: 2,
|
||||||
voice_cooldown: 60
|
voice_cooldown: 60,
|
||||||
|
levelup_channel_id: null,
|
||||||
|
levelup_message: '🎉 Congratulations {user}! You reached **Level {level}**!',
|
||||||
|
levelup_dm: false,
|
||||||
|
levelup_embed: false,
|
||||||
|
levelup_embed_color: '#5865F2'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -220,7 +271,12 @@ async function getXpConfig(supabase, guildId) {
|
||||||
reaction_cooldown: 30,
|
reaction_cooldown: 30,
|
||||||
voice_xp_enabled: true,
|
voice_xp_enabled: true,
|
||||||
voice_xp: 2,
|
voice_xp: 2,
|
||||||
voice_cooldown: 60
|
voice_cooldown: 60,
|
||||||
|
levelup_channel_id: null,
|
||||||
|
levelup_message: '🎉 Congratulations {user}! You reached **Level {level}**!',
|
||||||
|
levelup_dm: false,
|
||||||
|
levelup_embed: false,
|
||||||
|
levelup_embed_color: '#5865F2'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -244,6 +300,11 @@ async function saveXpConfig(supabase, guildId, config) {
|
||||||
voice_xp_enabled: config.voice_xp_enabled,
|
voice_xp_enabled: config.voice_xp_enabled,
|
||||||
voice_xp: config.voice_xp,
|
voice_xp: config.voice_xp,
|
||||||
voice_cooldown: config.voice_cooldown,
|
voice_cooldown: config.voice_cooldown,
|
||||||
|
levelup_channel_id: config.levelup_channel_id,
|
||||||
|
levelup_message: config.levelup_message,
|
||||||
|
levelup_dm: config.levelup_dm,
|
||||||
|
levelup_embed: config.levelup_embed,
|
||||||
|
levelup_embed_color: config.levelup_embed_color,
|
||||||
updated_at: new Date().toISOString()
|
updated_at: new Date().toISOString()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -282,6 +343,12 @@ async function handleView(interaction, config) {
|
||||||
const voiceXp = config.voice_xp ?? 2;
|
const voiceXp = config.voice_xp ?? 2;
|
||||||
const voiceCooldown = config.voice_cooldown ?? 60;
|
const voiceCooldown = config.voice_cooldown ?? 60;
|
||||||
|
|
||||||
|
const levelupChannel = config.levelup_channel_id ? `<#${config.levelup_channel_id}>` : 'Current channel';
|
||||||
|
const levelupMessage = config.levelup_message || '🎉 Congratulations {user}! You reached **Level {level}**!';
|
||||||
|
const levelupDm = config.levelup_dm === true;
|
||||||
|
const levelupEmbed = config.levelup_embed === true;
|
||||||
|
const levelupColor = config.levelup_embed_color || '#5865F2';
|
||||||
|
|
||||||
const embed = new EmbedBuilder()
|
const embed = new EmbedBuilder()
|
||||||
.setTitle('⚙️ XP Settings')
|
.setTitle('⚙️ XP Settings')
|
||||||
.setColor(config.xp_enabled ? 0x00ff88 : 0xff4444)
|
.setColor(config.xp_enabled ? 0x00ff88 : 0xff4444)
|
||||||
|
|
@ -298,9 +365,14 @@ async function handleView(interaction, config) {
|
||||||
{ name: '⏳ Reaction Cooldown', value: `${reactionCooldown}s`, inline: true },
|
{ name: '⏳ Reaction Cooldown', value: `${reactionCooldown}s`, inline: true },
|
||||||
{ name: '🎤 Voice XP', value: voiceEnabled ? '✅ Enabled' : '❌ Disabled', inline: true },
|
{ name: '🎤 Voice XP', value: voiceEnabled ? '✅ Enabled' : '❌ Disabled', inline: true },
|
||||||
{ name: '🔊 XP per Minute', value: `${voiceXp} XP`, inline: true },
|
{ name: '🔊 XP per Minute', value: `${voiceXp} XP`, inline: true },
|
||||||
{ name: '⏰ Voice Cooldown', value: `${voiceCooldown}s`, inline: true }
|
{ name: '⏰ Voice Cooldown', value: `${voiceCooldown}s`, inline: true },
|
||||||
|
{ name: '\u200B', value: '**Level-Up Announcements**', inline: false },
|
||||||
|
{ name: '📣 Channel', value: levelupChannel, inline: true },
|
||||||
|
{ name: '💌 DM Mode', value: levelupDm ? '✅ Enabled' : '❌ Disabled', inline: true },
|
||||||
|
{ name: '🖼️ Use Embed', value: levelupEmbed ? `✅ (${levelupColor})` : '❌ Disabled', inline: true },
|
||||||
|
{ name: '💬 Message', value: `\`${levelupMessage.slice(0, 100)}${levelupMessage.length > 100 ? '...' : ''}\``, inline: false }
|
||||||
)
|
)
|
||||||
.setFooter({ text: 'Use /xp-settings subcommands to modify' })
|
.setFooter({ text: 'Use /xp-settings subcommands to modify | Placeholders: {user}, {level}, {xp}, {server}' })
|
||||||
.setTimestamp();
|
.setTimestamp();
|
||||||
|
|
||||||
return interaction.reply({ embeds: [embed] });
|
return interaction.reply({ embeds: [embed] });
|
||||||
|
|
@ -596,3 +668,139 @@ async function handleVoiceToggle(interaction, supabase, guildId, config) {
|
||||||
ephemeral: true
|
ephemeral: true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleLevelupChannel(interaction, supabase, guildId, config) {
|
||||||
|
const channel = interaction.options.getChannel('channel');
|
||||||
|
|
||||||
|
if (!channel) {
|
||||||
|
config.levelup_channel_id = null;
|
||||||
|
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: '✅ Level-up announcements will now appear in the channel where the user leveled up.',
|
||||||
|
ephemeral: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
config.levelup_channel_id = channel.id;
|
||||||
|
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: `✅ Level-up announcements will now be sent to ${channel}.`,
|
||||||
|
ephemeral: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleLevelupMessage(interaction, supabase, guildId, config) {
|
||||||
|
const message = interaction.options.getString('message');
|
||||||
|
config.levelup_message = message;
|
||||||
|
|
||||||
|
const saved = await saveXpConfig(supabase, guildId, config);
|
||||||
|
if (!saved) {
|
||||||
|
return interaction.reply({
|
||||||
|
content: '❌ Failed to save settings. Please try again.',
|
||||||
|
ephemeral: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const preview = message
|
||||||
|
.replace(/{user}/g, interaction.user.toString())
|
||||||
|
.replace(/{username}/g, interaction.user.username)
|
||||||
|
.replace(/{level}/g, '10')
|
||||||
|
.replace(/{xp}/g, '10,000')
|
||||||
|
.replace(/{server}/g, interaction.guild.name);
|
||||||
|
|
||||||
|
return interaction.reply({
|
||||||
|
content: `✅ Level-up message updated!\n\n**Preview:**\n${preview}`,
|
||||||
|
ephemeral: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleLevelupDm(interaction, supabase, guildId, config) {
|
||||||
|
const enabled = interaction.options.getBoolean('enabled');
|
||||||
|
config.levelup_dm = 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
|
||||||
|
? '✅ Level-up notifications will now be sent via **DM** to users.'
|
||||||
|
: '✅ Level-up notifications will now be sent in the **channel**.',
|
||||||
|
ephemeral: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleLevelupEmbed(interaction, supabase, guildId, config) {
|
||||||
|
const enabled = interaction.options.getBoolean('enabled');
|
||||||
|
const color = interaction.options.getString('color');
|
||||||
|
|
||||||
|
config.levelup_embed = enabled;
|
||||||
|
|
||||||
|
if (color) {
|
||||||
|
if (!/^#[0-9A-Fa-f]{6}$/.test(color)) {
|
||||||
|
return interaction.reply({
|
||||||
|
content: '❌ Invalid color format. Please use hex format like `#5865F2`.',
|
||||||
|
ephemeral: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
config.levelup_embed_color = color;
|
||||||
|
}
|
||||||
|
|
||||||
|
const saved = await saveXpConfig(supabase, guildId, config);
|
||||||
|
if (!saved) {
|
||||||
|
return interaction.reply({
|
||||||
|
content: '❌ Failed to save settings. Please try again.',
|
||||||
|
ephemeral: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (enabled) {
|
||||||
|
return interaction.reply({
|
||||||
|
content: `✅ Level-up announcements will now use **embeds** with color \`${config.levelup_embed_color}\`.`,
|
||||||
|
ephemeral: true
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return interaction.reply({
|
||||||
|
content: '✅ Level-up announcements will now use **plain text** messages.',
|
||||||
|
ephemeral: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleLevelupReset(interaction, supabase, guildId, config) {
|
||||||
|
config.levelup_channel_id = null;
|
||||||
|
config.levelup_message = '🎉 Congratulations {user}! You reached **Level {level}**!';
|
||||||
|
config.levelup_dm = false;
|
||||||
|
config.levelup_embed = false;
|
||||||
|
config.levelup_embed_color = '#5865F2';
|
||||||
|
|
||||||
|
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: '✅ Level-up announcement settings have been reset to defaults.',
|
||||||
|
ephemeral: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
const { EmbedBuilder } = require('discord.js');
|
||||||
|
|
||||||
const xpCooldowns = new Map();
|
const xpCooldowns = new Map();
|
||||||
const xpConfigCache = new Map();
|
const xpConfigCache = new Map();
|
||||||
const CACHE_TTL = 60000; // 1 minute cache
|
const CACHE_TTL = 60000; // 1 minute cache
|
||||||
|
|
@ -102,20 +104,7 @@ module.exports = {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newLevel > oldLevel) {
|
if (newLevel > oldLevel) {
|
||||||
const serverConfig = client.serverConfigs?.get(guildId);
|
await sendLevelUpAnnouncement(message, newLevel, newXp, config, client);
|
||||||
const levelUpChannelId = serverConfig?.level_up_channel;
|
|
||||||
|
|
||||||
const levelUpMessage = `🎉 Congratulations ${message.author}! You reached **Level ${newLevel}**!`;
|
|
||||||
|
|
||||||
if (levelUpChannelId) {
|
|
||||||
const channel = await client.channels.fetch(levelUpChannelId).catch(() => null);
|
|
||||||
if (channel) {
|
|
||||||
await channel.send(levelUpMessage);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
await message.channel.send(levelUpMessage).catch(() => {});
|
|
||||||
}
|
|
||||||
|
|
||||||
await checkLevelRoles(message.member, newLevel, supabase, guildId);
|
await checkLevelRoles(message.member, newLevel, supabase, guildId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -125,6 +114,55 @@ module.exports = {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
async function sendLevelUpAnnouncement(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 = config.levelup_embed_color || '#5865F2';
|
||||||
|
|
||||||
|
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 }))
|
||||||
|
.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('Level-up announcement error:', error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function getXpConfig(supabase, guildId) {
|
async function getXpConfig(supabase, guildId) {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const cached = xpConfigCache.get(guildId);
|
const cached = xpConfigCache.get(guildId);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue