From 0241e09e806c6f9e2b16f6475712f80cab22d31f Mon Sep 17 00:00:00 2001 From: sirpiglr <49359077-sirpiglr@users.noreply.replit.com> Date: Sat, 13 Dec 2025 00:45:03 +0000 Subject: [PATCH] Add welcome card customization and seasonal event features Introduces customizable welcome cards using the canvas library and enhances the XP tracking system to incorporate active seasonal events with multipliers and bonus XP. Also adds presets and custom creation for seasonal events. Replit-Commit-Author: Agent Replit-Commit-Session-Id: aed2e46d-25bb-4b73-81a1-bb9e8437c261 Replit-Commit-Checkpoint-Type: intermediate_checkpoint Replit-Commit-Event-Id: 6d6bb71e-5e8e-4841-9c13-fd5e76c7d9e6 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/3bdfff67-975a-46ad-9845-fbb6b4a4c4b5/aed2e46d-25bb-4b73-81a1-bb9e8437c261/auRwRay Replit-Helium-Checkpoint-Created: true --- aethex-bot/commands/seasonal.js | 404 ++++++++++++++++++++++++++++ aethex-bot/commands/welcome-card.js | 221 +++++++++++++++ aethex-bot/listeners/welcome.js | 53 +++- aethex-bot/listeners/xpTracker.js | 27 ++ aethex-bot/utils/seasonalEvents.js | 72 +++++ aethex-bot/utils/welcomeCard.js | 126 +++++++++ package-lock.json | 402 +++++++++++++++++++++++++++ package.json | 1 + replit.nix | 1 + 9 files changed, 1306 insertions(+), 1 deletion(-) create mode 100644 aethex-bot/commands/seasonal.js create mode 100644 aethex-bot/commands/welcome-card.js create mode 100644 aethex-bot/utils/seasonalEvents.js create mode 100644 aethex-bot/utils/welcomeCard.js diff --git a/aethex-bot/commands/seasonal.js b/aethex-bot/commands/seasonal.js new file mode 100644 index 0000000..c57d343 --- /dev/null +++ b/aethex-bot/commands/seasonal.js @@ -0,0 +1,404 @@ +const { SlashCommandBuilder, EmbedBuilder, PermissionFlagsBits } = require('discord.js'); + +const PRESET_EVENTS = { + halloween: { + name: 'Halloween Spooktacular', + description: 'Spooky season is here! Earn bonus XP and unlock exclusive Halloween rewards.', + emoji: 'šŸŽƒ', + color: 0xff6600, + xp_multiplier: 1.5, + month: 10, + start_day: 15, + end_day: 31 + }, + christmas: { + name: 'Winter Wonderland', + description: 'Celebrate the holidays with festive rewards and XP bonuses!', + emoji: 'šŸŽ„', + color: 0x00a651, + xp_multiplier: 2.0, + month: 12, + start_day: 15, + end_day: 31 + }, + newyear: { + name: 'New Year Celebration', + description: 'Ring in the new year with special bonuses!', + emoji: 'šŸŽ†', + color: 0xffd700, + xp_multiplier: 2.0, + month: 1, + start_day: 1, + end_day: 7 + }, + valentine: { + name: 'Valentine\'s Day', + description: 'Spread the love with bonus XP for reactions and kind messages!', + emoji: 'šŸ’•', + color: 0xff69b4, + xp_multiplier: 1.5, + month: 2, + start_day: 10, + end_day: 14 + }, + easter: { + name: 'Easter Egg Hunt', + description: 'Hunt for hidden eggs and earn bonus rewards!', + emoji: '🐰', + color: 0x87ceeb, + xp_multiplier: 1.5, + month: 4, + start_day: 1, + end_day: 15 + }, + summer: { + name: 'Summer Bash', + description: 'Beat the heat with cool rewards and XP boosts!', + emoji: 'ā˜€ļø', + color: 0xffa500, + xp_multiplier: 1.25, + month: 7, + start_day: 1, + end_day: 31 + }, + anniversary: { + name: 'Server Anniversary', + description: 'Celebrate your server\'s special day!', + emoji: 'šŸŽ‚', + color: 0x9b59b6, + xp_multiplier: 2.0, + month: null, + start_day: null, + end_day: null + } +}; + +module.exports = { + data: new SlashCommandBuilder() + .setName('seasonal') + .setDescription('Manage seasonal events and XP bonuses') + .setDefaultMemberPermissions(PermissionFlagsBits.ManageGuild) + .addSubcommand(sub => sub.setName('active').setDescription('View currently active seasonal events')) + .addSubcommand(sub => sub.setName('list').setDescription('List all configured seasonal events')) + .addSubcommand(sub => sub.setName('create').setDescription('Create a custom seasonal event') + .addStringOption(opt => opt.setName('name').setDescription('Event name').setRequired(true).setMaxLength(50)) + .addStringOption(opt => opt.setName('description').setDescription('Event description').setRequired(true).setMaxLength(200)) + .addStringOption(opt => opt.setName('start').setDescription('Start date (YYYY-MM-DD)').setRequired(true)) + .addStringOption(opt => opt.setName('end').setDescription('End date (YYYY-MM-DD)').setRequired(true)) + .addNumberOption(opt => opt.setName('multiplier').setDescription('XP multiplier (e.g., 1.5)').setMinValue(1).setMaxValue(5)) + .addIntegerOption(opt => opt.setName('bonus_xp').setDescription('Bonus XP per message during event')) + .addStringOption(opt => opt.setName('emoji').setDescription('Event emoji').setMaxLength(10))) + .addSubcommand(sub => sub.setName('preset').setDescription('Enable a preset seasonal event') + .addStringOption(opt => opt.setName('event').setDescription('Preset event to enable') + .addChoices( + { name: 'šŸŽƒ Halloween', value: 'halloween' }, + { name: 'šŸŽ„ Christmas', value: 'christmas' }, + { name: 'šŸŽ† New Year', value: 'newyear' }, + { name: 'šŸ’• Valentine\'s Day', value: 'valentine' }, + { name: '🐰 Easter', value: 'easter' }, + { name: 'ā˜€ļø Summer Bash', value: 'summer' }, + { name: 'šŸŽ‚ Anniversary', value: 'anniversary' } + ).setRequired(true)) + .addIntegerOption(opt => opt.setName('year').setDescription('Year for the event (defaults to current/next)')) + .addStringOption(opt => opt.setName('start').setDescription('Custom start date for anniversary (YYYY-MM-DD)'))) + .addSubcommand(sub => sub.setName('delete').setDescription('Delete a seasonal event') + .addStringOption(opt => opt.setName('name').setDescription('Event name to delete').setRequired(true).setAutocomplete(true))) + .addSubcommand(sub => sub.setName('announce').setDescription('Announce an active event to a channel') + .addChannelOption(opt => opt.setName('channel').setDescription('Channel to announce in').setRequired(true))), + + async execute(interaction, supabase) { + if (!supabase) { + return interaction.reply({ content: 'Database not available.', ephemeral: true }); + } + + const subcommand = interaction.options.getSubcommand(); + + if (subcommand === 'active') { + const now = new Date(); + const { data: events } = await supabase + .from('seasonal_events') + .select('*') + .eq('guild_id', interaction.guildId) + .lte('start_date', now.toISOString()) + .gte('end_date', now.toISOString()) + .order('start_date', { ascending: true }); + + if (!events || events.length === 0) { + const embed = new EmbedBuilder() + .setColor(0x7c3aed) + .setTitle('šŸŽ‰ Active Seasonal Events') + .setDescription('No seasonal events are currently active.\nUse `/seasonal create` or `/seasonal preset` to set one up!') + .setTimestamp(); + return interaction.reply({ embeds: [embed] }); + } + + const embed = new EmbedBuilder() + .setColor(0x22c55e) + .setTitle('šŸŽ‰ Active Seasonal Events') + .setTimestamp(); + + for (const event of events) { + const endDate = new Date(event.end_date); + const daysLeft = Math.ceil((endDate - now) / (1000 * 60 * 60 * 24)); + + embed.addFields({ + name: `${event.emoji || 'šŸŽŠ'} ${event.name}`, + value: `${event.description}\n**XP Multiplier:** ${event.xp_multiplier}x\n**Bonus XP:** ${event.bonus_xp || 0}/message\n**Ends:** (${daysLeft} days left)`, + inline: false + }); + } + + await interaction.reply({ embeds: [embed] }); + } + + if (subcommand === 'list') { + const { data: events } = await supabase + .from('seasonal_events') + .select('*') + .eq('guild_id', interaction.guildId) + .order('start_date', { ascending: true }); + + if (!events || events.length === 0) { + const embed = new EmbedBuilder() + .setColor(0x7c3aed) + .setTitle('šŸ“… Seasonal Events') + .setDescription('No seasonal events configured.\nUse `/seasonal create` or `/seasonal preset` to add one!') + .setTimestamp(); + return interaction.reply({ embeds: [embed] }); + } + + const now = new Date(); + const embed = new EmbedBuilder() + .setColor(0x7c3aed) + .setTitle('šŸ“… Seasonal Events') + .setFooter({ text: `${events.length} event(s) configured` }) + .setTimestamp(); + + for (const event of events) { + const startDate = new Date(event.start_date); + const endDate = new Date(event.end_date); + const isActive = now >= startDate && now <= endDate; + const isPast = now > endDate; + const status = isActive ? '🟢 Active' : isPast ? '⚫ Ended' : '🟔 Upcoming'; + + embed.addFields({ + name: `${event.emoji || 'šŸŽŠ'} ${event.name} ${status}`, + value: `${event.description}\n**Period:** - \n**XP:** ${event.xp_multiplier}x | **Bonus XP:** +${event.bonus_xp || 0}`, + inline: false + }); + } + + await interaction.reply({ embeds: [embed] }); + } + + if (subcommand === 'create') { + const name = interaction.options.getString('name'); + const description = interaction.options.getString('description'); + const startStr = interaction.options.getString('start'); + const endStr = interaction.options.getString('end'); + const multiplier = interaction.options.getNumber('multiplier') || 1.5; + const bonusXp = interaction.options.getInteger('bonus_xp') || 0; + const emoji = interaction.options.getString('emoji') || 'šŸŽŠ'; + + const startDate = new Date(startStr); + startDate.setHours(0, 0, 0, 0); + const endDate = new Date(endStr); + endDate.setHours(23, 59, 59, 999); + + if (isNaN(startDate.getTime()) || isNaN(endDate.getTime())) { + return interaction.reply({ content: 'Invalid date format. Use YYYY-MM-DD.', ephemeral: true }); + } + + if (endDate <= startDate) { + return interaction.reply({ content: 'End date must be after start date.', ephemeral: true }); + } + + const { count } = await supabase + .from('seasonal_events') + .select('*', { count: 'exact', head: true }) + .eq('guild_id', interaction.guildId); + + if (count >= 20) { + return interaction.reply({ content: 'Maximum of 20 seasonal events per server.', ephemeral: true }); + } + + const { error } = await supabase.from('seasonal_events').insert({ + guild_id: interaction.guildId, + name, + description, + emoji, + start_date: startDate.toISOString(), + end_date: endDate.toISOString(), + xp_multiplier: multiplier, + bonus_xp: bonusXp, + created_by: interaction.user.id + }); + + if (error) { + console.error('Seasonal event create error:', error); + return interaction.reply({ content: 'Failed to create event.', ephemeral: true }); + } + + const embed = new EmbedBuilder() + .setColor(0x22c55e) + .setTitle(`${emoji} Seasonal Event Created!`) + .setDescription(`**${name}**\n${description}`) + .addFields( + { name: 'Start', value: ``, inline: true }, + { name: 'End', value: ``, inline: true }, + { name: 'XP Multiplier', value: `${multiplier}x`, inline: true }, + { name: 'Bonus XP', value: `+${bonusXp}/message`, inline: true } + ) + .setTimestamp(); + + await interaction.reply({ embeds: [embed] }); + } + + if (subcommand === 'preset') { + const presetKey = interaction.options.getString('event'); + const preset = PRESET_EVENTS[presetKey]; + const year = interaction.options.getInteger('year') || new Date().getFullYear(); + const customStart = interaction.options.getString('start'); + + let startDate, endDate; + + if (presetKey === 'anniversary' && customStart) { + startDate = new Date(customStart); + endDate = new Date(startDate); + endDate.setDate(endDate.getDate() + 7); + } else if (preset.month) { + startDate = new Date(year, preset.month - 1, preset.start_day); + endDate = new Date(year, preset.month - 1, preset.end_day, 23, 59, 59); + + if (startDate < new Date() && presetKey !== 'newyear') { + startDate.setFullYear(startDate.getFullYear() + 1); + endDate.setFullYear(endDate.getFullYear() + 1); + } + } else { + return interaction.reply({ content: 'Anniversary events require a custom start date.', ephemeral: true }); + } + + const { error } = await supabase.from('seasonal_events').insert({ + guild_id: interaction.guildId, + name: preset.name, + description: preset.description, + emoji: preset.emoji, + start_date: startDate.toISOString(), + end_date: endDate.toISOString(), + xp_multiplier: preset.xp_multiplier, + bonus_xp: 0, + preset_type: presetKey, + created_by: interaction.user.id + }); + + if (error) { + if (error.code === '23505') { + return interaction.reply({ content: 'This preset event already exists for that time period.', ephemeral: true }); + } + console.error('Preset event error:', error); + return interaction.reply({ content: 'Failed to create preset event.', ephemeral: true }); + } + + const embed = new EmbedBuilder() + .setColor(preset.color) + .setTitle(`${preset.emoji} ${preset.name} Enabled!`) + .setDescription(preset.description) + .addFields( + { name: 'Start', value: ``, inline: true }, + { name: 'End', value: ``, inline: true }, + { name: 'XP Multiplier', value: `${preset.xp_multiplier}x`, inline: true } + ) + .setTimestamp(); + + await interaction.reply({ embeds: [embed] }); + } + + if (subcommand === 'delete') { + const eventName = interaction.options.getString('name'); + + const { data: event } = await supabase + .from('seasonal_events') + .select('id, name, emoji') + .eq('guild_id', interaction.guildId) + .ilike('name', `%${eventName}%`) + .maybeSingle(); + + if (!event) { + return interaction.reply({ content: `No event found matching "${eventName}".`, ephemeral: true }); + } + + const { error } = await supabase + .from('seasonal_events') + .delete() + .eq('id', event.id); + + if (error) { + return interaction.reply({ content: 'Failed to delete event.', ephemeral: true }); + } + + const embed = new EmbedBuilder() + .setColor(0xef4444) + .setTitle('Event Deleted') + .setDescription(`${event.emoji || 'šŸŽŠ'} **${event.name}** has been removed.`) + .setTimestamp(); + + await interaction.reply({ embeds: [embed] }); + } + + if (subcommand === 'announce') { + const channel = interaction.options.getChannel('channel'); + const now = new Date(); + + const { data: events } = await supabase + .from('seasonal_events') + .select('*') + .eq('guild_id', interaction.guildId) + .lte('start_date', now.toISOString()) + .gte('end_date', now.toISOString()); + + if (!events || events.length === 0) { + return interaction.reply({ content: 'No active events to announce.', ephemeral: true }); + } + + const embed = new EmbedBuilder() + .setColor(0x7c3aed) + .setTitle('šŸŽ‰ Seasonal Event Active!') + .setDescription('Special bonuses are now available!') + .setTimestamp(); + + for (const event of events) { + const endDate = new Date(event.end_date); + embed.addFields({ + name: `${event.emoji || 'šŸŽŠ'} ${event.name}`, + value: `${event.description}\n\n**Bonuses:**\n• ${event.xp_multiplier}x XP multiplier\n${event.bonus_xp > 0 ? `• +${event.bonus_xp} XP per message\n` : ''}• Ends: `, + inline: false + }); + } + + embed.setFooter({ text: `${events.length} event(s) active` }); + + try { + await channel.send({ embeds: [embed] }); + await interaction.reply({ content: `Event announcement sent to ${channel}!`, ephemeral: true }); + } catch (err) { + await interaction.reply({ content: 'Failed to send announcement. Check bot permissions.', ephemeral: true }); + } + } + }, + + async autocomplete(interaction, supabase) { + if (!supabase) return; + + const focusedValue = interaction.options.getFocused(); + + const { data: events } = await supabase + .from('seasonal_events') + .select('name') + .eq('guild_id', interaction.guildId) + .ilike('name', `%${focusedValue}%`) + .limit(25); + + const choices = (events || []).map(e => ({ name: e.name, value: e.name })); + await interaction.respond(choices); + } +}; diff --git a/aethex-bot/commands/welcome-card.js b/aethex-bot/commands/welcome-card.js new file mode 100644 index 0000000..39b9148 --- /dev/null +++ b/aethex-bot/commands/welcome-card.js @@ -0,0 +1,221 @@ +const { SlashCommandBuilder, EmbedBuilder, PermissionFlagsBits, AttachmentBuilder } = require('discord.js'); +const { generateWelcomeCard } = require('../utils/welcomeCard'); + +module.exports = { + data: new SlashCommandBuilder() + .setName('welcome-card') + .setDescription('Configure custom welcome image cards') + .setDefaultMemberPermissions(PermissionFlagsBits.ManageGuild) + .addSubcommand(sub => sub.setName('enable').setDescription('Enable welcome image cards')) + .addSubcommand(sub => sub.setName('disable').setDescription('Disable welcome image cards (use text only)')) + .addSubcommand(sub => sub.setName('preview').setDescription('Preview the welcome card for yourself')) + .addSubcommand(sub => sub.setName('settings').setDescription('View current welcome card settings')) + .addSubcommand(sub => sub.setName('title').setDescription('Set the welcome title text') + .addStringOption(opt => opt.setName('text').setDescription('Title text (e.g., WELCOME, HELLO, etc.)').setRequired(true).setMaxLength(20))) + .addSubcommand(sub => sub.setName('colors').setDescription('Customize card colors') + .addStringOption(opt => opt.setName('background').setDescription('Background color (hex, e.g., #1a1a2e)')) + .addStringOption(opt => opt.setName('accent').setDescription('Accent color (hex, e.g., #7c3aed)')) + .addStringOption(opt => opt.setName('text').setDescription('Text color (hex, e.g., #ffffff)')) + .addStringOption(opt => opt.setName('subtext').setDescription('Subtext color (hex, e.g., #a0a0a0)'))), + + async execute(interaction, supabase) { + if (!supabase) { + return interaction.reply({ content: 'Database not available.', ephemeral: true }); + } + + const subcommand = interaction.options.getSubcommand(); + + const { data: config } = await supabase + .from('server_config') + .select('welcome_card_enabled, welcome_card_title, welcome_card_bg_color, welcome_card_accent_color, welcome_card_text_color, welcome_card_subtext_color') + .eq('guild_id', interaction.guildId) + .maybeSingle(); + + const safeConfig = config || {}; + + if (subcommand === 'enable') { + const { error } = await supabase + .from('server_config') + .upsert({ + guild_id: interaction.guildId, + welcome_card_enabled: true, + updated_at: new Date().toISOString() + }, { onConflict: 'guild_id' }); + + if (error) { + console.error('Welcome card enable error:', error); + return interaction.reply({ content: 'Failed to enable welcome cards.', ephemeral: true }); + } + + const embed = new EmbedBuilder() + .setColor(0x22c55e) + .setTitle('Welcome Cards Enabled') + .setDescription('New members will now receive custom welcome image cards!') + .setFooter({ text: 'Use /welcome-card preview to see how it looks' }) + .setTimestamp(); + + await interaction.reply({ embeds: [embed] }); + } + + if (subcommand === 'disable') { + const { error } = await supabase + .from('server_config') + .upsert({ + guild_id: interaction.guildId, + welcome_card_enabled: false, + updated_at: new Date().toISOString() + }, { onConflict: 'guild_id' }); + + if (error) { + return interaction.reply({ content: 'Failed to disable welcome cards.', ephemeral: true }); + } + + const embed = new EmbedBuilder() + .setColor(0xef4444) + .setTitle('Welcome Cards Disabled') + .setDescription('Welcome messages will now be text-only embeds.') + .setTimestamp(); + + await interaction.reply({ embeds: [embed] }); + } + + if (subcommand === 'preview') { + await interaction.deferReply(); + + try { + const cardConfig = { + title: safeConfig.welcome_card_title || 'WELCOME', + background_color: safeConfig.welcome_card_bg_color || '#1a1a2e', + accent_color: safeConfig.welcome_card_accent_color || '#7c3aed', + text_color: safeConfig.welcome_card_text_color || '#ffffff', + subtext_color: safeConfig.welcome_card_subtext_color || '#a0a0a0' + }; + + const buffer = await generateWelcomeCard(interaction.member, cardConfig); + const attachment = new AttachmentBuilder(buffer, { name: 'welcome-preview.png' }); + + const embed = new EmbedBuilder() + .setColor(0x7c3aed) + .setTitle('Welcome Card Preview') + .setDescription('This is how welcome cards will look for new members.') + .setImage('attachment://welcome-preview.png') + .setTimestamp(); + + await interaction.editReply({ embeds: [embed], files: [attachment] }); + } catch (err) { + console.error('Welcome card preview error:', err); + await interaction.editReply({ content: 'Failed to generate preview. Please try again.' }); + } + } + + if (subcommand === 'settings') { + const embed = new EmbedBuilder() + .setColor(0x7c3aed) + .setTitle('Welcome Card Settings') + .addFields( + { name: 'Enabled', value: safeConfig.welcome_card_enabled ? 'āœ… Yes' : 'āŒ No', inline: true }, + { name: 'Title', value: safeConfig.welcome_card_title || 'WELCOME', inline: true }, + { name: '\u200B', value: '\u200B', inline: true }, + { name: 'Background Color', value: safeConfig.welcome_card_bg_color || '#1a1a2e', inline: true }, + { name: 'Accent Color', value: safeConfig.welcome_card_accent_color || '#7c3aed', inline: true }, + { name: 'Text Color', value: safeConfig.welcome_card_text_color || '#ffffff', inline: true }, + { name: 'Subtext Color', value: safeConfig.welcome_card_subtext_color || '#a0a0a0', inline: true } + ) + .setFooter({ text: 'Use /welcome-card commands to customize' }) + .setTimestamp(); + + await interaction.reply({ embeds: [embed] }); + } + + if (subcommand === 'title') { + const titleText = interaction.options.getString('text').toUpperCase(); + + const { error } = await supabase + .from('server_config') + .upsert({ + guild_id: interaction.guildId, + welcome_card_title: titleText, + updated_at: new Date().toISOString() + }, { onConflict: 'guild_id' }); + + if (error) { + return interaction.reply({ content: 'Failed to update title.', ephemeral: true }); + } + + const embed = new EmbedBuilder() + .setColor(0x22c55e) + .setTitle('Title Updated') + .setDescription(`Welcome card title set to: **${titleText}**`) + .setTimestamp(); + + await interaction.reply({ embeds: [embed] }); + } + + if (subcommand === 'colors') { + const bgColor = interaction.options.getString('background'); + const accentColor = interaction.options.getString('accent'); + const textColor = interaction.options.getString('text'); + const subtextColor = interaction.options.getString('subtext'); + + if (!bgColor && !accentColor && !textColor && !subtextColor) { + return interaction.reply({ content: 'Please provide at least one color to update.', ephemeral: true }); + } + + const hexRegex = /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/; + const updates = { updated_at: new Date().toISOString() }; + const changes = []; + + if (bgColor) { + if (!hexRegex.test(bgColor)) { + return interaction.reply({ content: 'Invalid background color format. Use hex (e.g., #1a1a2e)', ephemeral: true }); + } + updates.welcome_card_bg_color = bgColor; + changes.push(`Background: ${bgColor}`); + } + + if (accentColor) { + if (!hexRegex.test(accentColor)) { + return interaction.reply({ content: 'Invalid accent color format. Use hex (e.g., #7c3aed)', ephemeral: true }); + } + updates.welcome_card_accent_color = accentColor; + changes.push(`Accent: ${accentColor}`); + } + + if (textColor) { + if (!hexRegex.test(textColor)) { + return interaction.reply({ content: 'Invalid text color format. Use hex (e.g., #ffffff)', ephemeral: true }); + } + updates.welcome_card_text_color = textColor; + changes.push(`Text: ${textColor}`); + } + + if (subtextColor) { + if (!hexRegex.test(subtextColor)) { + return interaction.reply({ content: 'Invalid subtext color format. Use hex (e.g., #a0a0a0)', ephemeral: true }); + } + updates.welcome_card_subtext_color = subtextColor; + changes.push(`Subtext: ${subtextColor}`); + } + + const { error } = await supabase + .from('server_config') + .upsert({ + guild_id: interaction.guildId, + ...updates + }, { onConflict: 'guild_id' }); + + if (error) { + return interaction.reply({ content: 'Failed to update colors.', ephemeral: true }); + } + + const embed = new EmbedBuilder() + .setColor(parseInt((accentColor || safeConfig.welcome_card_accent_color || '#7c3aed').replace('#', ''), 16)) + .setTitle('Colors Updated') + .setDescription(`Updated:\n${changes.join('\n')}`) + .setFooter({ text: 'Use /welcome-card preview to see changes' }) + .setTimestamp(); + + await interaction.reply({ embeds: [embed] }); + } + }, +}; diff --git a/aethex-bot/listeners/welcome.js b/aethex-bot/listeners/welcome.js index 86955d4..92bde63 100644 --- a/aethex-bot/listeners/welcome.js +++ b/aethex-bot/listeners/welcome.js @@ -1,4 +1,5 @@ const { EmbedBuilder, AttachmentBuilder } = require('discord.js'); +const { generateWelcomeCard } = require('../utils/welcomeCard'); module.exports = { name: 'guildMemberAdd', @@ -13,7 +14,7 @@ module.exports = { try { const { data: config } = await supabase .from('server_config') - .select('welcome_channel, auto_role') + .select('welcome_channel, auto_role, welcome_card_enabled, welcome_card_title, welcome_card_bg_color, welcome_card_accent_color, welcome_card_text_color, welcome_card_subtext_color') .eq('guild_id', member.guild.id) .single(); @@ -33,6 +34,56 @@ module.exports = { const accountAge = getAccountAge(member.user.createdTimestamp); const isNewAccount = (Date.now() - member.user.createdTimestamp) < 7 * 24 * 60 * 60 * 1000; + if (config.welcome_card_enabled) { + try { + const cardConfig = { + title: config.welcome_card_title || 'WELCOME', + background_color: config.welcome_card_bg_color || '#1a1a2e', + accent_color: config.welcome_card_accent_color || '#7c3aed', + text_color: config.welcome_card_text_color || '#ffffff', + subtext_color: config.welcome_card_subtext_color || '#a0a0a0' + }; + + const buffer = await generateWelcomeCard(member, cardConfig); + const attachment = new AttachmentBuilder(buffer, { name: 'welcome.png' }); + + const welcomeMessages = [ + `Welcome to **${member.guild.name}**, ${member}!`, + `Hey ${member}, welcome to **${member.guild.name}**!`, + `${member} just joined! Welcome aboard!`, + `A wild ${member} appeared! Welcome!`, + `${member} has arrived! Let's go!`, + ]; + const randomMessage = welcomeMessages[Math.floor(Math.random() * welcomeMessages.length)]; + + const embed = new EmbedBuilder() + .setColor(parseInt((config.welcome_card_accent_color || '#7c3aed').replace('#', ''), 16)) + .setDescription(randomMessage) + .setImage('attachment://welcome.png'); + + if (isNewAccount) { + embed.addFields({ + name: 'āš ļø Notice', + value: 'This is a new account (created within the last 7 days)', + inline: false + }); + } + + if (member.guild.rulesChannel) { + embed.addFields({ + name: 'šŸ“œ Get Started', + value: `Check out the rules in ${member.guild.rulesChannel}`, + inline: false + }); + } + + await channel.send({ embeds: [embed], files: [attachment] }); + return; + } catch (cardError) { + console.error('Welcome card generation failed, falling back to text:', cardError.message); + } + } + const welcomeMessages = [ `Welcome to **${member.guild.name}**, ${member}! We're glad you're here!`, `Hey ${member}, welcome to **${member.guild.name}**! Make yourself at home!`, diff --git a/aethex-bot/listeners/xpTracker.js b/aethex-bot/listeners/xpTracker.js index 01cd3af..a89f093 100644 --- a/aethex-bot/listeners/xpTracker.js +++ b/aethex-bot/listeners/xpTracker.js @@ -2,6 +2,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 { getActiveSeasonalEvents, getSeasonalMultiplier, getSeasonalBonusCoins } = require('../utils/seasonalEvents'); const xpCooldowns = new Map(); const xpConfigCache = new Map(); @@ -165,6 +166,19 @@ module.exports = { xpGain = Math.floor(xpGain * prestigeBonus); } + // Apply seasonal event multiplier + const seasonalEvents = await getActiveSeasonalEvents(supabase, guildId); + const seasonalMultiplier = getSeasonalMultiplier(seasonalEvents); + if (seasonalMultiplier > 1) { + xpGain = Math.floor(xpGain * seasonalMultiplier); + } + + // Add seasonal bonus XP (converted from coins concept since XP is the currency) + const bonusXp = getSeasonalBonusCoins(seasonalEvents); + if (bonusXp > 0) { + xpGain += bonusXp; + } + const currentXp = profile.xp || 0; const newXp = currentXp + xpGain; @@ -593,6 +607,19 @@ async function handleStandaloneXp(message, client, supabase, config, discordUser xpGain = Math.floor(xpGain * prestigeBonus); } + // Apply seasonal event multiplier + const seasonalEvents = await getActiveSeasonalEvents(supabase, guildId); + const seasonalMultiplier = getSeasonalMultiplier(seasonalEvents); + if (seasonalMultiplier > 1) { + xpGain = Math.floor(xpGain * seasonalMultiplier); + } + + // Add seasonal bonus XP (converted from coins concept since XP is the currency) + const bonusXp = getSeasonalBonusCoins(seasonalEvents); + if (bonusXp > 0) { + xpGain += bonusXp; + } + const currentXp = existingXp?.xp || 0; const newXp = currentXp + xpGain; diff --git a/aethex-bot/utils/seasonalEvents.js b/aethex-bot/utils/seasonalEvents.js new file mode 100644 index 0000000..bc26c12 --- /dev/null +++ b/aethex-bot/utils/seasonalEvents.js @@ -0,0 +1,72 @@ +const seasonalEventCache = new Map(); +const CACHE_TTL = 60000; + +async function getActiveSeasonalEvents(supabase, guildId) { + const now = Date.now(); + const cacheKey = guildId; + const cached = seasonalEventCache.get(cacheKey); + + if (cached && (now - cached.timestamp < CACHE_TTL)) { + return cached.events; + } + + try { + const currentTime = new Date().toISOString(); + const { data: events, error } = await supabase + .from('seasonal_events') + .select('*') + .eq('guild_id', guildId) + .lte('start_date', currentTime) + .gte('end_date', currentTime); + + if (error) { + seasonalEventCache.set(cacheKey, { events: [], timestamp: now }); + return []; + } + + const activeEvents = events || []; + seasonalEventCache.set(cacheKey, { events: activeEvents, timestamp: now }); + return activeEvents; + } catch (e) { + return []; + } +} + +function getSeasonalMultiplier(events) { + if (!events || events.length === 0) return 1; + + let highestMultiplier = 1; + for (const event of events) { + if (event.xp_multiplier && event.xp_multiplier > highestMultiplier) { + highestMultiplier = event.xp_multiplier; + } + } + return highestMultiplier; +} + +function getSeasonalBonusCoins(events) { + if (!events || events.length === 0) return 0; + + let totalBonus = 0; + for (const event of events) { + if (event.bonus_coins) { + totalBonus += event.bonus_coins; + } + } + return totalBonus; +} + +function clearSeasonalCache(guildId) { + if (guildId) { + seasonalEventCache.delete(guildId); + } else { + seasonalEventCache.clear(); + } +} + +module.exports = { + getActiveSeasonalEvents, + getSeasonalMultiplier, + getSeasonalBonusCoins, + clearSeasonalCache +}; diff --git a/aethex-bot/utils/welcomeCard.js b/aethex-bot/utils/welcomeCard.js new file mode 100644 index 0000000..3527e80 --- /dev/null +++ b/aethex-bot/utils/welcomeCard.js @@ -0,0 +1,126 @@ +const { createCanvas, loadImage, registerFont } = require('canvas'); +const path = require('path'); + +const CARD_WIDTH = 900; +const CARD_HEIGHT = 300; + +async function generateWelcomeCard(member, config = {}) { + const canvas = createCanvas(CARD_WIDTH, CARD_HEIGHT); + const ctx = canvas.getContext('2d'); + + const bgColor = config.background_color || '#1a1a2e'; + const accentColor = config.accent_color || '#7c3aed'; + const textColor = config.text_color || '#ffffff'; + const subtextColor = config.subtext_color || '#a0a0a0'; + + ctx.fillStyle = bgColor; + ctx.fillRect(0, 0, CARD_WIDTH, CARD_HEIGHT); + + const gradient = ctx.createLinearGradient(0, 0, CARD_WIDTH, 0); + gradient.addColorStop(0, hexToRgba(accentColor, 0.3)); + gradient.addColorStop(0.5, hexToRgba(accentColor, 0.1)); + gradient.addColorStop(1, 'transparent'); + ctx.fillStyle = gradient; + ctx.fillRect(0, 0, CARD_WIDTH, CARD_HEIGHT); + + ctx.strokeStyle = accentColor; + ctx.lineWidth = 4; + ctx.strokeRect(2, 2, CARD_WIDTH - 4, CARD_HEIGHT - 4); + + for (let i = 0; i < 50; i++) { + const x = Math.random() * CARD_WIDTH; + const y = Math.random() * CARD_HEIGHT; + const size = Math.random() * 2; + ctx.fillStyle = hexToRgba(accentColor, Math.random() * 0.3); + ctx.beginPath(); + ctx.arc(x, y, size, 0, Math.PI * 2); + ctx.fill(); + } + + const avatarSize = 160; + const avatarX = 70; + const avatarY = (CARD_HEIGHT - avatarSize) / 2; + + try { + const avatarURL = member.displayAvatarURL({ extension: 'png', size: 256 }); + const avatar = await loadImage(avatarURL); + + ctx.save(); + ctx.beginPath(); + ctx.arc(avatarX + avatarSize / 2, avatarY + avatarSize / 2, avatarSize / 2 + 4, 0, Math.PI * 2); + ctx.fillStyle = accentColor; + ctx.fill(); + ctx.closePath(); + + ctx.beginPath(); + ctx.arc(avatarX + avatarSize / 2, avatarY + avatarSize / 2, avatarSize / 2, 0, Math.PI * 2); + ctx.clip(); + ctx.drawImage(avatar, avatarX, avatarY, avatarSize, avatarSize); + ctx.restore(); + } catch (err) { + ctx.save(); + ctx.beginPath(); + ctx.arc(avatarX + avatarSize / 2, avatarY + avatarSize / 2, avatarSize / 2, 0, Math.PI * 2); + ctx.fillStyle = accentColor; + ctx.fill(); + ctx.restore(); + + ctx.fillStyle = textColor; + ctx.font = 'bold 60px Arial'; + ctx.textAlign = 'center'; + ctx.fillText(member.user.username[0].toUpperCase(), avatarX + avatarSize / 2, avatarY + avatarSize / 2 + 20); + } + + const textX = avatarX + avatarSize + 50; + + ctx.fillStyle = textColor; + ctx.font = 'bold 36px Arial'; + ctx.textAlign = 'left'; + const welcomeText = config.title || 'WELCOME'; + ctx.fillText(welcomeText, textX, 80); + + const username = member.user.username; + const maxUsernameWidth = CARD_WIDTH - textX - 50; + let fontSize = 48; + ctx.font = `bold ${fontSize}px Arial`; + while (ctx.measureText(username).width > maxUsernameWidth && fontSize > 24) { + fontSize -= 2; + ctx.font = `bold ${fontSize}px Arial`; + } + ctx.fillStyle = accentColor; + ctx.fillText(username, textX, 140); + + ctx.fillStyle = subtextColor; + ctx.font = '22px Arial'; + const serverName = member.guild.name; + ctx.fillText(`to ${serverName}`, textX, 175); + + const memberCount = member.guild.memberCount; + ctx.fillStyle = textColor; + ctx.font = 'bold 28px Arial'; + ctx.fillText(`Member #${memberCount.toLocaleString()}`, textX, 230); + + const joinDate = new Date().toLocaleDateString('en-US', { + month: 'short', + day: 'numeric', + year: 'numeric' + }); + ctx.fillStyle = subtextColor; + ctx.font = '20px Arial'; + ctx.fillText(`Joined: ${joinDate}`, textX, 265); + + return canvas.toBuffer('image/png'); +} + +function hexToRgba(hex, alpha) { + const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); + if (result) { + const r = parseInt(result[1], 16); + const g = parseInt(result[2], 16); + const b = parseInt(result[3], 16); + return `rgba(${r}, ${g}, ${b}, ${alpha})`; + } + return `rgba(124, 58, 237, ${alpha})`; +} + +module.exports = { generateWelcomeCard }; diff --git a/package-lock.json b/package-lock.json index dee0fcf..b02996f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "1.0.0", "license": "ISC", "dependencies": { + "canvas": "^3.2.0", "cookie-parser": "^1.4.7", "cors": "^2.8.5", "express": "^5.2.1", @@ -31,6 +32,37 @@ "node": ">= 0.6" } }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, "node_modules/body-parser": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.1.tgz", @@ -55,6 +87,30 @@ "url": "https://opencollective.com/express" } }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -93,6 +149,26 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/canvas": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/canvas/-/canvas-3.2.0.tgz", + "integrity": "sha512-jk0GxrLtUEmW/TmFsk2WghvgHe8B0pxGilqCL21y8lHkPUGa6FTsnCNtHPOzT8O3y+N+m3espawV80bbBlgfTA==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^7.0.0", + "prebuild-install": "^7.1.3" + }, + "engines": { + "node": "^18.12.0 || >= 20.9.0" + } + }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, "node_modules/content-disposition": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", @@ -173,6 +249,30 @@ } } }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -182,6 +282,15 @@ "node": ">= 0.8" } }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -211,6 +320,15 @@ "node": ">= 0.8" } }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -256,6 +374,15 @@ "node": ">= 0.6" } }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, "node_modules/express": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", @@ -387,6 +514,12 @@ "node": ">= 0.8" } }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -433,6 +566,12 @@ "node": ">= 0.4" } }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -505,12 +644,38 @@ "url": "https://opencollective.com/express" } }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -581,12 +746,45 @@ "url": "https://opencollective.com/express" } }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT" + }, "node_modules/negotiator": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", @@ -596,6 +794,24 @@ "node": ">= 0.6" } }, + "node_modules/node-abi": { + "version": "3.85.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.85.0.tgz", + "integrity": "sha512-zsFhmbkAzwhTft6nd3VxcG0cvJsT70rL+BIGHWVq5fi6MwGrHwzqKaxXE+Hl2GmnGItnDKPPkO5/LQqjVkIdFg==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "license": "MIT" + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -794,6 +1010,32 @@ "node": ">=0.10.0" } }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -807,6 +1049,16 @@ "node": ">= 0.10" } }, + "node_modules/pump": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", + "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, "node_modules/qs": { "version": "6.14.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", @@ -855,6 +1107,35 @@ "node": ">= 0.10" } }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/router": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", @@ -897,6 +1178,18 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, + "node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/send": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", @@ -1012,6 +1305,51 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, "node_modules/split2": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", @@ -1030,6 +1368,24 @@ "node": ">= 0.8" } }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/stripe": { "version": "20.0.0", "resolved": "https://registry.npmjs.org/stripe/-/stripe-20.0.0.tgz", @@ -1050,6 +1406,34 @@ } } }, + "node_modules/tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/toidentifier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", @@ -1059,6 +1443,18 @@ "node": ">=0.6" } }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, "node_modules/type-is": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", @@ -1094,6 +1490,12 @@ "node": ">= 0.8" } }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", diff --git a/package.json b/package.json index 52df247..0e40b7c 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "author": "", "license": "ISC", "dependencies": { + "canvas": "^3.2.0", "cookie-parser": "^1.4.7", "cors": "^2.8.5", "express": "^5.2.1", diff --git a/replit.nix b/replit.nix index 4250110..e66934e 100644 --- a/replit.nix +++ b/replit.nix @@ -1,5 +1,6 @@ { pkgs }: { deps = [ + pkgs.libuuid pkgs.dig pkgs.unzip ];