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
This commit is contained in:
sirpiglr 2025-12-13 00:45:03 +00:00
parent 2b40769784
commit 0241e09e80
9 changed files with 1306 additions and 1 deletions

View file

@ -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:** <t:${Math.floor(endDate.getTime() / 1000)}:R> (${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:** <t:${Math.floor(startDate.getTime() / 1000)}:D> - <t:${Math.floor(endDate.getTime() / 1000)}:D>\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: `<t:${Math.floor(startDate.getTime() / 1000)}:F>`, inline: true },
{ name: 'End', value: `<t:${Math.floor(endDate.getTime() / 1000)}:F>`, 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: `<t:${Math.floor(startDate.getTime() / 1000)}:F>`, inline: true },
{ name: 'End', value: `<t:${Math.floor(endDate.getTime() / 1000)}:F>`, 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: <t:${Math.floor(endDate.getTime() / 1000)}:R>`,
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);
}
};

View file

@ -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] });
}
},
};

View file

@ -1,4 +1,5 @@
const { EmbedBuilder, AttachmentBuilder } = require('discord.js'); const { EmbedBuilder, AttachmentBuilder } = require('discord.js');
const { generateWelcomeCard } = require('../utils/welcomeCard');
module.exports = { module.exports = {
name: 'guildMemberAdd', name: 'guildMemberAdd',
@ -13,7 +14,7 @@ module.exports = {
try { try {
const { data: config } = await supabase const { data: config } = await supabase
.from('server_config') .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) .eq('guild_id', member.guild.id)
.single(); .single();
@ -33,6 +34,56 @@ module.exports = {
const accountAge = getAccountAge(member.user.createdTimestamp); const accountAge = getAccountAge(member.user.createdTimestamp);
const isNewAccount = (Date.now() - member.user.createdTimestamp) < 7 * 24 * 60 * 60 * 1000; 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 = [ const welcomeMessages = [
`Welcome to **${member.guild.name}**, ${member}! We're glad you're here!`, `Welcome to **${member.guild.name}**, ${member}! We're glad you're here!`,
`Hey ${member}, welcome to **${member.guild.name}**! Make yourself at home!`, `Hey ${member}, welcome to **${member.guild.name}**! Make yourself at home!`,

View file

@ -2,6 +2,7 @@ const { EmbedBuilder } = require('discord.js');
const { checkAchievements } = require('../commands/achievements'); const { checkAchievements } = require('../commands/achievements');
const { getServerMode, getEmbedColor } = require('../utils/modeHelper'); const { getServerMode, getEmbedColor } = require('../utils/modeHelper');
const { updateStandaloneXp, calculateLevel: standaloneCalcLevel } = require('../utils/standaloneXp'); const { updateStandaloneXp, calculateLevel: standaloneCalcLevel } = require('../utils/standaloneXp');
const { getActiveSeasonalEvents, getSeasonalMultiplier, getSeasonalBonusCoins } = require('../utils/seasonalEvents');
const xpCooldowns = new Map(); const xpCooldowns = new Map();
const xpConfigCache = new Map(); const xpConfigCache = new Map();
@ -165,6 +166,19 @@ module.exports = {
xpGain = Math.floor(xpGain * prestigeBonus); 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 currentXp = profile.xp || 0;
const newXp = currentXp + xpGain; const newXp = currentXp + xpGain;
@ -593,6 +607,19 @@ async function handleStandaloneXp(message, client, supabase, config, discordUser
xpGain = Math.floor(xpGain * prestigeBonus); 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 currentXp = existingXp?.xp || 0;
const newXp = currentXp + xpGain; const newXp = currentXp + xpGain;

View file

@ -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
};

View file

@ -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 };

402
package-lock.json generated
View file

@ -9,6 +9,7 @@
"version": "1.0.0", "version": "1.0.0",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"canvas": "^3.2.0",
"cookie-parser": "^1.4.7", "cookie-parser": "^1.4.7",
"cors": "^2.8.5", "cors": "^2.8.5",
"express": "^5.2.1", "express": "^5.2.1",
@ -31,6 +32,37 @@
"node": ">= 0.6" "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": { "node_modules/body-parser": {
"version": "2.2.1", "version": "2.2.1",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.1.tgz", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.1.tgz",
@ -55,6 +87,30 @@
"url": "https://opencollective.com/express" "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": { "node_modules/bytes": {
"version": "3.1.2", "version": "3.1.2",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
@ -93,6 +149,26 @@
"url": "https://github.com/sponsors/ljharb" "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": { "node_modules/content-disposition": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", "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": { "node_modules/depd": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
@ -182,6 +282,15 @@
"node": ">= 0.8" "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": { "node_modules/dunder-proto": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
@ -211,6 +320,15 @@
"node": ">= 0.8" "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": { "node_modules/es-define-property": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
@ -256,6 +374,15 @@
"node": ">= 0.6" "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": { "node_modules/express": {
"version": "5.2.1", "version": "5.2.1",
"resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
@ -387,6 +514,12 @@
"node": ">= 0.8" "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": { "node_modules/function-bind": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
@ -433,6 +566,12 @@
"node": ">= 0.4" "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": { "node_modules/gopd": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
@ -505,12 +644,38 @@
"url": "https://opencollective.com/express" "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": { "node_modules/inherits": {
"version": "2.0.4", "version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"license": "ISC" "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": { "node_modules/ipaddr.js": {
"version": "1.9.1", "version": "1.9.1",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
@ -581,12 +746,45 @@
"url": "https://opencollective.com/express" "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": { "node_modules/ms": {
"version": "2.1.3", "version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT" "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": { "node_modules/negotiator": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
@ -596,6 +794,24 @@
"node": ">= 0.6" "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": { "node_modules/object-assign": {
"version": "4.1.1", "version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
@ -794,6 +1010,32 @@
"node": ">=0.10.0" "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": { "node_modules/proxy-addr": {
"version": "2.0.7", "version": "2.0.7",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
@ -807,6 +1049,16 @@
"node": ">= 0.10" "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": { "node_modules/qs": {
"version": "6.14.0", "version": "6.14.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz",
@ -855,6 +1107,35 @@
"node": ">= 0.10" "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": { "node_modules/router": {
"version": "2.2.0", "version": "2.2.0",
"resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz",
@ -897,6 +1178,18 @@
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"license": "MIT" "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": { "node_modules/send": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz",
@ -1012,6 +1305,51 @@
"url": "https://github.com/sponsors/ljharb" "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": { "node_modules/split2": {
"version": "4.2.0", "version": "4.2.0",
"resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
@ -1030,6 +1368,24 @@
"node": ">= 0.8" "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": { "node_modules/stripe": {
"version": "20.0.0", "version": "20.0.0",
"resolved": "https://registry.npmjs.org/stripe/-/stripe-20.0.0.tgz", "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": { "node_modules/toidentifier": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
@ -1059,6 +1443,18 @@
"node": ">=0.6" "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": { "node_modules/type-is": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz",
@ -1094,6 +1490,12 @@
"node": ">= 0.8" "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": { "node_modules/vary": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",

View file

@ -10,6 +10,7 @@
"author": "", "author": "",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"canvas": "^3.2.0",
"cookie-parser": "^1.4.7", "cookie-parser": "^1.4.7",
"cors": "^2.8.5", "cors": "^2.8.5",
"express": "^5.2.1", "express": "^5.2.1",

View file

@ -1,5 +1,6 @@
{ pkgs }: { { pkgs }: {
deps = [ deps = [
pkgs.libuuid
pkgs.dig pkgs.dig
pkgs.unzip pkgs.unzip
]; ];