diff --git a/.env.example b/.env.example index c2947ea926..37cda13863 100644 --- a/.env.example +++ b/.env.example @@ -5,7 +5,10 @@ BOT_TOKEN=PUT_YOUR_TOKEN_HERE # You may need to change these: ROBOCHIMP_DATABASE_URL=postgresql://postgres:postgres@localhost:5436/robochimp_test DATABASE_URL=postgresql://postgres:postgres@localhost:5435/osb_test -#REDIS_PORT=6379 #OPTIONAL + +# Optional +#REDIS_PORT=6379 +#TESTING_SERVER_ID=123456789012345678 # Dont change these: TZ="UTC" \ No newline at end of file diff --git a/package.json b/package.json index 12f92c1863..4efdb48704 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ }, "dependencies": { "@napi-rs/canvas": "^0.1.53", - "@oldschoolgg/toolkit": "git+https://github.com/oldschoolgg/toolkit.git#e148e18bec1be9bbb82151dced9e3f83ea0d4e85", + "@oldschoolgg/toolkit": "git+https://github.com/oldschoolgg/toolkit.git#87450a60c73136601714c77092793d2f432b70b5", "@prisma/client": "^5.16.1", "@sapphire/snowflake": "^3.5.3", "@sapphire/time-utilities": "^1.6.0", diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 7e94f2f0e2..19b3e0e739 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -434,6 +434,8 @@ model User { cached_networth_value BigInt? + username String? @db.VarChar(32) + geListings GEListing[] bingo_participant BingoParticipant[] bingo Bingo[] @@ -583,14 +585,13 @@ model Minigame { } model CommandUsage { - id Int @id @default(autoincrement()) - date DateTime @default(now()) @db.Timestamp(6) + id Int @id @default(autoincrement()) + date DateTime @default(now()) @db.Timestamp(6) user_id BigInt - command_name String @db.VarChar(32) - status command_usage_status @default(value: Unknown) - is_continue Boolean @default(false) + command_name String @db.VarChar(32) + is_continue Boolean @default(false) flags Json? - inhibited Boolean? @default(false) + inhibited Boolean? @default(false) is_mention_command Boolean @default(false) @@ -854,13 +855,6 @@ model HistoricalData { @@map("historical_data") } -enum command_usage_status { - Unknown - Success - Error - Inhibited -} - enum activity_type_enum { Agility Cooking diff --git a/src/config.example.ts b/src/config.example.ts index 45610175d4..7c3d2c498f 100644 --- a/src/config.example.ts +++ b/src/config.example.ts @@ -2,12 +2,8 @@ import type { IDiscordSettings } from './lib/types'; export const production = false; export const SENTRY_DSN: string | null = null; -export const CLIENT_SECRET = ''; -export const DEV_SERVER_ID = ''; export const DISCORD_SETTINGS: Partial = {}; -// Add or replace these with your Discord ID: export const OWNER_IDS = ['157797566833098752']; export const ADMIN_IDS = ['425134194436341760']; export const MAXING_MESSAGE = 'Congratulations on maxing!'; -// Discord server where admin commands will be allowed: export const SupportServer = '940758552425955348'; diff --git a/src/index.ts b/src/index.ts index 0601c2fdb3..f33252c2d2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -11,7 +11,7 @@ import type { TextChannel } from 'discord.js'; import { GatewayIntentBits, Options, Partials } from 'discord.js'; import { isObject } from 'e'; -import { DEV_SERVER_ID, SENTRY_DSN, SupportServer } from './config'; +import { SENTRY_DSN, SupportServer } from './config'; import { syncActivityCache } from './lib/Task'; import { BLACKLISTED_GUILDS, BLACKLISTED_USERS } from './lib/blacklists'; import { Channel, Events, META_CONSTANTS, gitHash, globalConfig } from './lib/constants'; @@ -71,7 +71,10 @@ const client = new OldSchoolBotClient({ maxSize: 200, keepOverLimit: member => CACHED_ACTIVE_USER_IDS.has(member.user.id) }, - GuildEmojiManager: { maxSize: 1, keepOverLimit: i => [DEV_SERVER_ID, SupportServer].includes(i.guild.id) }, + GuildEmojiManager: { + maxSize: 1, + keepOverLimit: i => [globalConfig.testingServerID, SupportServer].includes(i.guild.id) + }, GuildStickerManager: { maxSize: 0 }, PresenceManager: { maxSize: 0 }, VoiceStateManager: { maxSize: 0 }, @@ -92,7 +95,7 @@ const client = new OldSchoolBotClient({ }); export const mahojiClient = new MahojiClient({ - developmentServerID: DEV_SERVER_ID, + developmentServerID: globalConfig.testingServerID, applicationID: globalConfig.clientID, commands: allCommands, handlers: { @@ -138,13 +141,14 @@ client.on('interactionCreate', async interaction => { if (interaction.guildId && BLACKLISTED_GUILDS.has(interaction.guildId)) return; if (!client.isReady()) { - if (interaction.isChatInputCommand()) { + if (interaction.isRepliable()) { await interaction.reply({ content: 'Old School Bot is currently down for maintenance/updates, please try again in a couple minutes! Thank you <3', ephemeral: true }); } + return; } @@ -180,7 +184,6 @@ client.on('guildCreate', guild => { } }); -client.on('shardDisconnect', ({ wasClean, code, reason }) => debugLog('Shard Disconnect', { wasClean, code, reason })); client.on('shardError', err => debugLog('Shard Error', { error: err.message })); client.once('ready', () => runTimedLoggedFn('OnStartup', async () => onStartup())); @@ -188,11 +191,11 @@ async function main() { if (process.env.TEST) return; await Promise.all([ runTimedLoggedFn('Sync Active User IDs', syncActiveUserIDs), - runTimedLoggedFn('Sync Activity Cache', syncActivityCache) + runTimedLoggedFn('Sync Activity Cache', syncActivityCache), + runTimedLoggedFn('Startup Scripts', runStartupScripts) ]); - await runTimedLoggedFn('Startup Scripts', runStartupScripts); - await runTimedLoggedFn('Log In', () => client.login(globalConfig.botToken)); + console.log(`Logged in as ${globalClient.user.username}`); } process.on('uncaughtException', err => { diff --git a/src/lib/DynamicButtons.ts b/src/lib/DynamicButtons.ts index f894c792ec..5d892ba99e 100644 --- a/src/lib/DynamicButtons.ts +++ b/src/lib/DynamicButtons.ts @@ -9,7 +9,7 @@ import type { ThreadChannel } from 'discord.js'; import { ButtonBuilder, ButtonStyle } from 'discord.js'; -import { Time, noOp } from 'e'; +import { Time, isFunction, noOp } from 'e'; import murmurhash from 'murmurhash'; import { BLACKLISTED_USERS } from './blacklists'; @@ -23,7 +23,7 @@ export class DynamicButtons { buttons: { name: string; id: string; - fn: DynamicButtonFn; + fn?: DynamicButtonFn; emoji: string | undefined; cantBeBusy: boolean; style?: ButtonStyle; @@ -109,18 +109,21 @@ export class DynamicButtons { for (const button of this.buttons) { if (collectedInteraction.customId === button.id) { if (minionIsBusy(collectedInteraction.user.id) && button.cantBeBusy) { - return collectedInteraction.reply({ + await collectedInteraction.reply({ content: "Your action couldn't be performed, because your minion is busy.", ephemeral: true }); + return null; } - await button.fn({ message: this.message!, interaction: collectedInteraction }); - return collectedInteraction; + if ('fn' in button && isFunction(button.fn)) { + await button.fn({ message: this.message!, interaction: collectedInteraction }); + } + return button; } } } - return collectedInteraction; + return null; } add({ @@ -131,7 +134,7 @@ export class DynamicButtons { style }: { name: string; - fn: DynamicButtonFn; + fn?: DynamicButtonFn; emoji?: string; cantBeBusy?: boolean; style?: ButtonStyle; diff --git a/src/lib/InteractionID.ts b/src/lib/InteractionID.ts new file mode 100644 index 0000000000..cff0e337de --- /dev/null +++ b/src/lib/InteractionID.ts @@ -0,0 +1,17 @@ +export const InteractionID = { + PaginatedMessage: { + FirstPage: 'PM_FIRST_PAGE', + PreviousPage: 'PM_PREVIOUS_PAGE', + NextPage: 'PM_NEXT_PAGE', + LastPage: 'PM_LAST_PAGE' + }, + Slayer: { + AutoSlaySaved: 'SLAYER_AUTO_SLAY_SAVED', + AutoSlayDefault: 'SLAYER_AUTO_SLAY_DEFAULT', + AutoSlayEHP: 'SLAYER_AUTO_SLAY_EHP', + AutoSlayBoss: 'SLAYER_AUTO_SLAY_BOSS', + SkipTask: 'SLAYER_SKIP_TASK', + CancelTask: 'SLAYER_CANCEL_TASK', + BlockTask: 'SLAYER_BLOCK_TASK' + } +} as const; diff --git a/src/lib/MUser.ts b/src/lib/MUser.ts index 0bedb50310..f250588d47 100644 --- a/src/lib/MUser.ts +++ b/src/lib/MUser.ts @@ -15,7 +15,7 @@ import { userIsBusy } from './busyCounterCache'; import { ClueTiers } from './clues/clueTiers'; import type { CATier } from './combat_achievements/combatAchievements'; import { CombatAchievements } from './combat_achievements/combatAchievements'; -import { BitField, Emoji, badges, projectiles, usernameCache } from './constants'; +import { BitField, projectiles } from './constants'; import { bossCLItems } from './data/Collections'; import { allPetIDs } from './data/CollectionsExport'; import { getSimilarItems } from './data/similarItems'; @@ -41,11 +41,12 @@ import type { BankSortMethod } from './sorts'; import type { ChargeBank } from './structures/Bank'; import { Gear, defaultGear } from './structures/Gear'; import type { ItemBank, Skills } from './types'; -import { addItemToBank, convertXPtoLVL, itemNameFromID } from './util'; +import { addItemToBank, cacheUsername, convertXPtoLVL, itemNameFromID } from './util'; import { determineRunes } from './util/determineRunes'; import { getKCByName } from './util/getKCByName'; import getOSItem, { getItem } from './util/getOSItem'; import { logError } from './util/logError'; +import { makeBadgeString } from './util/makeBadgeString'; import { minionIsBusy } from './util/minionIsBusy'; import { minionName } from './util/minionUtils'; import type { TransactItemsArgs } from './util/transactItemsFromBank'; @@ -94,6 +95,7 @@ export class MUserClass { gear!: UserFullGearSetup; skillsAsXP!: Required; skillsAsLevels!: Required; + badgesString!: string; constructor(user: User) { this.user = user; @@ -101,6 +103,10 @@ export class MUserClass { this.updateProperties(); syncPerkTierOfUser(this); + + if (this.user.username) { + cacheUsername(this.id, this.user.username, this.badgesString); + } } private updateProperties() { @@ -130,6 +136,8 @@ export class MUserClass { this.skillsAsXP = this.getSkills(false); this.skillsAsLevels = this.getSkills(true); + + this.badgesString = makeBadgeString(this.user.badges, this.isIronman); } countSkillsAtleast99() { @@ -213,23 +221,15 @@ export class MUserClass { } get rawUsername() { - return globalClient.users.cache.get(this.id)?.username ?? usernameCache.get(this.id) ?? 'Unknown'; + return globalClient.users.cache.get(this.id)?.username ?? this.user.username ?? 'Unknown'; } get usernameOrMention() { - return usernameCache.get(this.id) ?? this.mention; - } - - get badgeString() { - const rawBadges = this.user.badges.map(num => badges[num]); - if (this.isIronman) { - rawBadges.push(Emoji.Ironman); - } - return rawBadges.join(' '); + return this.user.username ?? this.mention; } get badgedUsername() { - return `${this.badgeString} ${this.usernameOrMention}`; + return `${this.badgesString} ${this.usernameOrMention}`; } toString() { @@ -950,12 +950,12 @@ declare global { var GlobalMUserClass: typeof MUserClass; } -async function srcMUserFetch(userID: string) { +async function srcMUserFetch(userID: string, updates: Prisma.UserUpdateInput = {}) { const user = await prisma.user.upsert({ create: { id: userID }, - update: {}, + update: updates, where: { id: userID } diff --git a/src/lib/PaginatedMessage.ts b/src/lib/PaginatedMessage.ts index 8e572f15f6..8f184a31fd 100644 --- a/src/lib/PaginatedMessage.ts +++ b/src/lib/PaginatedMessage.ts @@ -1,10 +1,11 @@ import { UserError } from '@oldschoolgg/toolkit'; import type { BaseMessageOptions, ComponentType, MessageEditOptions, TextChannel } from 'discord.js'; import { ActionRowBuilder, ButtonBuilder, ButtonStyle } from 'discord.js'; -import { Time } from 'e'; +import { Time, isFunction } from 'e'; +import { InteractionID } from './InteractionID'; import type { PaginatedMessagePage } from './util'; -import { logError } from './util/logError'; +import { logError, logErrorForInteraction } from './util/logError'; const controlButtons: { customId: string; @@ -12,12 +13,12 @@ const controlButtons: { run: (opts: { paginatedMessage: PaginatedMessage }) => unknown; }[] = [ { - customId: 'pm-first-page', + customId: InteractionID.PaginatedMessage.FirstPage, emoji: '⏪', run: ({ paginatedMessage }) => (paginatedMessage.index = 0) }, { - customId: 'pm-previous-page', + customId: InteractionID.PaginatedMessage.PreviousPage, emoji: '◀️', run: ({ paginatedMessage }) => { if (paginatedMessage.index === 0) { @@ -28,7 +29,7 @@ const controlButtons: { } }, { - customId: 'pm-next-page', + customId: InteractionID.PaginatedMessage.NextPage, emoji: '▶️', run: ({ paginatedMessage }) => { if (paginatedMessage.index === paginatedMessage.totalPages - 1) { @@ -39,7 +40,7 @@ const controlButtons: { } }, { - customId: 'pm-last-page', + customId: InteractionID.PaginatedMessage.LastPage, emoji: '⏩', run: ({ paginatedMessage }) => (paginatedMessage.index = paginatedMessage.totalPages - 1) } @@ -76,8 +77,9 @@ export class PaginatedMessage { const rawPage = !Array.isArray(this.pages) ? await this.pages.generate({ currentPage: this.index }) : this.pages[this.index]; + return { - ...rawPage, + ...(isFunction(rawPage) ? await rawPage() : rawPage), components: numberOfPages === 1 ? [] @@ -121,7 +123,11 @@ export class PaginatedMessage { }); if (previousIndex !== this.index) { - await interaction.update(await this.render()); + try { + await interaction.update(await this.render()); + } catch (err) { + logErrorForInteraction(err, interaction); + } return; } } diff --git a/src/lib/Task.ts b/src/lib/Task.ts index 374ffc4982..fce856a374 100644 --- a/src/lib/Task.ts +++ b/src/lib/Task.ts @@ -220,7 +220,6 @@ export async function processPendingActivities() { } export async function syncActivityCache() { const tasks = await prisma.activity.findMany({ where: { completed: false } }); - minionActivityCache.clear(); for (const task of tasks) { activitySync(task); diff --git a/src/lib/constants.ts b/src/lib/constants.ts index 52860b3548..235f257d6f 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -19,6 +19,7 @@ export { PerkTier }; const TestingMainChannelID = DISCORD_SETTINGS.Channels?.TestingMain ?? '940760643525570591'; export const BOT_TYPE: 'BSO' | 'OSB' = 'OSB' as 'BSO' | 'OSB'; +export const BOT_TYPE_LOWERCASE: 'bso' | 'osb' = BOT_TYPE.toLowerCase() as 'bso' | 'osb'; export const Channel = { General: DISCORD_SETTINGS.Channels?.General ?? '342983479501389826', @@ -478,8 +479,6 @@ export const TWITCHERS_GLOVES = ['egg', 'ring', 'seed', 'clue'] as const; export type TwitcherGloves = (typeof TWITCHERS_GLOVES)[number]; export const busyImmuneCommands = ['admin', 'rp']; -export const usernameCache = new Map(); -export const badgesCache = new Map(); export const minionBuyButton = new ButtonBuilder() .setCustomId('BUY_MINION') .setLabel('Buy Minion') @@ -534,7 +533,8 @@ const globalConfigSchema = z.object({ redisPort: z.coerce.number().int().optional(), botToken: z.string().min(1), isCI: z.coerce.boolean().default(false), - isProduction: z.coerce.boolean().default(production) + isProduction: z.coerce.boolean().default(production), + testingServerID: z.string() }); dotenv.config({ path: path.resolve(process.cwd(), process.env.TEST ? '.env.test' : '.env') }); @@ -544,13 +544,17 @@ if (!process.env.BOT_TOKEN && !process.env.CI) { ); } +const OLDSCHOOLGG_TESTING_SERVER_ID = '940758552425955348'; +const isProduction = process.env.NODE_ENV === 'production'; + export const globalConfig = globalConfigSchema.parse({ clientID: process.env.CLIENT_ID, - geAdminChannelID: process.env.GE_ADMIN_CHANNEL_ID, + geAdminChannelID: isProduction ? '830145040495411210' : '1042760447830536212', redisPort: process.env.REDIS_PORT, botToken: process.env.BOT_TOKEN, isCI: process.env.CI, - isProduction: process.env.NODE_ENV === 'production' + isProduction, + testingServerID: process.env.TESTING_SERVER_ID ?? OLDSCHOOLGG_TESTING_SERVER_ID }); if ((process.env.NODE_ENV === 'production') !== globalConfig.isProduction || production !== globalConfig.isProduction) { diff --git a/src/lib/data/CollectionsExport.ts b/src/lib/data/CollectionsExport.ts index c1881b77bd..1022377dff 100644 --- a/src/lib/data/CollectionsExport.ts +++ b/src/lib/data/CollectionsExport.ts @@ -1546,7 +1546,8 @@ export const allPetsCL = resolveItems([ "Lil'viathan", 'Butch', 'Baron', - 'Scurry' + 'Scurry', + 'Smol heredit' ]); export const camdozaalCL = resolveItems([ 'Barronite mace', diff --git a/src/lib/grandExchange.ts b/src/lib/grandExchange.ts index b575080fde..c2581bdbb6 100644 --- a/src/lib/grandExchange.ts +++ b/src/lib/grandExchange.ts @@ -1,4 +1,3 @@ -import { Stopwatch } from '@oldschoolgg/toolkit'; import type { GEListing, GETransaction } from '@prisma/client'; import { GEListingType } from '@prisma/client'; import { ButtonBuilder, ButtonStyle, bold, userMention } from 'discord.js'; @@ -768,14 +767,11 @@ ${type} ${toKMB(quantity)} ${item.name} for ${toKMB(price)} each, for a total of } async extensiveVerification() { - const stopwatch = new Stopwatch(); - stopwatch.check('extensiveVerification start'); await Promise.all([ prisma.gETransaction.findMany().then(txs => txs.map(tx => sanityCheckTransaction(tx))), prisma.gEListing.findMany().then(listings => listings.map(listing => sanityCheckListing(listing))), this.checkGECanFullFilAllListings() ]); - stopwatch.check('extensiveVerification finish'); return true; } diff --git a/src/lib/party.ts b/src/lib/party.ts index 415abffb0e..dd42268c21 100644 --- a/src/lib/party.ts +++ b/src/lib/party.ts @@ -1,13 +1,14 @@ import { makeComponents } from '@oldschoolgg/toolkit'; import { UserError } from '@oldschoolgg/toolkit'; import type { TextChannel } from 'discord.js'; -import { ButtonBuilder, ButtonStyle, ComponentType, InteractionCollector, userMention } from 'discord.js'; +import { ButtonBuilder, ButtonStyle, ComponentType, InteractionCollector } from 'discord.js'; import { Time, debounce, noOp } from 'e'; import { production } from '../config'; import { BLACKLISTED_USERS } from './blacklists'; -import { SILENT_ERROR, usernameCache } from './constants'; +import { SILENT_ERROR } from './constants'; import type { MakePartyOptions } from './types'; +import { getUsername } from './util'; import { CACHED_ACTIVE_USER_IDS } from './util/cachedUserIDs'; const partyLockCache = new Set(); @@ -42,13 +43,13 @@ export async function setupParty(channel: TextChannel, leaderUser: MUser, option let deleted = false; let massStarted = false; - function getMessageContent() { + async function getMessageContent() { return { - content: `${options.message}\n\n**Users Joined:** ${usersWhoConfirmed - .map(u => usernameCache.get(u) ?? userMention(u)) - .join( - ', ' - )}\n\nThis party will automatically depart in 2 minutes, or if the leader clicks the start (start early) or stop button.`, + content: `${options.message}\n\n**Users Joined:** ${( + await Promise.all(usersWhoConfirmed.map(u => getUsername(u))) + ).join( + ', ' + )}\n\nThis party will automatically depart in 2 minutes, or if the leader clicks the start (start early) or stop button.`, components: makeComponents(buttons.map(i => i.button)), allowedMentions: { users: [] @@ -56,12 +57,12 @@ export async function setupParty(channel: TextChannel, leaderUser: MUser, option }; } - const confirmMessage = await channel.send(getMessageContent()); + const confirmMessage = await channel.send(await getMessageContent()); // Debounce message edits to prevent spam. - const updateUsersIn = debounce(() => { + const updateUsersIn = debounce(async () => { if (deleted) return; - confirmMessage.edit(getMessageContent()); + confirmMessage.edit(await getMessageContent()); }, 500); const removeUser = (userID: string) => { @@ -77,7 +78,7 @@ export async function setupParty(channel: TextChannel, leaderUser: MUser, option new Promise(async (resolve, reject) => { let partyCancelled = false; const collector = new InteractionCollector(globalClient, { - time: Time.Minute * 2, + time: Time.Minute * 5, maxUsers: options.usersAllowed?.length ?? options.maxSize, dispose: true, channel, @@ -138,7 +139,7 @@ export async function setupParty(channel: TextChannel, leaderUser: MUser, option return; } - resolve(await Promise.all(usersWhoConfirmed.map(mUserFetch))); + resolve(await Promise.all(usersWhoConfirmed.map(id => mUserFetch(id)))); } collector.on('collect', async interaction => { diff --git a/src/lib/perkTiers.ts b/src/lib/perkTiers.ts index ea63dd3c98..6582178ce9 100644 --- a/src/lib/perkTiers.ts +++ b/src/lib/perkTiers.ts @@ -103,5 +103,6 @@ export function getUsersPerkTier( export function syncPerkTierOfUser(user: MUser) { const perkTier = getUsersPerkTier(user, true); perkTierCache.set(user.id, perkTier); + redis.setUser(user.id, { perk_tier: perkTier }); return perkTier; } diff --git a/src/lib/rolesTask.ts b/src/lib/rolesTask.ts index 317fc988a6..dc694bb83c 100644 --- a/src/lib/rolesTask.ts +++ b/src/lib/rolesTask.ts @@ -3,12 +3,12 @@ import { noOp, notEmpty } from 'e'; import { SupportServer, production } from '../config'; import { ClueTiers } from '../lib/clues/clueTiers'; -import { Roles, usernameCache } from '../lib/constants'; +import { Roles } from '../lib/constants'; import { getCollectionItems } from '../lib/data/Collections'; import { Minigames } from '../lib/settings/minigames'; import Skills from '../lib/skilling/skills'; -import { convertXPtoLVL } from '../lib/util'; +import { convertXPtoLVL, getUsername } from '../lib/util'; import { logError } from '../lib/util/logError'; import { TeamLoot } from './simulation/TeamLoot'; import type { ItemBank } from './types'; @@ -110,7 +110,7 @@ async function addRoles({ if (userMap) { const userArr = []; for (const [id, arr] of Object.entries(userMap)) { - const username = usernameCache.get(id) ?? 'Unknown'; + const username = await getUsername(id); userArr.push(`${username}(${arr.join(', ')})`); } str += `\n${userArr.join(',')}`; diff --git a/src/lib/settings/settings.ts b/src/lib/settings/settings.ts index 98d19f3121..f8b57b40ac 100644 --- a/src/lib/settings/settings.ts +++ b/src/lib/settings/settings.ts @@ -13,7 +13,7 @@ import { postCommand } from '../../mahoji/lib/postCommand'; import { preCommand } from '../../mahoji/lib/preCommand'; import { convertMahojiCommandToAbstractCommand } from '../../mahoji/lib/util'; import { minionActivityCache } from '../constants'; -import { channelIsSendable, isGroupActivity } from '../util'; +import { channelIsSendable, isGroupActivity, roughMergeMahojiResponse } from '../util'; import { handleInteractionError, interactionReply } from '../util/interactionReply'; import { logError } from '../util/logError'; import { convertStoredActivityToFlatActivity } from './prisma'; @@ -95,6 +95,7 @@ interface RunCommandArgs { guildID: string | undefined | null; interaction: ButtonInteraction | ChatInputCommandInteraction; continueDeltaMillis: number | null; + ephemeral?: boolean; } export async function runCommand({ commandName, @@ -106,7 +107,8 @@ export async function runCommand({ user, member, interaction, - continueDeltaMillis + continueDeltaMillis, + ephemeral }: RunCommandArgs): Promise { const channel = globalClient.channels.cache.get(channelID.toString()); if (!channel || !channelIsSendable(channel)) return null; @@ -152,7 +154,8 @@ export async function runCommand({ user, interaction }); - if (result && !interaction.replied) await interactionReply(interaction, result); + if (result && !interaction.replied) + await interactionReply(interaction, roughMergeMahojiResponse(result, { ephemeral })); return result; } catch (err: any) { await handleInteractionError(err, interaction); diff --git a/src/lib/util.ts b/src/lib/util.ts index 768fcf0338..c5251f077f 100644 --- a/src/lib/util.ts +++ b/src/lib/util.ts @@ -1,4 +1,4 @@ -import { Stopwatch, stripEmojis } from '@oldschoolgg/toolkit'; +import { Stopwatch, cleanUsername, stripEmojis } from '@oldschoolgg/toolkit'; import type { CommandResponse } from '@oldschoolgg/toolkit'; import type { BaseMessageOptions, @@ -18,10 +18,11 @@ import { Time, objectEntries } from 'e'; import type { Bank } from 'oldschooljs'; import { bool, integer, nativeMath, nodeCrypto, real } from 'random-js'; +import { LRUCache } from 'lru-cache'; import { ADMIN_IDS, OWNER_IDS, SupportServer } from '../config'; import type { MUserClass } from './MUser'; import { PaginatedMessage } from './PaginatedMessage'; -import { BitField, badgesCache, projectiles, usernameCache } from './constants'; +import { BOT_TYPE_LOWERCASE, BitField, globalConfig, projectiles } from './constants'; import { getSimilarItems } from './data/similarItems'; import type { DefenceGearStat, GearSetupType, OffenceGearStat } from './gear/types'; import { GearSetupTypes, GearStat } from './gear/types'; @@ -210,7 +211,7 @@ export function isValidNickname(str?: string) { ); } -export type PaginatedMessagePage = MessageEditOptions; +export type PaginatedMessagePage = MessageEditOptions | (() => Promise); export async function makePaginatedMessage(channel: TextChannel, pages: PaginatedMessagePage[], target?: string) { const m = new PaginatedMessage({ pages, channel }); @@ -329,17 +330,30 @@ export function skillingPetDropRate( return { petDropRate: dropRate }; } -function getBadges(user: MUser | string | bigint) { - if (typeof user === 'string' || typeof user === 'bigint') { - return badgesCache.get(user.toString()) ?? ''; +const badgesKey = `${BOT_TYPE_LOWERCASE.toLowerCase()}_badges` as 'osb_badges' | 'bso_badges'; + +const usernameWithBadgesCache = new LRUCache({ max: 2000 }); +export function cacheUsername(id: string, username: string, badges: string) { + const current = usernameWithBadgesCache.get(id); + const newValue = `${badges ? `${badges} ` : ''}${username}`; + if (!current || current !== newValue) { + usernameWithBadgesCache.set(id, newValue); + redis.setUser(id, { username: cleanUsername(username), [badgesKey]: badges }); } - return user.badgeString; } - -export function getUsername(id: string | bigint, withBadges = true) { - let username = usernameCache.get(id.toString()) ?? 'Unknown'; - if (withBadges) username = `${getBadges(id)} ${username}`; - return username; +export async function getUsername(_id: string | bigint): Promise { + const id = _id.toString(); + const cached = usernameWithBadgesCache.get(id); + if (cached) return cached; + const user = await redis.getUser(id); + if (!user.username) return 'Unknown'; + const badges = user[badgesKey]; + const newValue = `${badges ? `${badges} ` : ''}${user.username}`; + usernameWithBadgesCache.set(id, newValue); + return newValue; +} +export function getUsernameSync(_id: string | bigint) { + return usernameWithBadgesCache.get(_id.toString()) ?? 'Unknown'; } export function awaitMessageComponentInteraction({ @@ -367,12 +381,14 @@ export function awaitMessageComponentInteraction({ } export async function runTimedLoggedFn(name: string, fn: () => Promise) { - debugLog(`Starting ${name}...`); + const logger = globalConfig.isProduction ? debugLog : console.log; const stopwatch = new Stopwatch(); stopwatch.start(); await fn(); stopwatch.stop(); - debugLog(`Finished ${name} in ${stopwatch.toString()}`); + if (!globalConfig.isProduction || stopwatch.duration > 50) { + logger(`Took ${stopwatch} to do ${name}`); + } } export function isModOrAdmin(user: MUser) { diff --git a/src/lib/util/cachedUserIDs.ts b/src/lib/util/cachedUserIDs.ts index 7d87fb5f48..bf2d394118 100644 --- a/src/lib/util/cachedUserIDs.ts +++ b/src/lib/util/cachedUserIDs.ts @@ -1,4 +1,4 @@ -import { Stopwatch } from '@oldschoolgg/toolkit'; +import { Prisma } from '@prisma/client'; import { ChannelType } from 'discord.js'; import { objectEntries } from 'e'; @@ -11,15 +11,15 @@ CACHED_ACTIVE_USER_IDS.add(globalConfig.clientID); for (const id of OWNER_IDS) CACHED_ACTIVE_USER_IDS.add(id); export async function syncActiveUserIDs() { - const [users, otherUsers] = await Promise.all([ - prisma.$queryRaw<{ user_id: string }[]>`SELECT DISTINCT(user_id::text) -FROM command_usage -WHERE date > now() - INTERVAL '72 hours';`, - prisma.$queryRaw<{ id: string }[]>`SELECT id + const [users, otherUsers] = await prisma.$transaction([ + prisma.$queryRawUnsafe<{ user_id: string }[]>(`SELECT DISTINCT(${Prisma.ActivityScalarFieldEnum.user_id}::text) +FROM activity +WHERE finish_date > now() - INTERVAL '48 hours'`), + prisma.$queryRawUnsafe<{ id: string }[]>(`SELECT id FROM users -WHERE main_account IS NOT NULL - OR CARDINALITY(ironman_alts) > 0 - OR bitfield && ARRAY[2,3,4,5,6,7,8,12,11,21,19];` +WHERE ${Prisma.UserScalarFieldEnum.main_account} IS NOT NULL +OR CARDINALITY(${Prisma.UserScalarFieldEnum.ironman_alts}) > 0 +OR ${Prisma.UserScalarFieldEnum.bitfield} && ARRAY[2,3,4,5,6,7,8,12,11,21,19];`) ]); for (const id of [...users.map(i => i.user_id), ...otherUsers.map(i => i.id)]) { @@ -97,11 +97,6 @@ export const emojiServers = new Set([ export function cacheCleanup() { if (!globalClient.isReady()) return; - const stopwatch = new Stopwatch(); - stopwatch.start(); - debugLog('Cache Cleanup Start', { - type: 'CACHE_CLEANUP' - }); return runTimedLoggedFn('Cache Cleanup', async () => { await runTimedLoggedFn('Clear Channels', async () => { for (const channel of globalClient.channels.cache.values()) { @@ -168,10 +163,5 @@ export function cacheCleanup() { } } }); - - stopwatch.stop(); - debugLog(`Cache Cleanup Finish After ${stopwatch.toString()}`, { - type: 'CACHE_CLEANUP' - }); }); } diff --git a/src/lib/util/commandUsage.ts b/src/lib/util/commandUsage.ts index 90821b3a48..365ec7b132 100644 --- a/src/lib/util/commandUsage.ts +++ b/src/lib/util/commandUsage.ts @@ -1,6 +1,5 @@ import type { CommandOptions } from '@oldschoolgg/toolkit'; import type { Prisma } from '@prisma/client'; -import { command_usage_status } from '@prisma/client'; import { getCommandArgs } from '../../mahoji/lib/util'; @@ -28,7 +27,6 @@ export function makeCommandUsage({ return { user_id: BigInt(userID), command_name: commandName, - status: command_usage_status.Unknown, args: getCommandArgs(commandName, args), channel_id: BigInt(channelID), guild_id: guildID ? BigInt(guildID) : null, diff --git a/src/lib/util/globalInteractions.ts b/src/lib/util/globalInteractions.ts index b4f369c74b..9ff0c472b6 100644 --- a/src/lib/util/globalInteractions.ts +++ b/src/lib/util/globalInteractions.ts @@ -11,6 +11,7 @@ import { shootingStarsCommand, starCache } from '../../mahoji/lib/abstracted_com import type { ClueTier } from '../clues/clueTiers'; import { BitField, PerkTier } from '../constants'; +import { InteractionID } from '../InteractionID'; import { runCommand } from '../settings/settings'; import { toaHelpCommand } from '../simulation/toa'; import type { ItemBank } from '../types'; @@ -145,6 +146,7 @@ async function giveawayButtonHandler(user: MUser, customID: string, interaction: }); } + await deferInteraction(interaction); return runCommand({ commandName: 'giveaway', args: { @@ -310,7 +312,14 @@ async function handleGEButton(user: MUser, id: string, interaction: ButtonIntera export async function interactionHook(interaction: Interaction) { if (!interaction.isButton()) return; - if (['CONFIRM', 'CANCEL', 'PARTY_JOIN'].includes(interaction.customId)) return; + const ignoredInteractionIDs = [ + 'CONFIRM', + 'CANCEL', + 'PARTY_JOIN', + ...Object.values(InteractionID.PaginatedMessage), + ...Object.values(InteractionID.Slayer) + ]; + if (ignoredInteractionIDs.includes(interaction.customId)) return; if (['DYN_', 'LP_'].some(s => interaction.customId.startsWith(s))) return; if (globalClient.isShuttingDown) { @@ -336,9 +345,8 @@ export async function interactionHook(interaction: Interaction) { }); } - await deferInteraction(interaction); - const user = await mUserFetch(userID); + if (id.includes('GIVEAWAY_')) return giveawayButtonHandler(user, id, interaction); if (id.includes('REPEAT_TRIP')) return repeatTripHandler(user, interaction); if (id.startsWith('GPE_')) return handleGearPresetEquip(user, id, interaction); @@ -385,7 +393,7 @@ export async function interactionHook(interaction: Interaction) { } async function doClue(tier: ClueTier['name']) { - runCommand({ + return runCommand({ commandName: 'clue', args: { tier }, bypassInhibitors: true, @@ -394,7 +402,7 @@ export async function interactionHook(interaction: Interaction) { } async function openCasket(tier: ClueTier['name']) { - runCommand({ + return runCommand({ commandName: 'open', args: { name: tier, diff --git a/src/lib/util/interactionHelpers.ts b/src/lib/util/interactionHelpers.ts deleted file mode 100644 index e75e393f63..0000000000 --- a/src/lib/util/interactionHelpers.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { createHash } from 'node:crypto'; -import type { ChatInputCommandInteraction } from 'discord.js'; -import { ButtonBuilder, ButtonStyle, ComponentType } from 'discord.js'; -import { Time, chunk, randInt } from 'e'; - -import { deferInteraction, interactionReply } from './interactionReply'; - -export async function interactionReplyGetDuration( - interaction: ChatInputCommandInteraction, - prompt: string, - ...durations: { display: string; duration: number }[] -) { - await deferInteraction(interaction); - const unique = createHash('sha256') - .update(String(Date.now()) + String(randInt(10_000, 99_999))) - .digest('hex') - .slice(2, 12); - let x = 0; - const buttons = durations.map(d => ({ label: d.display, customId: `${unique}_DUR_BUTTON_${x++}` })); - buttons.push({ label: 'Cancel', customId: `${unique}_DUR_BUTTON_${x}` }); - const components = makePlainButtons(...buttons); - - const response = await interactionReply(interaction, { content: prompt, components }); - - if (response === undefined) return false; - try { - const selection = await response.awaitMessageComponent({ - filter: i => i.user.id === interaction.user.id, - time: 15 * Time.Second - }); - const id = Number(selection.customId.split('_')[3]); - if (durations[id]) { - await interaction.editReply({ content: `Selected: ${durations[id].display}`, components: [] }); - return durations[id]; - } - await interaction.editReply({ content: 'Cancelled.', components: [] }); - return false; - } catch (e) { - await interaction.editReply({ content: 'Did not choose a duration in time.', components: [] }); - return false; - } -} - -function makePlainButtons(...buttons: { label: string; customId: string }[]) { - const components: ButtonBuilder[] = []; - for (let i = 0; i < buttons.length; i++) { - components.push( - new ButtonBuilder({ label: buttons[i].label, customId: buttons[i].customId, style: ButtonStyle.Secondary }) - ); - } - return chunk(components, 5).map(i => ({ components: i, type: ComponentType.ActionRow })); -} diff --git a/src/lib/util/interactionReply.ts b/src/lib/util/interactionReply.ts index 021ff723f7..36118215a4 100644 --- a/src/lib/util/interactionReply.ts +++ b/src/lib/util/interactionReply.ts @@ -16,6 +16,7 @@ import { logErrorForInteraction } from './logError'; export async function interactionReply(interaction: RepliableInteraction, response: string | InteractionReplyOptions) { let i: Promise | Promise | undefined = undefined; + if (interaction.replied) { i = interaction.followUp(response); } else if (interaction.deferred) { @@ -24,8 +25,8 @@ export async function interactionReply(interaction: RepliableInteraction, respon i = interaction.reply(response); } try { - await i; - return i; + const result = await i; + return result; } catch (e: any) { if (e instanceof DiscordAPIError && e.code !== 10_008) { // 10_008 is unknown message, e.g. if someone deletes the message before it's replied to. diff --git a/src/lib/util/logError.ts b/src/lib/util/logError.ts index 2eb4da18f1..75a85c0fcf 100644 --- a/src/lib/util/logError.ts +++ b/src/lib/util/logError.ts @@ -1,9 +1,10 @@ -import { convertAPIOptionsToCommandOptions } from '@oldschoolgg/toolkit'; +import { convertAPIOptionsToCommandOptions, deepMerge } from '@oldschoolgg/toolkit'; import { captureException } from '@sentry/node'; import type { Interaction } from 'discord.js'; import { isObject } from 'e'; import { production } from '../../config'; +import { globalConfig } from '../constants'; export function assert(condition: boolean, desc?: string, context?: Record) { if (!condition) { @@ -16,16 +17,20 @@ export function assert(condition: boolean, desc?: string, context?: Record, extra?: Record) { - debugLog(`${(err as any)?.message ?? JSON.stringify(err)}`, { type: 'ERROR', raw: JSON.stringify(err) }); - if (production) { + const metaInfo = deepMerge(context ?? {}, extra ?? {}); + debugLog(`${(err as any)?.message ?? JSON.stringify(err)}`, { + type: 'ERROR', + raw: JSON.stringify(err), + metaInfo: JSON.stringify(metaInfo) + }); + if (globalConfig.isProduction) { captureException(err, { tags: context, - extra + extra: metaInfo }); } else { console.error(err); - console.log(context); - console.log(extra); + console.log(metaInfo); } } diff --git a/src/lib/util/makeBadgeString.ts b/src/lib/util/makeBadgeString.ts new file mode 100644 index 0000000000..d8f5742b15 --- /dev/null +++ b/src/lib/util/makeBadgeString.ts @@ -0,0 +1,9 @@ +import { Emoji, badges } from '../constants'; + +export function makeBadgeString(badgeIDs: number[] | null | undefined, isIronman: boolean) { + const rawBadges: string[] = (badgeIDs ?? []).map(num => badges[num]); + if (isIronman) { + rawBadges.push(Emoji.Ironman); + } + return rawBadges.join(' '); +} diff --git a/src/lib/util/repeatStoredTrip.ts b/src/lib/util/repeatStoredTrip.ts index e254df427c..85ed69c2b7 100644 --- a/src/lib/util/repeatStoredTrip.ts +++ b/src/lib/util/repeatStoredTrip.ts @@ -63,7 +63,7 @@ import { itemNameFromID } from '../util'; import { giantsFoundryAlloys } from './../../mahoji/lib/abstracted_commands/giantsFoundryCommand'; import type { NightmareZoneActivityTaskOptions, UnderwaterAgilityThievingTaskOptions } from './../types/minions'; import getOSItem from './getOSItem'; -import { deferInteraction } from './interactionReply'; +import { deferInteraction, interactionReply } from './interactionReply'; const taskCanBeRepeated = (activity: Activity) => { if (activity.type === activity_type_enum.ClueCompletion) { @@ -693,6 +693,9 @@ export async function repeatTrip( interaction: ButtonInteraction, data: { data: Prisma.JsonValue; type: activity_type_enum } ) { + if (!data || !data.data || !data.type) { + return interactionReply(interaction, { content: "Couldn't find any trip to repeat.", ephemeral: true }); + } await deferInteraction(interaction); const handler = tripHandlers[data.type]; return runCommand({ diff --git a/src/mahoji/commands/bingo.ts b/src/mahoji/commands/bingo.ts index faa95659ce..6f56fdc656 100644 --- a/src/mahoji/commands/bingo.ts +++ b/src/mahoji/commands/bingo.ts @@ -13,9 +13,18 @@ import type { ItemBank } from 'oldschooljs/dist/meta/types'; import { production } from '../../config'; import { BLACKLISTED_USERS } from '../../lib/blacklists'; import { clImageGenerator } from '../../lib/collectionLogTask'; -import { BOT_TYPE, Emoji, usernameCache } from '../../lib/constants'; - -import { channelIsSendable, dateFm, isValidDiscordSnowflake, isValidNickname, md5sum, toKMB } from '../../lib/util'; +import { BOT_TYPE, Emoji } from '../../lib/constants'; + +import { + channelIsSendable, + dateFm, + getUsername, + getUsernameSync, + isValidDiscordSnowflake, + isValidNickname, + md5sum, + toKMB +} from '../../lib/util'; import { getItem } from '../../lib/util/getOSItem'; import { handleMahojiConfirmation } from '../../lib/util/handleMahojiConfirmation'; import { parseBank } from '../../lib/util/parseStringBank'; @@ -86,7 +95,7 @@ async function bingoTeamLeaderboard( .map( (team, j) => `${getPos(i, j)}** ${`${team.trophy?.emoji} ` ?? ''}${team.participants - .map(pt => usernameCache.get(pt.user_id) ?? '?') + .map(pt => getUsernameSync(pt.user_id)) .join(', ')}:** ${team.tilesCompletedCount.toLocaleString()}` ) .join('\n') @@ -804,9 +813,7 @@ The creator of the bingo (${userMention( teams .map(team => [ - team.participants - .map(u => usernameCache.get(u.user_id) ?? u.user_id) - .join(','), + team.participants.map(u => getUsernameSync(u.user_id)).join(','), team.tilesCompletedCount, team.trophy?.item.name ?? 'No Trophy' ].join('\t') @@ -946,22 +953,20 @@ Example: \`add_tile:Coal|Trout|Egg\` is a tile where you have to receive a coal ); for (const userID of team.participants.map(t => t.user_id)) { - const reclaimableItems: Prisma.ReclaimableItemCreateManyInput[] = trophiesToReceive.map( - trophy => ({ + const reclaimableItems: Prisma.ReclaimableItemCreateManyInput[] = await Promise.all( + trophiesToReceive.map(async trophy => ({ name: `Bingo Trophy (${trophy.item.name})`, quantity: 1, key: `bso-bingo-2-${trophy.item.id}`, item_id: trophy.item.id, description: `Awarded for placing in the top ${trophy.percentile}% of ${ bingo.title - }. Your team (${team.participants - .map(t => usernameCache.get(t.user_id) ?? t.user_id) - .join(', ')}) placed ${formatOrdinal(team.rank)} with ${ + }. Your team (${(await Promise.all(team.participants.map(async t => await getUsername(t.user_id)))).join(', ')}) placed ${formatOrdinal(team.rank)} with ${ team.tilesCompletedCount } tiles completed.`, date: bingo.endDate.toISOString(), user_id: userID - }) + })) ); toInsert.push(...reclaimableItems); } diff --git a/src/mahoji/commands/botleagues.ts b/src/mahoji/commands/botleagues.ts index 69b6ef567d..a6bda61cc3 100644 --- a/src/mahoji/commands/botleagues.ts +++ b/src/mahoji/commands/botleagues.ts @@ -169,13 +169,17 @@ ${leaguesTrophiesBuyables interaction, user, channelID, - chunk(result, 10).map(subList => - subList - .map( - ({ id, leagues_points_total }) => - `**${getUsername(id)}:** ${leagues_points_total.toLocaleString()} Pts` - ) - .join('\n') + await Promise.all( + chunk(result, 10).map(async subList => + ( + await Promise.all( + subList.map( + async ({ id, leagues_points_total }) => + `**${await getUsername(id)}:** ${leagues_points_total.toLocaleString()} Pts` + ) + ) + ).join('\n') + ) ), 'Leagues Points Leaderboard' ); diff --git a/src/mahoji/commands/config.ts b/src/mahoji/commands/config.ts index 91dcd090a1..46d145ebae 100644 --- a/src/mahoji/commands/config.ts +++ b/src/mahoji/commands/config.ts @@ -1,4 +1,4 @@ -import { hasBanMemberPerms, miniID } from '@oldschoolgg/toolkit'; +import { channelIsSendable, hasBanMemberPerms, miniID } from '@oldschoolgg/toolkit'; import type { CommandRunOptions } from '@oldschoolgg/toolkit'; import type { CommandResponse } from '@oldschoolgg/toolkit'; import type { activity_type_enum } from '@prisma/client'; @@ -14,6 +14,7 @@ import { BitField, ItemIconPacks, ParsedCustomEmojiWithGroups, PerkTier } from ' import { Eatables } from '../../lib/data/eatables'; import { CombatOptionsArray, CombatOptionsEnum } from '../../lib/minions/data/combatConstants'; +import { DynamicButtons } from '../../lib/DynamicButtons'; import { birdhouseSeeds } from '../../lib/skilling/skills/hunter/birdHouseTrapping'; import { autoslayChoices, slayerMasterChoices } from '../../lib/slayer/constants'; import { setDefaultAutoslay, setDefaultSlayerMaster } from '../../lib/slayer/slayerUtil'; @@ -21,7 +22,7 @@ import { BankSortMethods } from '../../lib/sorts'; import { formatDuration, isValidNickname, itemNameFromID, stringMatches } from '../../lib/util'; import { emojiServers } from '../../lib/util/cachedUserIDs'; import { getItem } from '../../lib/util/getOSItem'; -import { interactionReplyGetDuration } from '../../lib/util/interactionHelpers'; +import { deferInteraction } from '../../lib/util/interactionReply'; import { makeBankImage } from '../../lib/util/makeBankImage'; import { parseBank } from '../../lib/util/parseStringBank'; import { mahojiGuildSettingsFetch, mahojiGuildSettingsUpdate } from '../guildSettings'; @@ -88,32 +89,39 @@ const toggles: UserConfigToggle[] = [ return { result: true, message: 'Your Gambling lockout time has expired.' }; } else if (interaction) { const durations = [ - { display: '24 hours', duration: Time.Day }, + { display: '1 day', duration: Time.Day }, { display: '7 days', duration: Time.Day * 7 }, - { display: '2 weeks', duration: Time.Day * 14 }, { display: '1 month', duration: Time.Month }, - { display: '3 months', duration: Time.Month * 3 }, { display: '6 months', duration: Time.Month * 6 }, - { display: '1 year', duration: Time.Year }, - { display: '3 years', duration: Time.Year * 3 }, - { display: '5 years', duration: Time.Year * 5 } + { display: '1 year', duration: Time.Year } ]; - if (!production) { - durations.push({ display: '30 seconds', duration: Time.Second * 30 }); - durations.push({ display: '1 minute', duration: Time.Minute }); - durations.push({ display: '5 minutes', duration: Time.Minute * 5 }); + const channel = globalClient.channels.cache.get(interaction.channelId); + if (!channelIsSendable(channel)) return { result: false, message: 'Could not find channel.' }; + await deferInteraction(interaction); + const buttons = new DynamicButtons({ + channel: channel, + usersWhoCanInteract: [user.id], + deleteAfterConfirm: true + }); + for (const dur of durations) { + buttons.add({ + name: dur.display + }); } - const lockoutDuration = await interactionReplyGetDuration( - interaction, - `${user}, This will lockout your ability to gamble for the specified time. Choose carefully!`, - ...durations - ); + const pickedButton = await buttons.render({ + messageOptions: { + content: `${user}, This will lockout your ability to gamble for the specified time. Choose carefully!` + }, + isBusy: false + }); + + const pickedDuration = durations.find(d => stringMatches(d.display, pickedButton?.name ?? '')); - if (lockoutDuration !== false) { - await user.update({ gambling_lockout_expiry: new Date(Date.now() + lockoutDuration.duration) }); + if (pickedDuration) { + await user.update({ gambling_lockout_expiry: new Date(Date.now() + pickedDuration.duration) }); return { result: true, - message: `Locking out gambling for ${formatDuration(lockoutDuration.duration)}` + message: `Locking out gambling for ${formatDuration(pickedDuration.duration)}` }; } return { result: false, message: 'Cancelled.' }; diff --git a/src/mahoji/commands/data.ts b/src/mahoji/commands/data.ts index 2ba45ad0e3..4e1d5c88d4 100644 --- a/src/mahoji/commands/data.ts +++ b/src/mahoji/commands/data.ts @@ -30,7 +30,7 @@ export const dataCommand: OSBMahojiCommand = { ], run: async ({ interaction, options, userID }: CommandRunOptions<{ name: string }>) => { const user = await mUserFetch(userID); - deferInteraction(interaction); + await deferInteraction(interaction); return statsCommand(user, options.name); } }; diff --git a/src/mahoji/commands/leaderboard.ts b/src/mahoji/commands/leaderboard.ts index 6edf5b07fd..c07926510b 100644 --- a/src/mahoji/commands/leaderboard.ts +++ b/src/mahoji/commands/leaderboard.ts @@ -1,14 +1,13 @@ import { toTitleCase } from '@oldschoolgg/toolkit'; import type { CommandRunOptions } from '@oldschoolgg/toolkit'; -import type { Prisma } from '@prisma/client'; -import type { ChatInputCommandInteraction } from 'discord.js'; +import type { ChatInputCommandInteraction, MessageEditOptions } from 'discord.js'; import { EmbedBuilder } from 'discord.js'; import { ApplicationCommandOptionType } from 'discord.js'; -import { Time, calcWhatPercent, chunk, objectValues } from 'e'; +import { calcWhatPercent, chunk, isFunction } from 'e'; import type { ClueTier } from '../../lib/clues/clueTiers'; import { ClueTiers } from '../../lib/clues/clueTiers'; -import { Emoji, badges, badgesCache, masteryKey, usernameCache } from '../../lib/constants'; +import { masteryKey } from '../../lib/constants'; import { allClNames, getCollectionItems } from '../../lib/data/Collections'; import { effectiveMonsters } from '../../lib/minions/data/killableMonsters'; import { allOpenables } from '../../lib/openables'; @@ -23,9 +22,9 @@ import { convertXPtoLVL, formatDuration, getUsername, + getUsernameSync, makePaginatedMessage, - stringMatches, - stripEmojis + stringMatches } from '../../lib/util'; import { fetchCLLeaderboard } from '../../lib/util/clLeaderboard'; import { deferInteraction } from '../../lib/util/interactionReply'; @@ -48,11 +47,12 @@ export function getPos(page: number, record: number) { return `${page * LB_PAGE_SIZE + 1 + record}. `; } +export type AsyncPageString = () => Promise; export async function doMenu( interaction: ChatInputCommandInteraction, user: MUser, channelID: string, - pages: string[], + pages: string[] | AsyncPageString[], title: string ) { if (pages.length === 0) { @@ -63,11 +63,68 @@ export async function doMenu( makePaginatedMessage( channel, - pages.map(p => ({ embeds: [new EmbedBuilder().setTitle(title).setDescription(p)] })), + pages.map(p => { + if (isFunction(p)) { + return async () => ({ embeds: [new EmbedBuilder().setTitle(title).setDescription(await p())] }); + } + + return { embeds: [new EmbedBuilder().setTitle(title).setDescription(p)] }; + }), user.id ); } +function doMenuWrapper({ + user, + channelID, + users, + title, + ironmanOnly, + formatter +}: { + ironmanOnly: boolean; + users: { id: string; score: number }[]; + title: string; + interaction: ChatInputCommandInteraction; + user: MUser; + channelID: string; + formatter?: (val: number) => string; +}) { + const chunked = chunk(users, LB_PAGE_SIZE); + const pages: (() => Promise)[] = []; + for (let c = 0; c < chunked.length; c++) { + const makePage = async () => { + const chnk = chunked[c]; + const unwaited = chnk.map( + async (user, i) => + `${getPos(c, i)}**${await getUsername(user.id)}:** ${formatter ? formatter(user.score) : user.score.toLocaleString()}` + ); + const pageText = (await Promise.all(unwaited)).join('\n'); + return { embeds: [new EmbedBuilder().setTitle(title).setDescription(pageText)] }; + }; + pages.push(makePage); + } + if (pages.length === 0) { + return 'There are no users on this leaderboard.'; + } + const channel = globalClient.channels.cache.get(channelID); + if (!channelIsSendable(channel)) return 'Invalid channel.'; + + makePaginatedMessage( + channel, + pages.map(p => { + if (isFunction(p)) { + return p; + } + + return { embeds: [new EmbedBuilder().setTitle(title).setDescription(p)] }; + }), + user.id + ); + + return lbMsg(title, ironmanOnly); +} + async function kcLb( interaction: ChatInputCommandInteraction, user: MUser, @@ -77,8 +134,8 @@ async function kcLb( ) { const monster = effectiveMonsters.find(mon => [mon.name, ...mon.aliases].some(alias => stringMatches(alias, name))); if (!monster) return "That's not a valid monster!"; - const list = await prisma.$queryRawUnsafe<{ id: string; kc: number }[]>( - `SELECT user_id::text AS id, CAST("monster_scores"->>'${monster.id}' AS INTEGER) as kc + const list = await prisma.$queryRawUnsafe<{ id: string; score: number }[]>( + `SELECT user_id::text AS id, CAST("monster_scores"->>'${monster.id}' AS INTEGER) as score FROM user_stats ${ironmanOnly ? 'INNER JOIN "users" on "users"."id" = "user_stats"."user_id"::text' : ''} WHERE CAST("monster_scores"->>'${monster.id}' AS INTEGER) > 5 @@ -87,19 +144,14 @@ async function kcLb( LIMIT 2000;` ); - doMenu( - interaction, + return doMenuWrapper({ + ironmanOnly, user, + interaction, channelID, - chunk(list, LB_PAGE_SIZE).map((subList, i) => - subList - .map((user, j) => `${getPos(i, j)}**${getUsername(user.id)}:** ${user.kc.toLocaleString()}`) - .join('\n') - ), - `KC Leaderboard for ${monster.name}` - ); - - return lbMsg(`${monster.name} KC `, ironmanOnly); + users: list, + title: `KC Leaderboard for ${monster.name}` + }); } async function farmingContractLb( @@ -124,7 +176,7 @@ async function farmingContractLb( chunk(list, LB_PAGE_SIZE).map((subList, i) => subList .map(({ id, count }, j) => { - return `${getPos(i, j)}**${getUsername(id)}:** ${count.toLocaleString()}`; + return `${getPos(i, j)}**${getUsernameSync(id)}:** ${count.toLocaleString()}`; }) .join('\n') ), @@ -147,7 +199,7 @@ LIMIT 10;`); } return `**Inferno Records**\n\n${res - .map((e, i) => `${i + 1}. **${getUsername(e.user_id)}:** ${formatDuration(e.duration)}`) + .map((e, i) => `${i + 1}. **${getUsernameSync(e.user_id)}:** ${formatDuration(e.duration)}`) .join('\n')}`; } @@ -176,7 +228,10 @@ async function sacrificeLb( channelID, chunk(list, LB_PAGE_SIZE).map((subList, i) => subList - .map(({ id, amount }, j) => `${getPos(i, j)}**${getUsername(id)}:** ${amount.toLocaleString()} GP `) + .map( + ({ id, amount }, j) => + `${getPos(i, j)}**${getUsernameSync(id)}:** ${amount.toLocaleString()} GP ` + ) .join('\n') ), 'Sacrifice Leaderboard' @@ -202,7 +257,7 @@ async function sacrificeLb( subList .map( ({ id, sacbanklength }, j) => - `${getPos(i, j)}**${getUsername(id)}:** ${sacbanklength.toLocaleString()} Unique Sac's` + `${getPos(i, j)}**${getUsernameSync(id)}:** ${sacbanklength.toLocaleString()} Unique Sac's` ) .join('\n') ), @@ -231,7 +286,7 @@ async function minigamesLb(interaction: ChatInputCommandInteraction, user: MUser channelID, chunk(titheCompletions, LB_PAGE_SIZE).map((subList, i) => subList - .map(({ id, amount }, j) => `${getPos(i, j)}**${getUsername(id)}:** ${amount.toLocaleString()}`) + .map(({ id, amount }, j) => `${getPos(i, j)}**${getUsernameSync(id)}:** ${amount.toLocaleString()}`) .join('\n') ), 'Tithe farm Leaderboard' @@ -250,18 +305,14 @@ async function minigamesLb(interaction: ChatInputCommandInteraction, user: MUser take: 10 }); - doMenu( - interaction, + return doMenuWrapper({ + ironmanOnly: false, user, + interaction, channelID, - chunk(res, LB_PAGE_SIZE).map((subList, i) => - subList - .map((u, j) => `${getPos(i, j)}**${getUsername(u.user_id)}:** ${u[minigame.column].toLocaleString()}`) - .join('\n') - ), - `${minigame.name} Leaderboard` - ); - return lbMsg(`${minigame.name} Leaderboard`); + users: res.map(u => ({ id: u.user_id, score: u[minigame.column] })), + title: `${minigame.name} Leaderboard` + }); } async function clLb( @@ -288,24 +339,16 @@ async function clLb( const users = await fetchCLLeaderboard({ ironmenOnly, items, resultLimit: 200, userEvents: userEventOrders }); inputType = toTitleCase(inputType.toLowerCase()); - doMenu( - interaction, + + return doMenuWrapper({ + ironmanOnly: ironmenOnly, user, + interaction, channelID, - chunk(users, LB_PAGE_SIZE).map((subList, i) => - subList - .map( - ({ id, qty }, j) => - `${getPos(i, j)}**${getUsername(id)}:** ${qty.toLocaleString()} (${calcWhatPercent( - qty, - items.length - ).toFixed(1)}%)` - ) - .join('\n') - ), - `${inputType} Collection Log Leaderboard (${items.length} slots)` - ); - return lbMsg(`${inputType} Collection Log Leaderboard`, ironmenOnly); + users: users.map(u => ({ id: u.id, score: u.qty })), + title: `${inputType} Collection Log Leaderboard`, + formatter: val => `${val.toLocaleString()} (${calcWhatPercent(val, items.length).toFixed(1)}%)` + }); } async function creaturesLb( @@ -332,7 +375,7 @@ async function creaturesLb( channelID, chunk(data, LB_PAGE_SIZE).map((subList, i) => subList - .map(({ id, count }, j) => `${getPos(i, j)}**${getUsername(id)}:** ${count.toLocaleString()}`) + .map(({ id, count }, j) => `${getPos(i, j)}**${getUsernameSync(id)}:** ${count.toLocaleString()}`) .join('\n') ), `Catch Leaderboard for ${creature.name}` @@ -357,7 +400,7 @@ async function lapsLb(interaction: ChatInputCommandInteraction, user: MUser, cha channelID, chunk(data, LB_PAGE_SIZE).map((subList, i) => subList - .map(({ id, count }, j) => `${getPos(i, j)}**${getUsername(id)}:** ${count.toLocaleString()}`) + .map(({ id, count }, j) => `${getPos(i, j)}**${getUsernameSync(id)}:** ${count.toLocaleString()}`) .join('\n') ), `${course.name} Laps Leaderboard` @@ -403,18 +446,14 @@ async function openLb( ORDER BY qty DESC LIMIT 30;` ); - doMenu( - interaction, + return doMenuWrapper({ + ironmanOnly, user, + interaction, channelID, - chunk(list, LB_PAGE_SIZE).map((subList, i) => - subList - .map((user, j) => `${getPos(i, j)}**${getUsername(user.id)}:** ${user.qty.toLocaleString()}`) - .join('\n') - ), - `Open Leaderboard for ${openableName}` - ); - return lbMsg(`${openableName} Opening`); + users: list.map(u => ({ id: u.id, score: u.qty })), + title: `${openableName} Opening Leaderboard` + }); } async function gpLb(interaction: ChatInputCommandInteraction, user: MUser, channelID: string, ironmanOnly: boolean) { @@ -425,22 +464,19 @@ async function gpLb(interaction: ChatInputCommandInteraction, user: MUser, chann WHERE "GP" > 1000000 ${ironmanOnly ? ' AND "minion.ironman" = true ' : ''} ORDER BY "GP" DESC - LIMIT 500;` + LIMIT 100;` ) - ).map(res => ({ ...res, GP: Number(res.GP) })); + ).map(res => ({ ...res, score: Number(res.GP) })); - doMenu( - interaction, + return doMenuWrapper({ + ironmanOnly, user, + interaction, channelID, - chunk(users, LB_PAGE_SIZE).map((subList, i) => - subList - .map(({ id, GP }, j) => `${getPos(i, j)}**${getUsername(id)}:** ${GP.toLocaleString()} GP`) - .join('\n') - ), - 'GP Leaderboard' - ); - return lbMsg('GP Leaderboard', ironmanOnly); + users, + title: 'GP Leaderboard', + formatter: val => `${val.toLocaleString()} GP` + }); } async function skillsLb( @@ -575,7 +611,7 @@ async function skillsLb( chunk(overallUsers, LB_PAGE_SIZE).map((subList, i) => subList .map((obj, j) => { - return `${getPos(i, j)}**${getUsername( + return `${getPos(i, j)}**${getUsernameSync( obj.id )}:** ${obj.totalLevel.toLocaleString()} (${obj.totalXP.toLocaleString()} XP)`; }) @@ -596,7 +632,7 @@ async function skillsLb( const objKey = `skills.${skill?.id}`; const skillXP = Number(obj[objKey] ?? 0); - return `${getPos(i, j)}**${getUsername(obj.id)}:** ${skillXP.toLocaleString()} XP (${convertXPtoLVL( + return `${getPos(i, j)}**${getUsernameSync(obj.id)}:** ${skillXP.toLocaleString()} XP (${convertXPtoLVL( skillXP )})`; }) @@ -635,7 +671,10 @@ LIMIT 50;` channelID, chunk(users, LB_PAGE_SIZE).map((subList, i) => subList - .map(({ id, score }, j) => `${getPos(i, j)}**${getUsername(id)}:** ${score.toLocaleString()} Completed`) + .map( + ({ id, score }, j) => + `${getPos(i, j)}**${getUsernameSync(id)}:** ${score.toLocaleString()} Completed` + ) .join('\n') ), `${clueTier.name} Clue Leaderboard` @@ -643,105 +682,6 @@ LIMIT 50;` return lbMsg('Clue Leaderboard', ironmanOnly); } -export async function cacheUsernames() { - const roboChimpUseresToCache = await roboChimpClient.user.findMany({ - where: { - OR: [ - { - osb_cl_percent: { - gte: 80 - } - }, - { - bso_total_level: { - gte: 80 - } - }, - { - osb_total_level: { - gte: 1500 - } - }, - { - bso_total_level: { - gte: 1500 - } - }, - { - leagues_points_total: { - gte: 20_000 - } - } - ] - }, - select: { - id: true - } - }); - - const orConditions: Prisma.UserWhereInput[] = []; - for (const skill of objectValues(SkillsEnum)) { - orConditions.push({ - [`skills_${skill}`]: { - gte: 15_000_000 - } - }); - } - const usersToCache = await prisma.user.findMany({ - where: { - OR: [ - ...orConditions, - { - last_command_date: { - gt: new Date(Date.now() - Number(Time.Month)) - } - } - ], - id: { - notIn: roboChimpUseresToCache.map(i => i.id.toString()) - } - }, - select: { - id: true - } - }); - - const userIDsToCache = [...usersToCache, ...roboChimpUseresToCache].map(i => i.id.toString()); - debugLog(`Caching usernames of ${userIDsToCache.length} users`); - - const allNewUsers = await prisma.newUser.findMany({ - where: { - username: { - not: null - }, - id: { - in: userIDsToCache - } - }, - select: { - id: true, - username: true - } - }); - - const arrayOfIronmenAndBadges: { badges: number[]; id: string; ironman: boolean }[] = await prisma.$queryRawUnsafe( - 'SELECT "badges", "id", "minion.ironman" as "ironman" FROM users WHERE ARRAY_LENGTH(badges, 1) > 0 OR "minion.ironman" = true;' - ); - - for (const user of allNewUsers) { - const badgeUser = arrayOfIronmenAndBadges.find(i => i.id === user.id); - const name = stripEmojis(user.username!); - usernameCache.set(user.id, name); - if (badgeUser) { - const rawBadges = badgeUser.badges.map(num => badges[num]); - if (badgeUser.ironman) { - rawBadges.push(Emoji.Ironman); - } - badgesCache.set(user.id, rawBadges.join(' ')); - } - } -} - const globalLbTypes = ['xp', 'cl', 'mastery'] as const; type GlobalLbType = (typeof globalLbTypes)[number]; async function globalLb(interaction: ChatInputCommandInteraction, user: MUser, channelID: string, type: GlobalLbType) { @@ -772,7 +712,7 @@ LIMIT 10; subList .map( ({ id, osb_xp_percent, bso_xp_percent }, j) => - `${getPos(i, j)}**${getUsername(id)}:** ${osb_xp_percent.toFixed( + `${getPos(i, j)}**${getUsernameSync(id)}:** ${osb_xp_percent.toFixed( 2 )}% OSB, ${bso_xp_percent.toFixed(2)}% BSO` ) @@ -800,7 +740,9 @@ LIMIT 10; user, channelID, chunk(result, LB_PAGE_SIZE).map((subList, i) => - subList.map(({ id, avg }, j) => `${getPos(i, j)}**${getUsername(id)}:** ${avg.toFixed(2)}%`).join('\n') + subList + .map(({ id, avg }, j) => `${getPos(i, j)}**${getUsernameSync(id)}:** ${avg.toFixed(2)}%`) + .join('\n') ), 'Global (OSB+BSO) Mastery Leaderboard' ); @@ -821,7 +763,7 @@ LIMIT 20;`; subList .map( ({ id, total_cl_percent }, j) => - `${getPos(i, j)}**${getUsername(id)}:** ${total_cl_percent.toLocaleString()}%` + `${getPos(i, j)}**${getUsernameSync(id)}:** ${total_cl_percent.toLocaleString()}%` ) .join('\n') ), @@ -901,7 +843,7 @@ LIMIT 10; subList .map( ({ user_id, cl_completion_count, cl_global_rank, count_increase, rank_difference }, j) => - `${getPos(i, j)}**${getUsername( + `${getPos(i, j)}**${getUsernameSync( user_id )}:** Gained ${count_increase} CL slots, from ${cl_completion_count} to ${ cl_completion_count + count_increase @@ -932,7 +874,8 @@ LIMIT 50;` chunk(users, LB_PAGE_SIZE).map((subList, i) => subList .map( - ({ id, qty }, j) => `${getPos(i, j)}**${getUsername(id)}:** ${qty.toLocaleString()} Tasks Completed` + ({ id, qty }, j) => + `${getPos(i, j)}**${getUsernameSync(id)}:** ${qty.toLocaleString()} Tasks Completed` ) .join('\n') ), @@ -942,36 +885,32 @@ LIMIT 50;` } async function masteryLb(interaction: ChatInputCommandInteraction, user: MUser, channelID: string) { - const users = await roboChimpClient.user.findMany({ - where: { - [masteryKey]: { not: null } - }, - orderBy: { - [masteryKey]: 'desc' - }, - take: 50, - select: { - id: true, - osb_mastery: true, - bso_mastery: true - } - }); + const users = ( + await roboChimpClient.user.findMany({ + where: { + [masteryKey]: { not: null } + }, + orderBy: { + [masteryKey]: 'desc' + }, + take: 50, + select: { + id: true, + osb_mastery: true, + bso_mastery: true + } + }) + ).map(u => ({ id: u.id.toString(), score: u[masteryKey] ?? 0 })); - doMenu( + return doMenuWrapper({ interaction, - user, + title: 'Mastery Leaderboard', channelID, - chunk(users, LB_PAGE_SIZE).map((subList, i) => - subList - .map( - (lUser, j) => - `${getPos(i, j)}**${getUsername(lUser.id)}:** ${lUser[masteryKey]?.toFixed(3)}% mastery` - ) - .join('\n') - ), - 'Mastery Leaderboard' - ); - return lbMsg('Mastery Leaderboard'); + ironmanOnly: false, + user, + users, + formatter: val => `${val.toFixed(3)}% mastery` + }); } const ironmanOnlyOption = { diff --git a/src/mahoji/commands/minion.ts b/src/mahoji/commands/minion.ts index c136cc9b88..ef993a5d43 100644 --- a/src/mahoji/commands/minion.ts +++ b/src/mahoji/commands/minion.ts @@ -12,7 +12,6 @@ import { FormattedCustomEmoji, MAX_LEVEL, PerkTier, - badges, minionActivityCache } from '../../lib/constants'; import { degradeableItems } from '../../lib/degradeableItems'; @@ -82,20 +81,18 @@ export async function getUserInfo(user: MUser) { const task = minionActivityCache.get(user.id); const taskText = task ? `${task.type}` : 'None'; - const userBadges = user.user.badges.map(i => badges[i]); - const premiumDate = Number(user.user.premium_balance_expiry_date); const premiumTier = user.user.premium_balance_tier; const result = { perkTier: user.perkTier(), isBlacklisted: BLACKLISTED_USERS.has(user.id), - badges: userBadges, + badges: user.badgesString, mainAccount: user.user.main_account !== null - ? `${getUsername(user.user.main_account)}[${user.user.main_account}]` + ? `${user.user.username ?? 'Unknown Username'}[${user.user.main_account}]` : 'None', - ironmanAlts: user.user.ironman_alts.map(id => `${getUsername(id)}[${id}]`), + ironmanAlts: user.user.ironman_alts.map(async id => `${await getUsername(id)}[${id}]`), premiumBalance: `${premiumDate ? new Date(premiumDate).toLocaleString() : ''} ${ premiumTier ? `Tier ${premiumTier}` : '' }`, @@ -116,7 +113,7 @@ export async function getUserInfo(user: MUser) { **Current Trip:** ${taskText} **Perk Tier:** ${result.perkTier} **Blacklisted:** ${result.isBlacklisted} -**Badges:** ${result.badges.join(' ')} +**Badges:** ${result.badges} **Main Account:** ${result.mainAccount} **Ironman Alts:** ${result.ironmanAlts} **Patron Balance:** ${result.premiumBalance} diff --git a/src/mahoji/commands/rp.ts b/src/mahoji/commands/rp.ts index 32a11433f3..c000ca9c45 100644 --- a/src/mahoji/commands/rp.ts +++ b/src/mahoji/commands/rp.ts @@ -1,12 +1,12 @@ import { toTitleCase } from '@oldschoolgg/toolkit'; import type { CommandRunOptions } from '@oldschoolgg/toolkit'; import type { MahojiUserOption } from '@oldschoolgg/toolkit'; -import { UserEventType, xp_gains_skill_enum } from '@prisma/client'; +import { type Prisma, UserEventType, xp_gains_skill_enum } from '@prisma/client'; import { DiscordSnowflake } from '@sapphire/snowflake'; import { Duration } from '@sapphire/time-utilities'; import { SnowflakeUtil, codeBlock } from 'discord.js'; import { ApplicationCommandOptionType } from 'discord.js'; -import { Time, randArrItem, sumArr } from 'e'; +import { Time, objectValues, randArrItem, sumArr } from 'e'; import { Bank } from 'oldschooljs'; import type { Item } from 'oldschooljs/dist/meta/types'; @@ -24,6 +24,7 @@ import { allPerkBitfields } from '../../lib/perkTiers'; import { premiumPatronTime } from '../../lib/premiumPatronTime'; import { TeamLoot } from '../../lib/simulation/TeamLoot'; +import { SkillsEnum } from '../../lib/skilling/types'; import type { ItemBank } from '../../lib/types'; import { dateFm, isValidDiscordSnowflake, returnStringOrFile } from '../../lib/util'; import getOSItem from '../../lib/util/getOSItem'; @@ -31,6 +32,7 @@ import { handleMahojiConfirmation } from '../../lib/util/handleMahojiConfirmatio import { deferInteraction } from '../../lib/util/interactionReply'; import itemIsTradeable from '../../lib/util/itemIsTradeable'; import { syncLinkedAccounts } from '../../lib/util/linkedAccountsUtil'; +import { makeBadgeString } from '../../lib/util/makeBadgeString'; import { makeBankImage } from '../../lib/util/makeBankImage'; import { migrateUser } from '../../lib/util/migrateUser'; import { parseBank } from '../../lib/util/parseStringBank'; @@ -51,6 +53,104 @@ const itemFilters = [ } ]; +async function redisSync() { + const roboChimpUsersToCache = ( + await roboChimpClient.user.findMany({ + where: { + OR: [ + { + osb_cl_percent: { + gte: 80 + } + }, + { + bso_total_level: { + gte: 80 + } + }, + { + osb_total_level: { + gte: 1500 + } + }, + { + bso_total_level: { + gte: 1500 + } + }, + { + leagues_points_total: { + gte: 20_000 + } + } + ] + }, + select: { + id: true + } + }) + ).map(i => i.id.toString()); + + const orConditions: Prisma.UserWhereInput[] = []; + for (const skill of objectValues(SkillsEnum)) { + orConditions.push({ + [`skills_${skill}`]: { + gte: 15_000_000 + } + }); + } + const usersToCache = ( + await prisma.user.findMany({ + where: { + OR: [ + ...orConditions, + { + last_command_date: { + gt: new Date(Date.now() - Number(Time.Month)) + } + } + ], + id: { + notIn: roboChimpUsersToCache + } + }, + select: { + id: true + } + }) + ).map(i => i.id); + + const response: string[] = []; + const allNewUsers = await prisma.newUser.findMany({ + where: { + username: { + not: null + }, + id: { + in: [...usersToCache, ...roboChimpUsersToCache] + } + }, + select: { + id: true, + username: true + } + }); + + for (const user of allNewUsers) { + redis.setUser(user.id, { username: user.username }); + } + response.push(`Cached ${allNewUsers.length} usernames.`); + + const arrayOfIronmenAndBadges: { badges: number[]; id: string; ironman: boolean }[] = await prisma.$queryRawUnsafe( + 'SELECT "badges", "id", "minion.ironman" as "ironman" FROM users WHERE ARRAY_LENGTH(badges, 1) > 0 OR "minion.ironman" = true;' + ); + for (const user of arrayOfIronmenAndBadges) { + redis.setUser(user.id, { osb_badges: makeBadgeString(user.badges, user.ironman) }); + } + response.push(`Cached ${arrayOfIronmenAndBadges.length} badges.`); + return response.join(', '); +} + function isProtectedAccount(user: MUser) { const botAccounts = ['303730326692429825', '729244028989603850', '969542224058654790']; if ([...ADMIN_IDS, ...OWNER_IDS, ...botAccounts].includes(user.id)) return true; @@ -97,6 +197,12 @@ export const rpCommand: OSBMahojiCommand = { name: 'networth_sync', description: 'networth_sync.', options: [] + }, + { + type: ApplicationCommandOptionType.Subcommand, + name: 'redis_sync', + description: 'redis sync.', + options: [] } ] }, @@ -455,6 +561,7 @@ export const rpCommand: OSBMahojiCommand = { view_all_items?: {}; analytics_tick?: {}; networth_sync?: {}; + redis_sync?: {}; }; player?: { viewbank?: { user: MahojiUserOption; json?: boolean }; @@ -563,6 +670,10 @@ Date: ${dateFm(date)}`; await analyticsTick(); return 'Finished.'; } + if (options.action?.redis_sync) { + const result = await redisSync(); + return result; + } if (options.action?.networth_sync) { const users = await prisma.user.findMany({ where: { diff --git a/src/mahoji/commands/tools.ts b/src/mahoji/commands/tools.ts index f375368be0..d86763f7c4 100644 --- a/src/mahoji/commands/tools.ts +++ b/src/mahoji/commands/tools.ts @@ -174,9 +174,14 @@ async function clueGains(interval: string, tier?: string, ironmanOnly?: boolean) const embed = new EmbedBuilder() .setTitle(title) .setDescription( - res - .map((i: any) => `${++place}. **${getUsername(i.user_id)}**: ${Number(i.qty).toLocaleString()}`) - .join('\n') + ( + await Promise.all( + res.map( + async (i: any) => + `${++place}. **${await getUsername(i.user_id)}**: ${Number(i.qty).toLocaleString()}` + ) + ) + ).join('\n') ); return { embeds: [embed] }; @@ -249,12 +254,14 @@ async function xpGains(interval: string, skill?: string, ironmanOnly?: boolean) const embed = new EmbedBuilder() .setTitle(`Highest ${skillObj ? skillObj.name : 'Overall'} XP Gains in the past ${interval}`) .setDescription( - xpRecords - .map( - record => - `${++place}. **${getUsername(record.user)}**: ${Number(record.total_xp).toLocaleString()} XP` + ( + await Promise.all( + xpRecords.map( + async record => + `${++place}. **${await getUsername(record.user)}**: ${Number(record.total_xp).toLocaleString()} XP` + ) ) - .join('\n') + ).join('\n') ); return { embeds: [embed] }; @@ -305,9 +312,14 @@ async function kcGains(interval: string, monsterName: string, ironmanOnly?: bool const embed = new EmbedBuilder() .setTitle(`Highest ${monster.name} KC gains in the past ${interval}`) .setDescription( - res - .map((i: any) => `${++place}. **${getUsername(i.user_id)}**: ${Number(i.qty).toLocaleString()}`) - .join('\n') + ( + await Promise.all( + res.map( + async (i: any) => + `${++place}. **${await getUsername(i.user_id)}**: ${Number(i.qty).toLocaleString()}` + ) + ) + ).join('\n') ); return { embeds: [embed] }; @@ -601,9 +613,11 @@ async function dryStreakCommand(monsterName: string, itemName: string, ironmanOn if (result.length === 0) return 'No results found.'; if (typeof result === 'string') return result; - return `**Dry Streaks for ${item.name} from ${entity.name}:**\n${result - .map(({ id, val }) => `${getUsername(id)}: ${entity.format(val || -1)}`) - .join('\n')}`; + return `**Dry Streaks for ${item.name} from ${entity.name}:**\n${( + await Promise.all( + result.map(async ({ id, val }) => `${await getUsername(id)}: ${entity.format(val || -1)}`) + ) + ).join('\n')}`; } const mon = effectiveMonsters.find(mon => mon.aliases.some(alias => stringMatches(alias, monsterName))); @@ -633,9 +647,13 @@ async function dryStreakCommand(monsterName: string, itemName: string, ironmanOn if (result.length === 0) return 'No results found.'; - return `**Dry Streaks for ${item.name} from ${mon.name}:**\n${result - .map(({ id, KC }) => `${getUsername(id) as string}: ${Number.parseInt(KC).toLocaleString()}`) - .join('\n')}`; + return `**Dry Streaks for ${item.name} from ${mon.name}:**\n${( + await Promise.all( + result.map( + async ({ id, KC }) => `${(await getUsername(id)) as string}: ${Number.parseInt(KC).toLocaleString()}` + ) + ) + ).join('\n')}`; } async function mostDrops(user: MUser, itemName: string, filter: string) { @@ -663,12 +681,14 @@ async function mostDrops(user: MUser, itemName: string, filter: string) { if (result.length === 0) return 'No results found.'; - return `**Most '${item.name}' received:**\n${result - .map( - ({ id, qty }) => - `${result.length < 10 ? '(Anonymous)' : getUsername(id)}: ${Number.parseInt(qty).toLocaleString()}` + return `**Most '${item.name}' received:**\n${( + await Promise.all( + result.map( + async ({ id, qty }) => + `${result.length < 10 ? '(Anonymous)' : await getUsername(id)}: ${Number.parseInt(qty).toLocaleString()}` + ) ) - .join('\n')}`; + ).join('\n')}`; } async function checkMassesCommand(guildID: string | undefined) { diff --git a/src/mahoji/lib/abstracted_commands/luckyPickCommand.ts b/src/mahoji/lib/abstracted_commands/luckyPickCommand.ts index a2eb72196c..e6712eb792 100644 --- a/src/mahoji/lib/abstracted_commands/luckyPickCommand.ts +++ b/src/mahoji/lib/abstracted_commands/luckyPickCommand.ts @@ -147,7 +147,9 @@ export async function luckyPickCommand(user: MUser, luckypickamount: string, int button: ButtonInstance; }) => { const amountReceived = Math.floor(button.mod(amount)); - await user.addItemsToBank({ items: new Bank().add('Coins', amountReceived) }); + if (amountReceived > 0) { + await user.addItemsToBank({ items: new Bank().add('Coins', amountReceived) }); + } await updateClientGPTrackSetting('gp_luckypick', amountReceived - amount); await updateGPTrackSetting('gp_luckypick', amountReceived - amount, user); await sentMessage.edit({ components: getCurrentButtons({ showTrueNames: true }) }).catch(noOp); diff --git a/src/mahoji/lib/abstracted_commands/slayerTaskCommand.ts b/src/mahoji/lib/abstracted_commands/slayerTaskCommand.ts index 69798b7833..a3ee9ebf69 100644 --- a/src/mahoji/lib/abstracted_commands/slayerTaskCommand.ts +++ b/src/mahoji/lib/abstracted_commands/slayerTaskCommand.ts @@ -6,6 +6,7 @@ import { Monsters } from 'oldschooljs'; import killableMonsters from '../../../lib/minions/data/killableMonsters'; +import { InteractionID } from '../../../lib/InteractionID'; import { runCommand } from '../../../lib/settings/settings'; import { slayerMasters } from '../../../lib/slayer/slayerMasters'; import { SlayerRewardsShop } from '../../../lib/slayer/slayerUnlocks'; @@ -29,39 +30,34 @@ const returnSuccessButtons = [ new ButtonBuilder({ label: 'Autoslay (Saved)', style: ButtonStyle.Secondary, - customId: 'assaved' + customId: InteractionID.Slayer.AutoSlaySaved }), new ButtonBuilder({ label: 'Autoslay (Default)', style: ButtonStyle.Secondary, - customId: 'asdef' + customId: InteractionID.Slayer.AutoSlayDefault }), new ButtonBuilder({ label: 'Autoslay (EHP)', style: ButtonStyle.Secondary, - customId: 'asehp' + customId: InteractionID.Slayer.AutoSlayEHP }), new ButtonBuilder({ label: 'Autoslay (Boss)', style: ButtonStyle.Secondary, - customId: 'asboss' + customId: InteractionID.Slayer.AutoSlayBoss }) ]), new ActionRowBuilder().addComponents([ new ButtonBuilder({ label: 'Cancel Task + New (30 points)', style: ButtonStyle.Danger, - customId: 'skip' + customId: InteractionID.Slayer.SkipTask }), new ButtonBuilder({ label: 'Block Task + New (100 points)', style: ButtonStyle.Danger, - customId: 'block' - }), - new ButtonBuilder({ - label: 'Do Nothing', - style: ButtonStyle.Secondary, - customId: 'doNothing' + customId: InteractionID.Slayer.BlockTask }) ]) ]; @@ -148,7 +144,7 @@ async function returnSuccess(channelID: string, user: MUser, content: string) { }); if (!selection.isButton()) return; switch (selection.customId) { - case 'assaved': { + case InteractionID.Slayer.AutoSlaySaved: { await runCommand({ commandName: 'slayer', args: { autoslay: {} }, @@ -158,7 +154,7 @@ async function returnSuccess(channelID: string, user: MUser, content: string) { }); return; } - case 'asdef': { + case InteractionID.Slayer.AutoSlayDefault: { await runCommand({ commandName: 'slayer', args: { autoslay: { mode: 'default' } }, @@ -168,7 +164,7 @@ async function returnSuccess(channelID: string, user: MUser, content: string) { }); return; } - case 'asehp': { + case InteractionID.Slayer.AutoSlayEHP: { await runCommand({ commandName: 'slayer', args: { autoslay: { mode: 'ehp' } }, @@ -178,7 +174,7 @@ async function returnSuccess(channelID: string, user: MUser, content: string) { }); return; } - case 'asboss': { + case InteractionID.Slayer.AutoSlayBoss: { await runCommand({ commandName: 'slayer', args: { autoslay: { mode: 'boss' } }, @@ -188,7 +184,7 @@ async function returnSuccess(channelID: string, user: MUser, content: string) { }); return; } - case 'skip': { + case InteractionID.Slayer.SkipTask: { await runCommand({ commandName: 'slayer', args: { manage: { command: 'skip', new: true } }, @@ -198,7 +194,7 @@ async function returnSuccess(channelID: string, user: MUser, content: string) { }); return; } - case 'block': { + case InteractionID.Slayer.BlockTask: { await runCommand({ commandName: 'slayer', args: { manage: { command: 'block', new: true } }, @@ -209,6 +205,11 @@ async function returnSuccess(channelID: string, user: MUser, content: string) { } } } catch (err: unknown) { + if ((err as any).message === 'time') return; + logError(err, { + user_id: user.id.toString(), + channel_id: channelID + }); } finally { await sentMessage.edit({ components: [] }); } diff --git a/src/mahoji/lib/abstracted_commands/statCommand.ts b/src/mahoji/lib/abstracted_commands/statCommand.ts index 5b98f8ae52..8d4092a356 100644 --- a/src/mahoji/lib/abstracted_commands/statCommand.ts +++ b/src/mahoji/lib/abstracted_commands/statCommand.ts @@ -288,6 +288,9 @@ GROUP BY data->>'collectableID';`); async function makeResponseForBank(bank: Bank, title: string, content?: string) { sanitizeBank(bank); + if (bank.length === 0) { + return { content: 'No results.' }; + } const image = await makeBankImage({ title, bank @@ -1213,20 +1216,24 @@ LIMIT 5;` }[][]; const response = `**Luckiest CoX Raiders** -${luckiest - .map( - i => - `${getUsername(i.id)}: ${i.points_per_item.toLocaleString()} points per item / 1 in ${(i.raids_total_kc / i.total_cox_items).toFixed(1)} raids` +${( + await Promise.all( + luckiest.map( + async i => + `${await getUsername(i.id)}: ${i.points_per_item.toLocaleString()} points per item / 1 in ${(i.raids_total_kc / i.total_cox_items).toFixed(1)} raids` + ) ) - .join('\n')} +).join('\n')} **Unluckiest CoX Raiders** -${unluckiest - .map( - i => - `${getUsername(i.id)}: ${i.points_per_item.toLocaleString()} points per item / 1 in ${(i.raids_total_kc / i.total_cox_items).toFixed(1)} raids` +${( + await Promise.all( + unluckiest.map( + async i => + `${await getUsername(i.id)}: ${i.points_per_item.toLocaleString()} points per item / 1 in ${(i.raids_total_kc / i.total_cox_items).toFixed(1)} raids` + ) ) - .join('\n')}`; +).join('\n')}`; return { content: response }; diff --git a/src/mahoji/lib/events.ts b/src/mahoji/lib/events.ts index db954c8fd4..43f5cee05c 100644 --- a/src/mahoji/lib/events.ts +++ b/src/mahoji/lib/events.ts @@ -1,7 +1,7 @@ import type { ItemBank } from 'oldschooljs/dist/meta/types'; import { bulkUpdateCommands } from '@oldschoolgg/toolkit'; -import { DEV_SERVER_ID, production } from '../../config'; +import { production } from '../../config'; import { cacheBadges } from '../../lib/badges'; import { syncBlacklists } from '../../lib/blacklists'; import { Channel, DISABLED_COMMANDS, META_CONSTANTS, globalConfig } from '../../lib/constants'; @@ -14,7 +14,6 @@ import { cacheCleanup } from '../../lib/util/cachedUserIDs'; import { mahojiClientSettingsFetch } from '../../lib/util/clientSettings'; import { syncLinkedAccounts } from '../../lib/util/linkedAccountsUtil'; import { sendToChannelID } from '../../lib/util/webhook'; -import { cacheUsernames } from '../commands/leaderboard'; import { CUSTOM_PRICE_CACHE } from '../commands/sell'; export async function syncCustomPrices() { @@ -24,10 +23,7 @@ export async function syncCustomPrices() { } } -export async function onStartup() { - globalClient.application.commands.fetch({ guildId: production ? undefined : DEV_SERVER_ID }); - - // Sync disabled commands +async function syncDisabledCommands() { const disabledCommands = await prisma.clientStorage.upsert({ where: { id: globalConfig.clientID @@ -44,23 +40,27 @@ export async function onStartup() { DISABLED_COMMANDS.add(command); } } +} - // Sync blacklists - await syncBlacklists(); +export async function onStartup() { + globalClient.application.commands.fetch({ guildId: production ? undefined : globalConfig.testingServerID }); if (!production) { console.log('Syncing commands locally...'); await bulkUpdateCommands({ client: globalClient.mahojiClient, commands: Array.from(globalClient.mahojiClient.commands.values()), - guildID: DEV_SERVER_ID + guildID: globalConfig.testingServerID }); } + syncDisabledCommands(); + + syncBlacklists(); + runTimedLoggedFn('Syncing prices', syncCustomPrices); runTimedLoggedFn('Caching badges', cacheBadges); - runTimedLoggedFn('Cache Usernames', cacheUsernames); cacheCleanup(); runTimedLoggedFn('Sync Linked Accounts', syncLinkedAccounts); diff --git a/src/mahoji/lib/postCommand.ts b/src/mahoji/lib/postCommand.ts index cfa61abad4..a602a345f4 100644 --- a/src/mahoji/lib/postCommand.ts +++ b/src/mahoji/lib/postCommand.ts @@ -30,13 +30,6 @@ export async function postCommand({ if (!busyImmuneCommands.includes(abstractCommand.name)) { setTimeout(() => modifyBusyCounter(userID, -1), 1000); } - debugLog('Postcommand', { - type: 'RUN_COMMAND', - command_name: abstractCommand.name, - user_id: userID, - guild_id: guildID, - channel_id: channelID - }); if (shouldTrackCommand(abstractCommand, args)) { const commandUsage = makeCommandUsage({ userID, diff --git a/src/mahoji/lib/preCommand.ts b/src/mahoji/lib/preCommand.ts index ba8bafbdb1..bc5895b79f 100644 --- a/src/mahoji/lib/preCommand.ts +++ b/src/mahoji/lib/preCommand.ts @@ -1,54 +1,20 @@ -import type { CommandOptions } from '@oldschoolgg/toolkit'; -import { type InteractionReplyOptions, type TextChannel, type User, escapeMarkdown } from 'discord.js'; +import { type CommandOptions, cleanUsername } from '@oldschoolgg/toolkit'; +import type { InteractionReplyOptions, TextChannel, User } from 'discord.js'; import { modifyBusyCounter, userIsBusy } from '../../lib/busyCounterCache'; -import { Emoji, badges, badgesCache, busyImmuneCommands, usernameCache } from '../../lib/constants'; +import { busyImmuneCommands } from '../../lib/constants'; -import { stripEmojis } from '../../lib/util'; import { CACHED_ACTIVE_USER_IDS } from '../../lib/util/cachedUserIDs'; import type { AbstractCommand } from './inhibitors'; import { runInhibitors } from './inhibitors'; -function cleanUsername(username: string) { - return escapeMarkdown(stripEmojis(username)).substring(0, 32); -} - -export async function syncNewUserUsername(user: MUser, username: string) { - const newUsername = cleanUsername(username); - const newUser = await prisma.newUser.findUnique({ - where: { id: user.id } - }); - if (!newUser || newUser.username !== newUsername) { - await prisma.newUser.upsert({ - where: { - id: user.id - }, - update: { - username - }, - create: { - id: user.id, - username - } - }); - } - const name = stripEmojis(username); - usernameCache.set(user.id, name); - const rawBadges = user.user.badges.map(num => badges[num]); - if (user.isIronman) { - rawBadges.push(Emoji.Ironman); - } - badgesCache.set(user.id, rawBadges.join(' ')); -} - export async function preCommand({ abstractCommand, userID, guildID, channelID, bypassInhibitors, - apiUser, - options + apiUser }: { apiUser: User | null; abstractCommand: AbstractCommand; @@ -65,14 +31,6 @@ export async function preCommand({ dontRunPostCommand?: boolean; } > { - debugLog('Attempt to run command', { - type: 'RUN_COMMAND', - command_name: abstractCommand.name, - user_id: userID, - guild_id: guildID, - channel_id: channelID, - options - }); CACHED_ACTIVE_USER_IDS.add(userID); if (globalClient.isShuttingDown) { return { @@ -81,7 +39,12 @@ export async function preCommand({ dontRunPostCommand: true }; } - const user = await mUserFetch(userID); + + const username = apiUser?.username ? cleanUsername(apiUser?.username) : undefined; + const user = await mUserFetch(userID, { + username + }); + user.checkBankBackground(); if (userIsBusy(userID) && !bypassInhibitors && !busyImmuneCommands.includes(abstractCommand.name)) { return { silent: true, reason: { content: 'You cannot use a command right now.' }, dontRunPostCommand: true }; @@ -91,9 +54,7 @@ export async function preCommand({ const guild = guildID ? globalClient.guilds.cache.get(guildID.toString()) : null; const member = guild?.members.cache.get(userID.toString()); const channel = globalClient.channels.cache.get(channelID.toString()) as TextChannel; - if (apiUser) { - await syncNewUserUsername(user, apiUser.username); - } + const inhibitResult = await runInhibitors({ user, APIUser: await globalClient.fetchUser(user.id), diff --git a/tests/integration/grandExchange.test.ts b/tests/integration/grandExchange.test.ts index 3028fd6efb..add00c8921 100644 --- a/tests/integration/grandExchange.test.ts +++ b/tests/integration/grandExchange.test.ts @@ -3,7 +3,6 @@ import { Bank } from 'oldschooljs'; import { resolveItems } from 'oldschooljs/dist/util/util'; import { describe, expect, test } from 'vitest'; -import { usernameCache } from '../../src/lib/constants'; import { GrandExchange } from '../../src/lib/grandExchange'; import PQueue from 'p-queue'; @@ -173,9 +172,6 @@ Based on G.E data, we should have received ${data.totalTax} tax`; const wes = await createTestUser(); const magnaboy = await createTestUser(); - usernameCache.set(wes.id, 'Wes'); - usernameCache.set(magnaboy.id, 'Magnaboy'); - await magnaboy.addItemsToBank({ items: sampleBank }); await wes.addItemsToBank({ items: sampleBank }); assert(magnaboy.bankWithGP.equals(sampleBank), 'Test users bank should match sample bank'); diff --git a/tests/integration/killSimulator.test.ts b/tests/integration/killSimulator.test.ts new file mode 100644 index 0000000000..3337d87087 --- /dev/null +++ b/tests/integration/killSimulator.test.ts @@ -0,0 +1,9 @@ +import { expect, test } from 'vitest'; + +import { killCommand } from '../../src/mahoji/commands/kill'; +import { createTestUser } from './util'; + +test('killSimulator.test', async () => { + const user = await createTestUser(); + expect(async () => await user.runCommand(killCommand, { name: 'man', quantity: 100 })).to.not.throw(); +}); diff --git a/tests/integration/migrateUser.test.ts b/tests/integration/migrateUser.test.ts index 167f500138..0df552d40f 100644 --- a/tests/integration/migrateUser.test.ts +++ b/tests/integration/migrateUser.test.ts @@ -52,7 +52,6 @@ import { stashUnitBuildAllCommand, stashUnitFillAllCommand } from '../../src/mahoji/lib/abstracted_commands/stashUnitsCommand'; -import { syncNewUserUsername } from '../../src/mahoji/lib/preCommand'; import type { OSBMahojiCommand } from '../../src/mahoji/lib/util'; import { updateClientGPTrackSetting, userStatsUpdate } from '../../src/mahoji/mahojiSettings'; import { calculateResultOfLMSGames, getUsersLMSStats } from '../../src/tasks/minions/minigames/lmsActivity'; @@ -689,13 +688,6 @@ const allTableCommands: TestCommand[] = [ await pohWallkitCommand(user, 'Hosidius'); } }, - { - name: 'Create new_users entry', - cmd: async user => { - await syncNewUserUsername(user, `testUser${randInt(1000, 9999).toString()}`); - }, - priority: true - }, { name: 'Buy command transaction', cmd: async user => { @@ -1062,7 +1054,6 @@ const allTableCommands: TestCommand[] = [ data: { user_id: BigInt(user.id), channel_id: 1_111_111_111n, - status: 'Unknown', args: {}, command_name: randArrItem(randCommands), guild_id: null, diff --git a/tests/unit/interactionid.test.ts b/tests/unit/interactionid.test.ts new file mode 100644 index 0000000000..50187c493c --- /dev/null +++ b/tests/unit/interactionid.test.ts @@ -0,0 +1,14 @@ +import { test } from 'vitest'; + +import { InteractionID } from '../../src/lib/InteractionID'; + +test('InteractionID', () => { + const allStrings = Object.values(InteractionID) + .map(obj => Object.values(obj)) + .flat(2); + for (const string of allStrings) { + if (string.length < 1 || string.length > 100) { + throw new Error(`String ${string} has length ${string.length} which is not between 1 and 100`); + } + } +}); diff --git a/tests/unit/setup.ts b/tests/unit/setup.ts index a0de646af7..9d71430269 100644 --- a/tests/unit/setup.ts +++ b/tests/unit/setup.ts @@ -1,9 +1,12 @@ import '../globalSetup'; +import { TSRedis } from '@oldschoolgg/toolkit/TSRedis'; import { vi } from 'vitest'; import { mockMUser, mockUserMap } from './utils'; +global.redis = new TSRedis({ mocked: true }); + vi.mock('../../src/lib/settings/prisma.ts', () => ({ __esModule: true, prisma: {} diff --git a/tests/unit/snapshots/cl.OSB.png b/tests/unit/snapshots/cl.OSB.png index b7daf0d5da..ba03eddb79 100644 Binary files a/tests/unit/snapshots/cl.OSB.png and b/tests/unit/snapshots/cl.OSB.png differ diff --git a/tests/unit/snapshots/clsnapshots.test.ts.snap b/tests/unit/snapshots/clsnapshots.test.ts.snap index e8c30a8a43..1a39a3fc00 100644 --- a/tests/unit/snapshots/clsnapshots.test.ts.snap +++ b/tests/unit/snapshots/clsnapshots.test.ts.snap @@ -5,7 +5,7 @@ exports[`OSB Collection Log Groups/Categories 1`] = ` Achievement Diary (48) Aerial Fishing (9) Alchemical Hydra (11) -All Pets (57) +All Pets (58) Barbarian Assault (11) Barrows Chests (25) Beginner Treasure Trails (16) diff --git a/tests/unit/snapshots/cox.OSB.png b/tests/unit/snapshots/cox.OSB.png index a80c273243..c382155554 100644 Binary files a/tests/unit/snapshots/cox.OSB.png and b/tests/unit/snapshots/cox.OSB.png differ diff --git a/tests/unit/snapshots/toa.OSB.png b/tests/unit/snapshots/toa.OSB.png index ddc8a04bf9..7029b55eb1 100644 Binary files a/tests/unit/snapshots/toa.OSB.png and b/tests/unit/snapshots/toa.OSB.png differ diff --git a/tests/unit/utils.ts b/tests/unit/utils.ts index 3382304225..b2a77220eb 100644 --- a/tests/unit/utils.ts +++ b/tests/unit/utils.ts @@ -89,7 +89,8 @@ const mockUser = (overrides?: MockUserArgs): User => { QP: overrides?.QP ?? 0, sacrificedValue: 0, id: overrides?.id ?? '', - monsterScores: {} + monsterScores: {}, + badges: [] } as unknown as User; return r; diff --git a/yarn.lock b/yarn.lock index 857b56bed8..15f2921fda 100644 --- a/yarn.lock +++ b/yarn.lock @@ -679,9 +679,9 @@ __metadata: languageName: node linkType: hard -"@oldschoolgg/toolkit@git+https://github.com/oldschoolgg/toolkit.git#e148e18bec1be9bbb82151dced9e3f83ea0d4e85": +"@oldschoolgg/toolkit@git+https://github.com/oldschoolgg/toolkit.git#87450a60c73136601714c77092793d2f432b70b5": version: 0.0.24 - resolution: "@oldschoolgg/toolkit@https://github.com/oldschoolgg/toolkit.git#commit=e148e18bec1be9bbb82151dced9e3f83ea0d4e85" + resolution: "@oldschoolgg/toolkit@https://github.com/oldschoolgg/toolkit.git#commit=87450a60c73136601714c77092793d2f432b70b5" dependencies: decimal.js: "npm:^10.4.3" deepmerge: "npm:4.3.1" @@ -695,7 +695,7 @@ __metadata: peerDependencies: discord.js: ^14.15.3 oldschooljs: ^2.5.9 - checksum: 10c0/f2ea9b78ae87f7e0817319b9b78a743c48a7e9a9de76e5a9aaedc07710d728b2ab8ccbed0f31dc8a8be1ea93b39f2355aa61b2a0aa806d8d7faf59cd2977ad99 + checksum: 10c0/3a8caa110fb1d8b90d3fb3eb24e3c98f7e388d2a3f7e35ea9a14d65d01cc3fc529cdb5e042e251f4b414d1b2fc86b62824d9d8c4814842cde9769101c6070661 languageName: node linkType: hard @@ -4090,7 +4090,7 @@ __metadata: dependencies: "@biomejs/biome": "npm:^1.8.3" "@napi-rs/canvas": "npm:^0.1.53" - "@oldschoolgg/toolkit": "git+https://github.com/oldschoolgg/toolkit.git#e148e18bec1be9bbb82151dced9e3f83ea0d4e85" + "@oldschoolgg/toolkit": "git+https://github.com/oldschoolgg/toolkit.git#87450a60c73136601714c77092793d2f432b70b5" "@prisma/client": "npm:^5.16.1" "@sapphire/snowflake": "npm:^3.5.3" "@sapphire/time-utilities": "npm:^1.6.0"