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 ];