diff --git a/src/lib/cache.ts b/src/lib/cache.ts index 1edf6347b7..c6ede27eb9 100644 --- a/src/lib/cache.ts +++ b/src/lib/cache.ts @@ -1,7 +1,19 @@ import type { PerkTier } from '@oldschoolgg/toolkit/util'; -import type { User } from '@prisma/client'; +import type { Giveaway, Guild, User } from '@prisma/client'; +import { Time } from 'e'; +import { LRUCache } from 'lru-cache'; export const perkTierCache = new Map(); export type PartialUser = Pick; export const partialUserCache = new Map(); + +type CachedGuild = Pick; +export const untrustedGuildSettingsCache = new LRUCache({ max: 1000 }); + +export const giveawayCache = new LRUCache({ + max: 10, + ttl: Time.Second * 10, + ttlAutopurge: true, + ttlResolution: Time.Second +}); diff --git a/src/lib/events.ts b/src/lib/events.ts index c267984ea0..4d006770ed 100644 --- a/src/lib/events.ts +++ b/src/lib/events.ts @@ -7,8 +7,8 @@ import { Items } from 'oldschooljs'; import { UserError } from '@oldschoolgg/toolkit/structures'; import { command_name_enum } from '@prisma/client'; -import { untrustedGuildSettingsCache } from '../mahoji/guildSettings'; import { minionStatusCommand } from '../mahoji/lib/abstracted_commands/minionStatusCommand'; +import { untrustedGuildSettingsCache } from './cache.js'; import { BitField, Channel, Emoji, globalConfig } from './constants'; import pets from './data/pets'; import type { ItemBank } from './types'; diff --git a/src/lib/util/giveaway.ts b/src/lib/util/giveaway.ts index 330ea2ca71..054896e930 100644 --- a/src/lib/util/giveaway.ts +++ b/src/lib/util/giveaway.ts @@ -1,12 +1,10 @@ import type { Giveaway } from '@prisma/client'; -import type { MessageEditOptions } from 'discord.js'; -import { time, userMention } from 'discord.js'; -import { Time, debounce, noOp, randArrItem } from 'e'; -import { Bank } from 'oldschooljs'; -import type { ItemBank } from 'oldschooljs/dist/meta/types'; +import { type MessageEditOptions, time, userMention } from 'discord.js'; +import { Time, debounce, noOp } from 'e'; +import { Bank, type ItemBank } from 'oldschooljs'; import { Events } from '../constants'; - +import { sql } from '../postgres.js'; import { channelIsSendable } from '../util'; import { logError } from './logError'; import { sendToChannelID } from './webhook'; @@ -38,6 +36,27 @@ export function generateGiveawayContent(host: string, finishDate: Date, usersEnt There are ${usersEntered.length} users entered in this giveaway.`; } +async function pickRandomGiveawayWinner(giveaway: Giveaway): Promise { + if (giveaway.users_entered.length === 0) return null; + const result: { id: string }[] = await sql`WITH giveaway_users AS ( + SELECT unnest(users_entered) AS user_id + FROM giveaway + WHERE id = ${giveaway.id} +) +SELECT id +FROM users +WHERE id IN (SELECT user_id FROM giveaway_users) +AND "minion.ironman" = false +AND id != ${giveaway.user_id} +ORDER BY random() +LIMIT 1; +`; + const id = result[0]?.id; + if (!id) return null; + const user = await mUserFetch(id); + return user; +} + export const updateGiveawayMessage = debounce(async (_giveaway: Giveaway) => { const giveaway = await prisma.giveaway.findFirst({ where: { id: _giveaway.id } }); if (!giveaway) return; @@ -72,19 +91,17 @@ export async function handleGiveawayCompletion(_giveaway: Giveaway) { } }); + await updateGiveawayMessage(giveaway); + const creator = await mUserFetch(giveaway.user_id); - const users = (await Promise.all(giveaway.users_entered.map(i => mUserFetch(i)))).filter( - u => !u.isIronman && u.id !== giveaway.user_id - ); - await updateGiveawayMessage(giveaway); + const winner = await pickRandomGiveawayWinner(giveaway); - if (users.length === 0) { + if (winner === null) { await refundGiveaway(creator, loot); return; } - const winner = randArrItem(users); await transactItems({ userID: winner.id, itemsToAdd: loot }); await prisma.economyTransaction.create({ data: { @@ -99,10 +116,10 @@ export async function handleGiveawayCompletion(_giveaway: Giveaway) { globalClient.emit( Events.EconomyLog, - `${winner.mention}[${winner.id}] won ${loot} in a giveaway of ${users.length} made by ${creator.mention}[${creator.id}].` + `${winner.mention}[${winner.id}] won ${loot} in a giveaway of ${giveaway.users_entered.length} made by ${creator.mention}[${creator.id}].` ); - const str = `<@${giveaway.user_id}> **Giveaway finished:** ${users.length} users joined, the winner is... **${winner.mention}** + const str = `<@${giveaway.user_id}> **Giveaway finished:** ${giveaway.users_entered.length} users joined, the winner is... **${winner.mention}** They received these items: ${loot}`; diff --git a/src/lib/util/globalInteractions.ts b/src/lib/util/globalInteractions.ts index 10fd6844db..0bffe3e172 100644 --- a/src/lib/util/globalInteractions.ts +++ b/src/lib/util/globalInteractions.ts @@ -10,8 +10,10 @@ import { shootingStarsCommand, starCache } from '../../mahoji/lib/abstracted_com import type { ClueTier } from '../clues/clueTiers'; import { BitField, PerkTier } from '../constants'; +import type { Giveaway } from '@prisma/client'; import { RateLimitManager } from '@sapphire/ratelimits'; import { InteractionID } from '../InteractionID'; +import { giveawayCache } from '../cache.js'; import { runCommand } from '../settings/settings'; import { toaHelpCommand } from '../simulation/toa'; import type { ItemBank } from '../types'; @@ -132,11 +134,15 @@ async function giveawayButtonHandler(user: MUser, customID: string, interaction: const split = customID.split('_'); if (split[0] !== 'GIVEAWAY') return; const giveawayID = Number(split[2]); - const giveaway = await prisma.giveaway.findFirst({ - where: { - id: giveawayID - } - }); + let giveaway: Giveaway | null = giveawayCache.get(giveawayID) ?? null; + if (!giveaway) { + giveaway = await prisma.giveaway.findFirst({ + where: { + id: giveawayID + } + }); + if (giveaway) giveawayCache.set(giveawayID, giveaway); + } if (!giveaway) { return interactionReply(interaction, { content: 'Invalid giveaway.', ephemeral: true }); } diff --git a/src/mahoji/commands/giveaway.ts b/src/mahoji/commands/giveaway.ts index 72e0f52196..99bb5dcedb 100644 --- a/src/mahoji/commands/giveaway.ts +++ b/src/mahoji/commands/giveaway.ts @@ -17,9 +17,9 @@ import { Time, randInt } from 'e'; import { Bank } from 'oldschooljs'; import type { ItemBank } from 'oldschooljs/dist/meta/types'; +import { giveawayCache } from '../../lib/cache.js'; import { Emoji, patronFeatures } from '../../lib/constants'; import { marketPriceOfBank } from '../../lib/marketPrices'; - import { channelIsSendable, isModOrAdmin, makeComponents, toKMB } from '../../lib/util'; import { generateGiveawayContent } from '../../lib/util/giveaway'; import { handleMahojiConfirmation } from '../../lib/util/handleMahojiConfirmation'; @@ -193,7 +193,7 @@ export const giveawayCommand: OSBMahojiCommand = { } try { - await prisma.giveaway.create({ + const giveaway = await prisma.giveaway.create({ data: { id: giveawayID, channel_id: channelID.toString(), @@ -207,6 +207,7 @@ export const giveawayCommand: OSBMahojiCommand = { users_entered: [] } }); + giveawayCache.set(giveaway.id, giveaway); } catch (err: any) { logError(err, { user_id: user.id, diff --git a/src/mahoji/guildSettings.ts b/src/mahoji/guildSettings.ts index 4ff7301811..a28adfd850 100644 --- a/src/mahoji/guildSettings.ts +++ b/src/mahoji/guildSettings.ts @@ -1,9 +1,7 @@ -import type { Guild, Prisma } from '@prisma/client'; +import type { Prisma } from '@prisma/client'; import type { Guild as DJSGuild } from 'discord.js'; -import { LRUCache } from 'lru-cache'; -type CachedGuild = Pick; -export const untrustedGuildSettingsCache = new LRUCache({ max: 1000 }); +import { untrustedGuildSettingsCache } from '../lib/cache.js'; export async function mahojiGuildSettingsFetch(guild: string | DJSGuild) { const id = typeof guild === 'string' ? guild : guild.id; diff --git a/src/mahoji/lib/inhibitors.ts b/src/mahoji/lib/inhibitors.ts index 238ab1530d..159dd67e39 100644 --- a/src/mahoji/lib/inhibitors.ts +++ b/src/mahoji/lib/inhibitors.ts @@ -3,12 +3,12 @@ import type { DMChannel, Guild, GuildMember, InteractionReplyOptions, TextChanne import { ComponentType, PermissionsBitField } from 'discord.js'; import { BLACKLISTED_GUILDS, BLACKLISTED_USERS } from '../../lib/blacklists'; -import { type PartialUser, partialUserCache, perkTierCache } from '../../lib/cache'; +import { type PartialUser, partialUserCache, perkTierCache, untrustedGuildSettingsCache } from '../../lib/cache'; import { BadgesEnum, BitField, Channel, DISABLED_COMMANDS, globalConfig } from '../../lib/constants'; import { minionBuyButton } from '../../lib/sharedComponents'; import type { CategoryFlag } from '../../lib/types'; import { minionIsBusy } from '../../lib/util/minionIsBusy'; -import { mahojiGuildSettingsFetch, untrustedGuildSettingsCache } from '../guildSettings'; +import { mahojiGuildSettingsFetch } from '../guildSettings'; import { Cooldowns } from './Cooldowns'; export interface AbstractCommandAttributes {