diff --git a/.github/workflows/integration_tests.yml b/.github/workflows/integration_tests.yml index 165927f7c5..5442864076 100644 --- a/.github/workflows/integration_tests.yml +++ b/.github/workflows/integration_tests.yml @@ -20,4 +20,4 @@ jobs: uses: docker/setup-buildx-action@v3 - name: Run Integration Tests - run: docker-compose up --build --abort-on-container-exit + run: docker compose up --build --abort-on-container-exit --remove-orphans && docker compose down --volumes --remove-orphans diff --git a/package.json b/package.json index fbd80a360a..0e160aa765 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ }, "dependencies": { "@napi-rs/canvas": "^0.1.53", - "@oldschoolgg/toolkit": "git+https://github.com/oldschoolgg/toolkit.git#2813f25327093fcf2cb12bee7d4c85ce629069a0", + "@oldschoolgg/toolkit": "git+https://github.com/oldschoolgg/toolkit.git#cd7c6865229ca7dc4a66b3816586f2d3f4a4fbed", "@prisma/client": "^5.17.0", "@sapphire/ratelimits": "^2.4.9", "@sapphire/snowflake": "^3.5.3", @@ -48,6 +48,7 @@ "p-queue": "^6.6.2", "piscina": "^4.6.1", "random-js": "^2.1.0", + "remeda": "^2.7.0", "simple-statistics": "^7.8.3", "sonic-boom": "^4.0.1", "zlib-sync": "^0.1.9", diff --git a/prisma/schema.prisma b/prisma/schema.prisma index e25b832224..0c894e7942 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -404,7 +404,6 @@ model User { store_bitfield Int[] @default([]) - // Migrate farmingPatches_herb Json? @map("farmingPatches.herb") @db.Json farmingPatches_fruit_tree Json? @map("farmingPatches.fruit tree") @db.Json farmingPatches_tree Json? @map("farmingPatches.tree") @db.Json @@ -436,6 +435,8 @@ model User { gift_boxes_owned GiftBox[] @relation("gift_boxes_owned") gift_boxes_created GiftBox[] @relation("gift_boxes_created") + cl_array Int[] @default([]) + @@index([id, last_command_date]) @@map("users") } @@ -744,7 +745,7 @@ model UserStats { creature_scores Json @default("{}") monster_scores Json @default("{}") laps_scores Json @default("{}") - sacrificed_bank Json @default("{}") + sacrificed_bank Json @default("{}") @db.JsonB openable_scores Json @default("{}") gp_luckypick BigInt @default(0) diff --git a/src/lib/combat_achievements/combatAchievements.ts b/src/lib/combat_achievements/combatAchievements.ts index 5545b07c0d..5aef785fb0 100644 --- a/src/lib/combat_achievements/combatAchievements.ts +++ b/src/lib/combat_achievements/combatAchievements.ts @@ -162,13 +162,13 @@ const indexesWithRng = entries.flatMap(i => i[1].tasks.filter(t => 'rng' in t)); export const combatAchievementTripEffect = async ({ data, messages, user }: Parameters[0]) => { const dataCopy = deepClone(data); if (dataCopy.type === 'Inferno' && !dataCopy.diedPreZuk && !dataCopy.diedZuk) { - (dataCopy as any).quantity = 1; + (dataCopy as any).q = 1; } if (dataCopy.type === 'Colosseum') { - (dataCopy as any).quantity = 1; + (dataCopy as any).q = 1; } - if (!('quantity' in dataCopy)) return; - let quantity = Number(dataCopy.quantity); + if (!('q' in dataCopy)) return; + let quantity = Number(dataCopy.q); if (Number.isNaN(quantity)) return; if (data.type === 'TombsOfAmascut') { diff --git a/src/lib/combat_achievements/hard.ts b/src/lib/combat_achievements/hard.ts index 82f33c3077..1522239efb 100644 --- a/src/lib/combat_achievements/hard.ts +++ b/src/lib/combat_achievements/hard.ts @@ -597,7 +597,7 @@ export const hardCombatAchievements: CombatAchievement[] = [ monster: 'Tempoross', desc: 'Subdue Tempoross, getting rewarded with 10 reward permits from a single Tempoross fight.', rng: { - chancePerKill: 30, + chancePerKill: 5, hasChance: data => data.type === 'Tempoross' } }, diff --git a/src/lib/combat_achievements/medium.ts b/src/lib/combat_achievements/medium.ts index 420f3c1b43..30d8255701 100644 --- a/src/lib/combat_achievements/medium.ts +++ b/src/lib/combat_achievements/medium.ts @@ -431,7 +431,7 @@ export const mediumCombatAchievements: CombatAchievement[] = [ monster: 'Skotizo', desc: 'Kill Skotizo with no altars active.', rng: { - chancePerKill: 15, + chancePerKill: 5, hasChance: isCertainMonsterTrip(Monsters.Skotizo.id) } }, diff --git a/src/lib/constants.ts b/src/lib/constants.ts index c3732622d0..cb7b989f77 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -344,7 +344,9 @@ export const BadgesEnum = { TopSkiller: 9, TopCollector: 10, TopMinigame: 11, - SotWTrophy: 12 + SotWTrophy: 12, + Slayer: 13, + TopGiveawayer: 14 } as const; export const badges: { [key: number]: string } = { @@ -360,7 +362,9 @@ export const badges: { [key: number]: string } = { [BadgesEnum.TopSkiller]: Emoji.Skiller, [BadgesEnum.TopCollector]: Emoji.CollectionLog, [BadgesEnum.TopMinigame]: Emoji.MinigameIcon, - [BadgesEnum.SotWTrophy]: Emoji.SOTWTrophy + [BadgesEnum.SotWTrophy]: Emoji.SOTWTrophy, + [BadgesEnum.Slayer]: Emoji.Slayer, + [BadgesEnum.TopGiveawayer]: Emoji.SantaHat }; export const MAX_XP = 200_000_000; diff --git a/src/lib/handleNewCLItems.ts b/src/lib/handleNewCLItems.ts index c57afd9041..d5e84942fa 100644 --- a/src/lib/handleNewCLItems.ts +++ b/src/lib/handleNewCLItems.ts @@ -9,6 +9,7 @@ import { allCLItems, allCollectionLogsFlat, calcCLDetails } from './data/Collect import { calculateMastery } from './mastery'; import { calculateOwnCLRanking, roboChimpSyncData } from './roboChimp'; +import { RawSQL } from './rawSql'; import { MUserStats } from './structures/MUserStats'; import { fetchCLLeaderboard } from './util/clLeaderboard'; import { insertUserEvent } from './util/userEvents'; @@ -49,6 +50,10 @@ export async function handleNewCLItems({ await prisma.historicalData.create({ data: await createHistoricalData(user) }); } + if (didGetNewCLItem) { + await prisma.$queryRawUnsafe(RawSQL.updateCLArray(user.id)); + } + if (!didGetNewCLItem) return; const previousCLDetails = calcCLDetails(previousCL); @@ -101,15 +106,14 @@ export async function handleNewCLItems({ })}!` : ''; - const nthUser = ( - await fetchCLLeaderboard({ - ironmenOnly: false, - items: finishedCL.items, - resultLimit: 100_000, - method: 'raw_cl', - userEvents: null - }) - ).filter(u => u.qty === finishedCL.items.length).length; + const leaderboardUsers = await fetchCLLeaderboard({ + ironmenOnly: false, + items: finishedCL.items, + resultLimit: 100_000, + clName: finishedCL.name + }); + + const nthUser = leaderboardUsers.users.filter(u => u.qty === finishedCL.items.length).length; const placeStr = nthUser > 100 ? '' : ` They are the ${formatOrdinal(nthUser)} user to finish this CL.`; diff --git a/src/lib/preStartup.ts b/src/lib/preStartup.ts index 0b98474cd1..b47aefc1f0 100644 --- a/src/lib/preStartup.ts +++ b/src/lib/preStartup.ts @@ -1,3 +1,4 @@ +import { noOp } from 'e'; import { syncCustomPrices } from '../mahoji/lib/events'; import { syncActivityCache } from './Task'; import { cacheBadges } from './badges'; @@ -5,6 +6,7 @@ import { syncBlacklists } from './blacklists'; import { GrandExchange } from './grandExchange'; import { cacheGEPrices } from './marketPrices'; import { populateRoboChimpCache } from './perkTier'; +import { RawSQL } from './rawSql'; import { runStartupScripts } from './startupScripts'; import { logWrapFn } from './util'; import { syncActiveUserIDs } from './util/cachedUserIDs'; @@ -21,6 +23,7 @@ export const preStartup = logWrapFn('PreStartup', async () => { cacheBadges(), GrandExchange.init(), populateRoboChimpCache(), - cacheGEPrices() + cacheGEPrices(), + prisma.$queryRawUnsafe(RawSQL.updateAllUsersCLArrays()).then(noOp) ]); }); diff --git a/src/lib/rawSql.ts b/src/lib/rawSql.ts new file mode 100644 index 0000000000..54b68fc695 --- /dev/null +++ b/src/lib/rawSql.ts @@ -0,0 +1,16 @@ +import { Prisma } from '@prisma/client'; + +const u = Prisma.UserScalarFieldEnum; + +export const RawSQL = { + updateAllUsersCLArrays: () => `UPDATE users +SET ${u.cl_array} = ( + SELECT (ARRAY(SELECT jsonb_object_keys("${u.collectionLogBank}")::int)) +) +WHERE last_command_date > now() - INTERVAL '1 week';`, + updateCLArray: (userID: string) => `UPDATE users +SET ${u.cl_array} = ( + SELECT (ARRAY(SELECT jsonb_object_keys("${u.collectionLogBank}")::int)) +) +WHERE ${u.id} = '${userID}';` +}; diff --git a/src/lib/rolesTask.ts b/src/lib/rolesTask.ts index dc694bb83c..b9c7d0e375 100644 --- a/src/lib/rolesTask.ts +++ b/src/lib/rolesTask.ts @@ -1,535 +1,446 @@ -import { Prisma } from '@prisma/client'; -import { noOp, notEmpty } from 'e'; +import { noOp, uniqueArr } from 'e'; -import { SupportServer, production } from '../config'; -import { ClueTiers } from '../lib/clues/clueTiers'; -import { Roles } from '../lib/constants'; +import { SupportServer } from '../config'; +import { BadgesEnum, Roles } from '../lib/constants'; import { getCollectionItems } from '../lib/data/Collections'; import { Minigames } from '../lib/settings/minigames'; -import Skills from '../lib/skilling/skills'; -import { convertXPtoLVL, getUsername } from '../lib/util'; -import { logError } from '../lib/util/logError'; +import { Prisma } from '@prisma/client'; +import PQueue from 'p-queue'; +import { partition } from 'remeda'; +import z from 'zod'; +import { type CommandResponse, Stopwatch, convertXPtoLVL, getUsernameSync, returnStringOrFile } from '../lib/util'; +import { ClueTiers } from './clues/clueTiers'; import { TeamLoot } from './simulation/TeamLoot'; +import { SkillsArray } from './skilling/types'; import type { ItemBank } from './types'; +import { fetchMultipleCLLeaderboards } from './util/clLeaderboard'; -function addToUserMap(userMap: Record, id: string, reason: string) { - if (!userMap[id]) userMap[id] = []; - userMap[id].push(reason); -} +const RoleResultSchema = z.object({ + roleID: z.string().min(17).max(19), + userID: z.string().min(17).max(19), + reason: z.string(), + badge: z.number().int().optional() +}); +type RoleResult = z.infer; const minigames = Minigames.map(game => game.column).filter(i => i !== 'tithe_farm'); -const collections = ['pets', 'skilling', 'clues', 'bosses', 'minigames', 'raids', 'slayer', 'other', 'custom']; - -for (const cl of collections) { +const CLS_THAT_GET_ROLE = [ + 'pets', + 'skilling', + 'clues', + 'bosses', + 'minigames', + 'raids', + 'slayer', + 'other', + 'custom', + 'overall' +]; + +for (const cl of CLS_THAT_GET_ROLE) { const items = getCollectionItems(cl); if (!items || items.length === 0) { throw new Error(`${cl} isn't a valid CL.`); } } -const mostSlayerPointsQuery = `SELECT id, 'Most Points' as desc -FROM users -WHERE "slayer.points" > 50 -ORDER BY "slayer.points" DESC -LIMIT 1;`; - -const longerSlayerTaskStreakQuery = `SELECT user_id::text as id, 'Longest Task Streak' as desc -FROM user_stats -WHERE "slayer_task_streak" > 20 -ORDER BY "slayer_task_streak" DESC -LIMIT 1;`; - -const mostSlayerTasksDoneQuery = `SELECT user_id::text as id, 'Most Tasks' as desc -FROM slayer_tasks -GROUP BY user_id -ORDER BY count(user_id)::int DESC -LIMIT 1;`; - -async function addRoles({ - users, - role, - badge, - userMap -}: { - users: string[]; - role: string; - badge: number | null; - userMap?: Record; -}): Promise { - if (process.env.TEST) return ''; - const g = globalClient.guilds.cache.get(SupportServer); - if (!g) throw new Error('No support guild'); - const added: string[] = []; - const removed: string[] = []; - const _role = await g.roles.fetch(role).catch(noOp); - if (!_role) return `\nCould not check ${role} role`; - for (const u of users.filter(notEmpty)) { - await g.members.fetch(u).catch(noOp); +async function topSkillers() { + const results: RoleResult[] = []; + + const [top200TotalXPUsers, ...top200ms] = await prisma.$transaction([ + prisma.$queryRawUnsafe( + `SELECT id, ${SkillsArray.map(s => `"skills.${s}"`)}, ${SkillsArray.map(s => `"skills.${s}"::bigint`).join( + ' + ' + )} as totalxp FROM users ORDER BY totalxp DESC LIMIT 200;` + ), + ...SkillsArray.map(s => { + const query = `SELECT id, "skills.${s}" AS xp, '${s}' AS skill FROM users ORDER BY xp DESC LIMIT 1;`; + return prisma.$queryRawUnsafe<{ + id: string; + xp: string; + skill: string; + }>(query); + }) + ]); + + for (const { id, skill } of top200ms.flat()) { + results.push({ + userID: id, + roleID: Roles.TopSkiller, + reason: `Rank 1 ${skill} XP`, + badge: BadgesEnum.TopSkiller + }); } - const roleName = _role.name!; - const noChangeUserDescriptions: string[] = []; - for (const mem of g.members.cache.values()) { - const mUser = await mUserFetch(mem.user.id); - if (mem.roles.cache.has(role) && !users.includes(mem.user.id)) { - if (production) { - await mem.roles.remove(role).catch(noOp); - } - - if (badge && mUser.user.badges.includes(badge)) { - await mUser.update({ - badges: mUser.user.badges.filter(i => i !== badge) - }); - } - removed.push(mem.user.username); - } - if (users.includes(mem.user.id)) { - noChangeUserDescriptions.push(`${mem.user.username}`); - if (production && !mem.roles.cache.has(role)) { - added.push(mem.user.username); - await mem.roles.add(role).catch(noOp); - } - if (badge && !mUser.user.badges.includes(badge)) { - await mUser.update({ - badges: { - push: badge - } - }); + const rankOneTotal = top200TotalXPUsers + .map((u: any) => { + let totalLevel = 0; + for (const skill of SkillsArray) { + totalLevel += convertXPtoLVL(Number(u[`skills.${skill}` as keyof any]) as any); } - } - } - let str = `\n**${roleName}**`; - if (added.length > 0) { - str += `\nAdded to: ${added.join(', ')}.`; - } - if (removed.length > 0) { - str += `\nRemoved from: ${removed.join(', ')}.`; - } - if (userMap) { - const userArr = []; - for (const [id, arr] of Object.entries(userMap)) { - const username = await getUsername(id); - userArr.push(`${username}(${arr.join(', ')})`); - } - str += `\n${userArr.join(',')}`; - } - if (added.length > 0 || removed.length > 0) { - str += '\n'; - } else { - return `**No Changes:** ${str}`; - } - return str; -} - -export async function runRolesTask() { - const skillVals = Object.values(Skills); + return { + id: u.id, + totalLevel + }; + }) + .sort((a: any, b: any) => b.totalLevel - a.totalLevel)[0]; - const results: string[] = []; - const q = async (str: string) => { - const result = await prisma.$queryRawUnsafe(str).catch(err => { - logError(`This query failed: ${str}`, err); - return []; - }); - return result; - }; - - // Top Skillers - async function topSkillers() { - const topSkillers = ( - await Promise.all([ - ...skillVals.map(s => - q< - { - id: string; - xp: string; - }[] - >(`SELECT id, "skills.${s.id}" as xp FROM users ORDER BY xp DESC LIMIT 1;`) - ), - q< - { - id: string; - }[] - >( - `SELECT id, ${skillVals.map(s => `"skills.${s.id}"`)}, ${skillVals - .map(s => `"skills.${s.id}"::bigint`) - .join(' + ')} as totalxp FROM users ORDER BY totalxp DESC LIMIT 1;` - ) - ]) - ).map(i => i[0]?.id); - - // Rank 1 Total Level - const rankOneTotal = ( - await q( - `SELECT id, ${skillVals.map(s => `"skills.${s.id}"`)}, ${skillVals - .map(s => `"skills.${s.id}"::bigint`) - .join(' + ')} as totalxp FROM users ORDER BY totalxp DESC LIMIT 200;` - ) - ) - .map((u: any) => { - let totalLevel = 0; - for (const skill of skillVals) { - totalLevel += convertXPtoLVL(Number(u[`skills.${skill.id}` as keyof any]) as any); - } - return { - id: u.id, - totalLevel - }; - }) - .sort((a: any, b: any) => b.totalLevel - a.totalLevel)[0]; - topSkillers.push(rankOneTotal.id); - - results.push(await addRoles({ users: topSkillers, role: Roles.TopSkiller, badge: 9 })); - } + results.push({ + userID: rankOneTotal.id, + roleID: Roles.TopSkiller, + reason: 'Rank 1 Total Level', + badge: BadgesEnum.TopSkiller + }); - // Top Collectors - async function topCollector() { - const userMap = {}; - - function generateQuery(items: number[], ironmenOnly: boolean, limit: number) { - const t = ` -SELECT id, (cardinality(u.cl_keys) - u.inverse_length) as qty - FROM ( - SELECT ARRAY(SELECT * FROM JSONB_OBJECT_KEYS("collectionLogBank")) "cl_keys", - id, "collectionLogBank", - cardinality(ARRAY(SELECT * FROM JSONB_OBJECT_KEYS("collectionLogBank" - array[${items - .map(i => `'${i}'`) - .join(', ')}]))) "inverse_length" - FROM users - WHERE "collectionLogBank" ?| array[${items.map(i => `'${i}'`).join(', ')}] - ${ironmenOnly ? 'AND "minion.ironman" = true' : ''} - ) u - ORDER BY qty DESC - LIMIT ${limit}; -`; - - return t; - } + return results; +} - const topCollectors = ( - await Promise.all( - collections.map(async clName => { - const items = getCollectionItems(clName); - if (!items || items.length === 0) { - logError(`${clName} collection log doesnt exist`); - return []; - } - - function handleErr(): any[] { - logError(`Failed to select top collectors for ${clName}`); - return []; - } - - const [users, ironUsers] = await Promise.all([ - q(generateQuery(items, false, 1)) - .then(i => i.filter((i: any) => i.qty > 0) as any[]) - .catch(handleErr), - q(generateQuery(items, true, 1)) - .then(i => i.filter((i: any) => i.qty > 0) as any[]) - .catch(handleErr) - ]); - - const result = []; - const userID = users[0]?.id; - const ironmanID = ironUsers[0]?.id; - - if (userID) { - addToUserMap(userMap, userID, `Rank 1 ${clName} CL`); - result.push(userID); - } - if (ironmanID) { - addToUserMap(userMap, ironmanID, `Rank 1 Ironman ${clName} CL`); - result.push(ironmanID); - } - - return result; - }) - ) - ).flat(2); - - const topIronUsers = (await q(generateQuery(getCollectionItems('overall'), true, 3))).filter( - (i: any) => i.qty > 0 - ) as any[]; - for (let i = 0; i < topIronUsers.length; i++) { - const id = topIronUsers[i]?.id; - addToUserMap(userMap, id, `Rank ${i + 1} Ironman Collector`); - topCollectors.push(id); - } - const topNormieUsers = (await q(generateQuery(getCollectionItems('overall'), false, 3))).filter( - (i: any) => i.qty > 0 - ) as any[]; - for (let i = 0; i < topNormieUsers.length; i++) { - const id = topNormieUsers[i]?.id; - addToUserMap(userMap, id, `Rank ${i + 1} Collector`); - topCollectors.push(id); +async function topCollector() { + const results: RoleResult[] = []; + const rankOneInSpecifiedCLs = await fetchMultipleCLLeaderboards( + CLS_THAT_GET_ROLE.map(cl => { + const items = getCollectionItems(cl); + const base = { + items, + clName: cl, + resultLimit: cl === 'overall' ? 3 : 1 + } as const; + return [ + { ...base, ironmenOnly: true }, + { ...base, ironmenOnly: false } + ]; + }).flat(2) + ); + for (const { users, clName, ironmenOnly } of rankOneInSpecifiedCLs) { + for (const user of users) { + results.push({ + userID: user.id, + roleID: Roles.TopCollector, + reason: `Rank 1 ${ironmenOnly ? 'Iron' : 'Main'} ${clName}`, + badge: BadgesEnum.TopCollector + }); } - - results.push(await addRoles({ users: topCollectors, role: Roles.TopCollector, badge: 10, userMap })); } + return results; +} - // Top sacrificers - async function topSacrificers() { - const userMap = {}; - const topSacrificers: string[] = []; - const mostValue = await q('SELECT id FROM users ORDER BY "sacrificedValue" DESC LIMIT 3;'); - for (let i = 0; i < 3; i++) { - if (mostValue[i] !== undefined) { - topSacrificers.push(mostValue[i].id); - addToUserMap(userMap, mostValue[i].id, `Rank ${i + 1} Sacrifice Value`); - } - } - const mostValueIronman = await q( - 'SELECT id FROM users WHERE "minion.ironman" = true ORDER BY "sacrificedValue" DESC LIMIT 1;' - ); - topSacrificers.push(mostValueIronman[0].id); - addToUserMap(userMap, mostValueIronman[0].id, 'Rank 1 Ironman Sacrificed Value'); - - const mostUniques = await q(`SELECT u.id, u.sacbanklength FROM ( +async function topSacrificers() { + const results: RoleResult[] = []; + const users = await prisma.$transaction([ + prisma.$queryRawUnsafe<{ id: string; reason: string }[]>( + `SELECT id, 'Top 3' AS reason FROM users ORDER BY "sacrificedValue" DESC LIMIT 3;` + ), + prisma.$queryRawUnsafe<{ id: string; reason: string }[]>( + `SELECT id, 'Top Ironman' AS reason FROM users WHERE "minion.ironman" = true ORDER BY "sacrificedValue" DESC LIMIT 1;` + ), + prisma.$queryRawUnsafe<{ id: string; reason: string }[]>(`SELECT u.id, 'Top Uniques' AS reason FROM ( SELECT (SELECT COUNT(*)::int FROM JSONB_OBJECT_KEYS("sacrificed_bank")) sacbanklength, user_id::text as id FROM user_stats ) u -ORDER BY u.sacbanklength DESC LIMIT 1;`); - - const mostUniquesIron = await q(`SELECT u.id, u.sacbanklength FROM ( +ORDER BY u.sacbanklength DESC LIMIT 1;`), + prisma.$queryRawUnsafe<{ id: string; reason: string }[]>(`SELECT u.id, 'Top Ironman Uniques' AS reason FROM ( SELECT (SELECT COUNT(*)::int FROM JSONB_OBJECT_KEYS("sacrificed_bank")) sacbanklength, user_id::text as id FROM user_stats INNER JOIN users ON "user_stats"."user_id"::text = "users"."id" WHERE "users"."minion.ironman" = true ) u -ORDER BY u.sacbanklength DESC LIMIT 1;`); - topSacrificers.push(mostUniques[0].id); - addToUserMap(userMap, mostUniques[0].id, 'Most Uniques Sacrificed'); - topSacrificers.push(mostUniquesIron[0].id); - addToUserMap(userMap, mostUniquesIron[0].id, 'Most Ironman Uniques Sacrificed'); - - results.push(await addRoles({ users: topSacrificers, role: Roles.TopSacrificer, badge: 8, userMap })); +ORDER BY u.sacbanklength DESC LIMIT 1;`) + ]); + + for (const res of users.flat()) { + results.push({ + userID: res.id, + reason: res.reason, + roleID: Roles.TopSacrificer, + badge: BadgesEnum.TopSacrifice + }); } - // Top minigamers - async function topMinigamers() { - const topMinigamers = ( - await Promise.all( - minigames.map(m => - q( - `SELECT user_id, '${m}' as m + return results; +} + +async function topMinigamers() { + const results: RoleResult[] = []; + const topMinigamers = await prisma.$transaction( + minigames.map(m => + prisma.$queryRawUnsafe<{ id: string; minigame_name: string; qty: number }[]>( + `SELECT user_id::text AS id, '${m}' AS minigame_name FROM minigames ORDER BY ${m} DESC LIMIT 1;` - ) - ) ) - ).map((i: any) => [i[0].user_id, Minigames.find(m => m.column === i[0].m)?.name]); - - const userMap = {}; - for (const [id, m] of topMinigamers) { - addToUserMap(userMap, id, `Rank 1 ${m}`); - } + ) + ); - results.push( - await addRoles({ - users: topMinigamers.map(i => i[0]), - role: Roles.TopMinigamer, - badge: 11, - userMap - }) - ); + for (const { id, minigame_name } of topMinigamers.flat()) { + results.push({ + userID: id, + roleID: Roles.TopMinigamer, + reason: `Rank 1 ${minigame_name}`, + badge: BadgesEnum.TopMinigame + }); } - // Top clue hunters - async function topClueHunters() { - const topClueHunters = ( - await Promise.all( - ClueTiers.map(t => - q( - `SELECT id, '${t.name}' as n, (openable_scores->>'${t.id}')::int as qty -FROM users -INNER JOIN "user_stats" ON "user_stats"."user_id"::text = "users"."id" + return results; +} + +async function topClueHunters() { + const results: RoleResult[] = []; + const topClueHunters = await prisma.$transaction( + ClueTiers.map(t => + prisma.$queryRawUnsafe<{ user_id: string; tier_name: string; qty: string }>( + ` +SELECT user_id::text, '${t.name}' AS tier_name, (openable_scores->>'${t.id}')::int AS qty +FROM user_stats WHERE "openable_scores"->>'${t.id}' IS NOT NULL ORDER BY qty DESC LIMIT 1;` - ) - ) ) ) - .filter((i: any) => Boolean(i[0]?.id)) - .map((i: any) => [i[0]?.id, i[0]?.n]); - - const userMap = {}; - - for (const [id, n] of topClueHunters) { - addToUserMap(userMap, id, `Rank 1 ${n} Clues`); - } + ); - results.push( - await addRoles({ - users: topClueHunters.map(i => i[0]), - role: Roles.TopClueHunter, - badge: null, - userMap - }) - ); + for (const res of topClueHunters.flat()) { + results.push({ + userID: res.user_id, + roleID: Roles.TopClueHunter, + reason: `Rank 1 ${res.tier_name} Clues`, + badge: BadgesEnum.TopMinigame + }); } + return results; +} - // Top farmers - async function farmers() { - const queries = [ - `SELECT id, 'Top 2 Farming Contracts' as desc +async function topFarmers() { + const results: RoleResult[] = []; + const queries = [ + `SELECT id, 'Top 2 Farming Contracts' as desc FROM users WHERE "minion.farmingContract" IS NOT NULL AND "minion.ironman" = true ORDER BY ("minion.farmingContract"->>'contractsCompleted')::int DESC LIMIT 2;`, - `SELECT id, 'Top 2 Ironman Farming Contracts' as desc + `SELECT id, 'Top 2 Ironman Farming Contracts' as desc FROM users WHERE "minion.farmingContract" IS NOT NULL ORDER BY ("minion.farmingContract"->>'contractsCompleted')::int DESC LIMIT 2;`, - `SELECT user_id::text as id, 'Top 2 Most Farming Trips' as desc + `SELECT user_id::text as id, 'Top 2 Most Farming Trips' as desc FROM activity WHERE type = 'Farming' GROUP BY user_id ORDER BY count(user_id)::int DESC LIMIT 2;`, - `SELECT user_id::text as id, 'Top 2 Tithe Farm' as desc + `SELECT user_id::text as id, 'Top 2 Tithe Farm' as desc FROM user_stats ORDER BY "tithe_farms_completed" DESC LIMIT 2;` - ]; - const res = (await Promise.all(queries.map(q))).map((i: any) => [i[0]?.id, i[0]?.desc]); - const userMap = {}; - for (const [id, desc] of res) { - addToUserMap(userMap, id, desc); - } - - results.push( - await addRoles({ - users: res.map(i => i[0]), - role: '894194027363205150', - badge: null, - userMap - }) - ); + ]; + const res = await prisma.$transaction(queries.map(q => prisma.$queryRawUnsafe<{ id: string; desc: string }[]>(q))); + for (const { id, desc } of res.flat()) { + results.push({ + userID: id, + roleID: '894194027363205150', + reason: desc, + badge: BadgesEnum.Slayer + }); } + return results; +} - // Top slayers - async function slayer() { - const topSlayers = ( - await Promise.all( - [mostSlayerPointsQuery, longerSlayerTaskStreakQuery, mostSlayerTasksDoneQuery].map(query => q(query)) - ) - ) - .filter((i: any) => Boolean(i[0]?.id)) - .map((i: any) => [i[0]?.id, i[0]?.desc]); - - const userMap = {}; - for (const [id, desc] of topSlayers) { - addToUserMap(userMap, id, desc); - } - - results.push( - await addRoles({ - users: topSlayers.map(i => i[0]), - role: Roles.TopSlayer, - badge: null, - userMap - }) - ); +async function fetchSlayerResults() { + const results: RoleResult[] = []; + const topSlayers = await prisma.$transaction([ + prisma.$queryRawUnsafe<{ id: string; desc: string }[]>(`SELECT id, 'Most Points' as desc +FROM users +WHERE "slayer.points" > 50 +ORDER BY "slayer.points" DESC +LIMIT 1;`), + prisma.$queryRawUnsafe<{ id: string; desc: string }[]>(`SELECT user_id::text as id, 'Longest Task Streak' as desc +FROM user_stats +WHERE "slayer_task_streak" > 20 +ORDER BY "slayer_task_streak" DESC +LIMIT 1;`), + prisma.$queryRawUnsafe<{ id: string; desc: string }[]>(`SELECT user_id::text as id, 'Most Tasks' as desc +FROM slayer_tasks +GROUP BY user_id +ORDER BY count(user_id)::int DESC +LIMIT 1;`) + ]); + + for (const { id, desc } of topSlayers.flat()) { + results.push({ + userID: id, + roleID: Roles.TopSlayer, + reason: desc, + badge: BadgesEnum.Slayer + }); } + return results; +} - // Top giveawayers - async function giveaways() { - const GIVEAWAY_CHANNELS = [ - '792691343284764693', - '732207379818479756', - '342983479501389826', - '982989775399174184', - '346304390858145792' - ]; - const res = await prisma.$queryRaw<{ user_id: string; qty: number }[]>`SELECT user_id, COUNT(user_id)::int AS qty -FROM giveaway -WHERE channel_id IN (${Prisma.join(GIVEAWAY_CHANNELS)}) -AND user_id NOT IN ('157797566833098752') -GROUP BY user_id -ORDER BY qty DESC -LIMIT 50;`; - const userIDsToCheck = res.map(i => i.user_id); - - const giveawayBank = new TeamLoot(); - - const giveaways = await prisma.giveaway.findMany({ - where: { - channel_id: { - in: GIVEAWAY_CHANNELS - }, - user_id: { - in: userIDsToCheck - } +async function giveaways() { + const results: RoleResult[] = []; + const GIVEAWAY_CHANNELS = [ + '792691343284764693', + '732207379818479756', + '342983479501389826', + '982989775399174184', + '346304390858145792' + ]; + const res = await prisma.$queryRaw<{ user_id: string; qty: number }[]>`SELECT user_id, COUNT(user_id)::int AS qty + FROM giveaway + WHERE channel_id IN (${Prisma.join(GIVEAWAY_CHANNELS)}) + AND user_id NOT IN ('157797566833098752') + GROUP BY user_id + ORDER BY qty DESC + LIMIT 50;`; + const userIDsToCheck = res.map(i => i.user_id); + + const giveawayBank = new TeamLoot(); + + const giveaways = await prisma.giveaway.findMany({ + where: { + channel_id: { + in: GIVEAWAY_CHANNELS + }, + user_id: { + in: userIDsToCheck } - }); - for (const giveaway of giveaways) { - giveawayBank.add(giveaway.user_id, giveaway.loot as ItemBank); } + }); - const userMap = {}; - const [[highestID, loot]] = giveawayBank.entries().sort((a, b) => b[1].value() - a[1].value()); - addToUserMap(userMap, highestID, `Most Value Given Away (${loot.value()})`); - - results.push( - await addRoles({ - users: [highestID], - role: '1052481561603346442', - badge: null, - userMap - }) - ); + if (giveaways.length === 0) return results; + + for (const giveaway of giveaways) { + giveawayBank.add(giveaway.user_id, giveaway.loot as ItemBank); } - // Global CL % - async function globalCL() { - const result = await roboChimpClient.$queryRaw<{ id: string; total_cl_percent: number }[]>`SELECT ((osb_cl_percent + bso_cl_percent) / 2) AS total_cl_percent, id::text AS id -FROM public.user -WHERE osb_cl_percent IS NOT NULL AND bso_cl_percent IS NOT NULL -ORDER BY total_cl_percent DESC -LIMIT 10;`; - - results.push( - await addRoles({ - users: result.slice(0, 10).map(i => i.id), - role: Roles.TopGlobalCL, - badge: null - }) - ); + const [[highestID, loot]] = giveawayBank.entries().sort((a, b) => b[1].value() - a[1].value()); + + results.push({ + userID: highestID, + roleID: '1052481561603346442', + reason: `Most Value Given Away (${loot.value()})`, + badge: BadgesEnum.TopGiveawayer + }); + return results; +} + +async function globalCL() { + const results: RoleResult[] = []; + const result = await roboChimpClient.$queryRaw<{ id: string; total_cl_percent: number }[]>`SELECT ((osb_cl_percent + bso_cl_percent) / 2) AS total_cl_percent, id::text AS id + FROM public.user + WHERE osb_cl_percent IS NOT NULL AND bso_cl_percent IS NOT NULL + ORDER BY total_cl_percent DESC + LIMIT 10;`; + + for (const user of result) { + results.push({ + userID: user.id, + roleID: Roles.TopGlobalCL, + reason: `Top Global CL ${user.total_cl_percent}%`, + badge: BadgesEnum.TopCollector + }); } + return results; +} + +export async function runRolesTask(dryRun: boolean): Promise { + const results: RoleResult[] = []; + + const promiseQueue = new PQueue({ concurrency: 2 }); const tup = [ - ['Top Slayer', slayer], + ['Top Slayer', fetchSlayerResults], ['Top Clue Hunters', topClueHunters], ['Top Minigamers', topMinigamers], ['Top Sacrificers', topSacrificers], ['Top Collectors', topCollector], ['Top Skillers', topSkillers], - ['Top Farmers', farmers], + ['Top Farmers', topFarmers], ['Top Giveawayers', giveaways], ['Global CL', globalCL] ] as const; - const failed: string[] = []; - await Promise.all( - tup.map(async ([name, fn]) => { - try { - await fn(); - } catch (err: any) { - if (process.env.TEST) { - throw err; + for (const [name, fn] of tup) { + promiseQueue.add(async () => { + const stopwatch = new Stopwatch(); + const res = await fn(); + console.log(`[RolesTask] Ran ${name} in ${stopwatch.stop()}`); + const [validResults, invalidResults] = partition(res, i => RoleResultSchema.safeParse(i).success); + results.push(...validResults); + if (invalidResults.length > 0) { + console.error(`[RolesTask] Invalid results for ${name}: ${JSON.stringify(invalidResults)}`); + } + }); + } + + await promiseQueue.onIdle(); + + debugLog(`Finished role functions, ${results.length} results`); + + const allBadgeIDs = uniqueArr(results.map(i => i.badge)); + const allRoleIDs = uniqueArr(results.map(i => i.roleID)); + + if (!dryRun) { + const roleNames = new Map(); + const supportServerGuild = globalClient.guilds.cache.get(SupportServer)!; + if (!supportServerGuild) throw new Error('No support guild'); + + // Remove all top badges from all users (and add back later) + debugLog('Removing badges...'); + const badgeIDs = `ARRAY[${allBadgeIDs.join(',')}]`; + await prisma.$queryRawUnsafe(` +UPDATE users +SET badges = badges - ${badgeIDs} +WHERE badges && ${badgeIDs} +`); + + // Remove roles from ineligible users + debugLog('Remove roles from ineligible users...'); + for (const member of supportServerGuild.members.cache.values()) { + const rolesToRemove = member.roles.cache.filter(r => allRoleIDs.includes(r.id)); + if (rolesToRemove.size > 0) { + await member.roles.remove(rolesToRemove.map(r => r.id)).catch(console.error); + } + } + + // Add roles to users + debugLog('Add roles to users...'); + for (const { userID, roleID, badge } of results) { + if (!userID) continue; + const role = await supportServerGuild.roles.fetch(roleID).catch(console.error); + const member = await supportServerGuild.members.fetch(userID).catch(noOp); + if (!member) { + debugLog(`Failed to find member ${userID}`); + continue; + } + if (!role) { + debugLog(`Failed to find role ${roleID}`); + continue; + } + roleNames.set(roleID, role.name); + + if (!member.roles.cache.has(roleID)) { + await member.roles.add(roleID).catch(console.error); + } + + if (badge) { + const user = await mUserFetch(userID); + if (!user.user.badges.includes(badge)) { + await user.update({ + badges: { + push: badge + } + }); } - failed.push(`${name} (${err.message})`); - logError(err); } - }) - ); + } - const res = `**Roles** -${results.join('\n')} -${failed.length > 0 ? `Failed: ${failed.join(', ')}` : ''}`; + return returnStringOrFile( + `**Roles**\n${results.map(r => `${getUsernameSync(r.userID)} got ${roleNames.get(r.roleID)} because ${r.reason}`).join('\n')}` + ); + } - return res; + return 'Dry run'; } diff --git a/src/lib/util/addSubTaskToActivityTask.ts b/src/lib/util/addSubTaskToActivityTask.ts index e717d57794..8e0fe2f06c 100644 --- a/src/lib/util/addSubTaskToActivityTask.ts +++ b/src/lib/util/addSubTaskToActivityTask.ts @@ -39,24 +39,28 @@ export default async function addSubTaskToActivityTask 0 ? userIds.map(i => `'${i}'`).join(', ') : 'NULL'; - const specificUsers = - userIds.length > 0 - ? await prisma.$queryRawUnsafe<{ id: string; qty: number }[]>(` - SELECT user_id::text AS id, CARDINALITY(cl_array) - CARDINALITY(cl_array - array[${items - .map(i => `${i}`) - .join(', ')}]) AS qty - FROM user_stats s INNER JOIN users u ON s.user_id::text = u.id - WHERE ${ironmenOnly ? 'u."minion.ironman" = true AND' : ''} user_id::text IN (${userIdsList}) -`) - : []; - const generalUsers = await prisma.$queryRawUnsafe<{ id: string; qty: number }[]>(` - SELECT user_id::text AS id, CARDINALITY(cl_array) - CARDINALITY(cl_array - array[${items - .map(i => `${i}`) - .join(', ')}]) AS qty - FROM user_stats - ${ironmenOnly ? 'INNER JOIN "users" on "users"."id" = "user_stats"."user_id"::text' : ''} - WHERE (cl_array && array[${items.map(i => `${i}`).join(', ')}] - ${ironmenOnly ? 'AND "users"."minion.ironman" = true' : ''}) - ${userIds.length > 0 ? `AND user_id::text NOT IN (${userIdsList})` : ''} - ORDER BY qty DESC - LIMIT ${resultLimit} -`); +export async function fetchMultipleCLLeaderboards( + leaderboards: { + ironmenOnly: boolean; + items: number[]; + resultLimit: number; + clName: string; + }[] +) { + const userEvents = await prisma.userEvent.findMany({ + where: { + type: 'CLCompletion' + }, + orderBy: { + date: 'asc' + } + }); + const parsedLeaderboards = leaderboards.map(l => { + const userEventMap = userEventsToMap( + userEvents.filter(e => e.collection_log_name && stringMatches(e.collection_log_name, l.clName)) + ); + return { + ...l, + userEventMap + }; + }); + + const results = await prisma.$transaction([ + ...parsedLeaderboards.map(({ items, userEventMap, ironmenOnly, resultLimit }) => { + const SQL_ITEMS = `ARRAY[${items.map(i => `${i}`).join(', ')}]`; + const userIds = Array.from(userEventMap.keys()); + const userIdsList = userIds.length > 0 ? userIds.map(i => `'${i}'`).join(', ') : 'NULL'; + + const query = ` +SELECT id, qty +FROM ( + SELECT id, CARDINALITY(cl_array & ${SQL_ITEMS}) AS qty + FROM users + WHERE (cl_array && ${SQL_ITEMS} + ${ironmenOnly ? 'AND "users"."minion.ironman" = true' : ''}) ${userIds.length > 0 ? `OR id IN (${userIdsList})` : ''} + ) AS subquery +ORDER BY qty DESC +LIMIT ${resultLimit}; +`; + return prisma.$queryRawUnsafe<{ id: string; qty: number }[]>(query); + }) + ]); - const users = [...specificUsers, ...generalUsers] + return results.map((res, index) => { + const src = parsedLeaderboards[index]; + + const users = res .sort((a, b) => { const valueDifference = b.qty - a.qty; if (valueDifference !== 0) { return valueDifference; } - const dateA = userEventMap.get(a.id); - const dateB = userEventMap.get(b.id); + const dateA = src.userEventMap.get(a.id); + const dateB = src.userEventMap.get(b.id); if (dateA && dateB) { return dateA - dateB; } @@ -62,30 +71,26 @@ export async function fetchCLLeaderboard({ return 0; }) .filter(i => i.qty > 0); - return users; - } - const users = ( - await prisma.$queryRawUnsafe<{ id: string; qty: number }[]>(` -SELECT id, (cardinality(u.cl_keys) - u.inverse_length) as qty - FROM ( - SELECT array(SELECT * FROM jsonb_object_keys("collectionLogBank")) "cl_keys", - id, "collectionLogBank", - cardinality(array(SELECT * FROM jsonb_object_keys("collectionLogBank" - array[${items - .map(i => `'${i}'`) - .join(', ')}]))) "inverse_length" - FROM users - WHERE ("collectionLogBank" ?| array[${items.map(i => `'${i}'`).join(', ')}] - ${ironmenOnly ? 'AND "minion.ironman" = true' : ''}) - ${ - userIds.length > 0 - ? `OR (id in (${userIds.map(i => `'${i}'`).join(', ')})${ironmenOnly ? 'AND "minion.ironman" = true' : ''})` - : '' - } -) u -ORDER BY qty DESC -LIMIT ${resultLimit}; -`) - ).filter(i => i.qty > 0); - return users; + return { + ...src, + users + }; + }); +} + +export async function fetchCLLeaderboard({ + ironmenOnly, + items, + resultLimit, + clName +}: { + ironmenOnly: boolean; + items: number[]; + resultLimit: number; + method?: 'cl_array'; + clName: string; +}) { + const result = await fetchMultipleCLLeaderboards([{ ironmenOnly, items, resultLimit, clName }]); + return result[0]; } diff --git a/src/mahoji/commands/admin.ts b/src/mahoji/commands/admin.ts index 64af8ef97a..92701fbe01 100644 --- a/src/mahoji/commands/admin.ts +++ b/src/mahoji/commands/admin.ts @@ -482,11 +482,6 @@ export const adminCommand: OSBMahojiCommand = { } ] }, - { - type: ApplicationCommandOptionType.Subcommand, - name: 'sync_roles', - description: 'Sync roles' - }, { type: ApplicationCommandOptionType.Subcommand, name: 'badges', @@ -665,7 +660,6 @@ export const adminCommand: OSBMahojiCommand = { sync_blacklist?: {}; loot_track?: { name: string }; cancel_task?: { user: MahojiUserOption }; - sync_roles?: {}; badges?: { user: MahojiUserOption; add?: string; remove?: string }; bypass_age?: { user: MahojiUserOption }; command?: { enable?: string; disable?: string }; @@ -697,20 +691,6 @@ export const adminCommand: OSBMahojiCommand = { minionActivityCacheDelete(user.id); return 'Done.'; } - if (options.sync_roles) { - // try { - // const result = await runRolesTask(); - // if (result.length < 2000) return result; - // return { - // content: 'The result was too big! Check the file.', - // files: [new AttachmentBuilder(Buffer.from(result), { name: 'roles.txt' })] - // }; - // } catch (err: any) { - // logError(err); - // return `Failed to run roles task. ${err.message}`; - // } - return 'The roles task is disabled for now.'; - } if (options.badges) { if ((!options.badges.remove && !options.badges.add) || (options.badges.add && options.badges.remove)) { @@ -884,6 +864,7 @@ export const adminCommand: OSBMahojiCommand = { ${META_CONSTANTS.RENDERED_STR}` }).catch(noOp); import('exit-hook').then(({ gracefulExit }) => gracefulExit(1)); + return 'Turning off...'; } if (options.shut_down) { debugLog('SHUTTING DOWN'); @@ -893,13 +874,14 @@ ${META_CONSTANTS.RENDERED_STR}` content: `Shutting down in ${dateFm(new Date(Date.now() + timer))}.` }); await economyLog('Flushing economy log due to shutdown', true); - await Promise.all([sleep(timer), GrandExchange.queue.onEmpty()]); + await Promise.all([sleep(timer), GrandExchange.queue.onIdle()]); await sendToChannelID(Channel.GeneralChannel, { content: `I am shutting down! Goodbye :( ${META_CONSTANTS.RENDERED_STR}` }).catch(noOp); import('exit-hook').then(({ gracefulExit }) => gracefulExit(0)); + return 'Turning off...'; } if (options.sync_blacklist) { diff --git a/src/mahoji/commands/leaderboard.ts b/src/mahoji/commands/leaderboard.ts index f1c2e39cd4..f98a813caf 100644 --- a/src/mahoji/commands/leaderboard.ts +++ b/src/mahoji/commands/leaderboard.ts @@ -324,20 +324,10 @@ async function clLb( ) { const { resolvedCl, items } = getCollectionItems(inputType, false, false, true); if (!items || items.length === 0) { - return "That's not a valid collection log category. Check +cl for all possible logs."; + return "That's not a valid collection log category. Check /cl for all possible logs."; } - const userEventOrders = await prisma.userEvent.findMany({ - where: { - type: 'CLCompletion', - collection_log_name: resolvedCl.toLowerCase() - }, - orderBy: { - date: 'asc' - } - }); - - const users = await fetchCLLeaderboard({ ironmenOnly, items, resultLimit: 200, userEvents: userEventOrders }); + const { users } = await fetchCLLeaderboard({ ironmenOnly, items, resultLimit: 200, clName: resolvedCl }); inputType = toTitleCase(inputType.toLowerCase()); return doMenuWrapper({ diff --git a/src/mahoji/commands/rp.ts b/src/mahoji/commands/rp.ts index 99f8533f3f..0ca844d664 100644 --- a/src/mahoji/commands/rp.ts +++ b/src/mahoji/commands/rp.ts @@ -11,8 +11,7 @@ import { Bank } from 'oldschooljs'; import type { Item } from 'oldschooljs/dist/meta/types'; import { ADMIN_IDS, OWNER_IDS, SupportServer, production } from '../../config'; -import { analyticsTick } from '../../lib/analytics'; -import { BitField, Channel } from '../../lib/constants'; +import { BitField, Channel, globalConfig } from '../../lib/constants'; import { allCollectionLogsFlat } from '../../lib/data/Collections'; import type { GearSetupType } from '../../lib/gear/types'; import { GrandExchange } from '../../lib/grandExchange'; @@ -21,10 +20,11 @@ import { unEquipAllCommand } from '../../lib/minions/functions/unequipAllCommand import { unequipPet } from '../../lib/minions/functions/unequipPet'; import { premiumPatronTime } from '../../lib/premiumPatronTime'; +import { runRolesTask } from '../../lib/rolesTask'; 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 { dateFm, isValidDiscordSnowflake } from '../../lib/util'; import getOSItem from '../../lib/util/getOSItem'; import { handleMahojiConfirmation } from '../../lib/util/handleMahojiConfirmation'; import { deferInteraction } from '../../lib/util/interactionReply'; @@ -44,7 +44,14 @@ import { sellPriceOfItem } from './sell'; const itemFilters = [ { name: 'Tradeable', - filter: (item: Item) => itemIsTradeable(item.id, true) + filter: (item: Item) => itemIsTradeable(item.id, true), + run: async () => { + const isValid = await GrandExchange.extensiveVerification(); + if (isValid) { + return 'No issues found.'; + } + return 'Something was invalid. Check logs!'; + } } ]; @@ -142,6 +149,35 @@ function isProtectedAccount(user: MUser) { return false; } +const actions = [ + { + name: 'validate_ge', + allowed: (user: MUser) => ADMIN_IDS.includes(user.id) || OWNER_IDS.includes(user.id), + run: async () => { + const isValid = await GrandExchange.extensiveVerification(); + if (isValid) { + return 'No issues found.'; + } + return 'Something was invalid. Check logs!'; + } + }, + { + name: 'sync_roles', + allowed: (user: MUser) => + ADMIN_IDS.includes(user.id) || OWNER_IDS.includes(user.id) || user.bitfield.includes(BitField.isModerator), + run: async () => { + return runRolesTask(!globalConfig.isProduction); + } + }, + { + name: 'sync_usernames', + allowed: (user: MUser) => ADMIN_IDS.includes(user.id) || OWNER_IDS.includes(user.id), + run: async () => { + return usernameSync(); + } + } +]; + export const rpCommand: OSBMahojiCommand = { name: 'rp', description: 'Admin tools second set', @@ -150,45 +186,13 @@ export const rpCommand: OSBMahojiCommand = { { type: ApplicationCommandOptionType.SubcommandGroup, name: 'action', - description: 'Action tools', - options: [ - { - type: ApplicationCommandOptionType.Subcommand, - name: 'validate_ge', - description: 'Validate the g.e.', - options: [] - }, - { - type: ApplicationCommandOptionType.Subcommand, - name: 'patreon_reset', - description: 'Reset all patreon data.', - options: [] - }, - { - type: ApplicationCommandOptionType.Subcommand, - name: 'view_all_items', - description: 'View all item IDs present in banks/cls.', - options: [] - }, - { - type: ApplicationCommandOptionType.Subcommand, - name: 'analytics_tick', - description: 'analyticsTick.', - options: [] - }, - { - type: ApplicationCommandOptionType.Subcommand, - name: 'networth_sync', - description: 'networth_sync.', - options: [] - }, - { - type: ApplicationCommandOptionType.Subcommand, - name: 'redis_sync', - description: 'redis sync.', - options: [] - } - ] + description: 'Actions', + options: actions.map(a => ({ + type: ApplicationCommandOptionType.Subcommand, + name: a.name, + description: a.name, + options: [] + })) }, { type: ApplicationCommandOptionType.SubcommandGroup, @@ -520,14 +524,7 @@ export const rpCommand: OSBMahojiCommand = { max_total?: { user: MahojiUserOption; type: UserEventType; message_id: string }; max?: { user: MahojiUserOption; type: UserEventType; skill: xp_gains_skill_enum; message_id: string }; }; - action?: { - validate_ge?: {}; - patreon_reset?: {}; - view_all_items?: {}; - analytics_tick?: {}; - networth_sync?: {}; - redis_sync?: {}; - }; + action?: any; player?: { viewbank?: { user: MahojiUserOption; json?: boolean }; add_patron_time?: { user: MahojiUserOption; tier: number; time: string }; @@ -620,52 +617,13 @@ Date: ${dateFm(date)}`; if (!isMod) return randArrItem(gifs); - if (options.action?.validate_ge) { - const isValid = await GrandExchange.extensiveVerification(); - if (isValid) { - return 'No issues found.'; - } - return 'Something was invalid. Check logs!'; - } - if (options.action?.analytics_tick) { - await analyticsTick(); - return 'Finished.'; - } - if (options.action?.redis_sync) { - const result = await usernameSync(); - return result; - } - if (options.action?.networth_sync) { - const users = await prisma.user.findMany({ - where: { - GP: { - gt: 10_000_000_000 - } - }, - take: 20, - orderBy: { - GP: 'desc' - }, - select: { - id: true + if (options.action) { + for (const action of actions) { + if (options.action[action.name]) { + if (!action.allowed(adminUser)) return randArrItem(gifs); + return action.run(); } - }); - for (const { id } of users) { - const user = await mUserFetch(id); - await user.update({ - cached_networth_value: (await user.calculateNetWorth()).value - }); } - return 'Done.'; - } - if (options.action?.view_all_items) { - const result = await prisma.$queryRawUnsafe<{ item_id: number }[]>(`SELECT DISTINCT json_object_keys(bank)::int AS item_id -FROM users -UNION -SELECT DISTINCT jsonb_object_keys("collectionLogBank")::int AS item_id -FROM users -ORDER BY item_id ASC;`); - return returnStringOrFile(`[${result.map(i => i.item_id).join(',')}]`); } if (options.player?.set_buy_date) { diff --git a/src/mahoji/lib/abstracted_commands/mageTrainingArenaCommand.ts b/src/mahoji/lib/abstracted_commands/mageTrainingArenaCommand.ts index 1ee8cb5719..3c71531837 100644 --- a/src/mahoji/lib/abstracted_commands/mageTrainingArenaCommand.ts +++ b/src/mahoji/lib/abstracted_commands/mageTrainingArenaCommand.ts @@ -113,7 +113,7 @@ ${mageTrainingArenaBuyables .map(i => `${i.item.name} - ${i.cost} pts - ${formatDuration((i.cost / pizazzPointsPerHour) * (Time.Minute * 60))}`) .join('\n')} -Hint: Magic Training Arena is combined into 1 room, and 1 set of points - rewards take approximately the same amount of time to get. To get started use **/minigames mage_training_arena train**. You can buy rewards using **/minigames mage_training_arena buy**.`; +Hint: Magic Training Arena is combined into 1 room, and 1 set of points - rewards take approximately the same amount of time to get. To get started use **/minigames mage_training_arena start**. You can buy rewards using **/minigames mage_training_arena buy**.`; } export async function mageTrainingArenaStartCommand(user: MUser, channelID: string): CommandResponse { diff --git a/src/tasks/minions/HunterActivity/hunterActivity.ts b/src/tasks/minions/HunterActivity/hunterActivity.ts index 914d29dfdd..7a333ffdc2 100644 --- a/src/tasks/minions/HunterActivity/hunterActivity.ts +++ b/src/tasks/minions/HunterActivity/hunterActivity.ts @@ -179,7 +179,7 @@ export const hunterTask: MinionTask = { let str = `${user}, ${user.minionName} finished hunting ${creature.name}${ crystalImpling ? '.' - : `${quantity}x times, due to clever creatures you missed out on ${ + : ` ${quantity}x times, due to clever creatures you missed out on ${ quantity - successfulQuantity }x catches. ` }${xpStr}\n\nYou received: ${loot}.${magicSecStr.length > 1 ? magicSecStr : ''}`; diff --git a/tests/integration/clArrayUpdate.test.ts b/tests/integration/clArrayUpdate.test.ts index 9c190aca5a..15f0917f16 100644 --- a/tests/integration/clArrayUpdate.test.ts +++ b/tests/integration/clArrayUpdate.test.ts @@ -1,4 +1,3 @@ -import { Time } from 'e'; import { expect, test } from 'vitest'; import { Bank } from 'oldschooljs'; @@ -6,39 +5,33 @@ import { itemID } from 'oldschooljs/dist/util'; import { roboChimpSyncData } from '../../src/lib/roboChimp'; import { createTestUser } from './util'; -test( - 'All Commands Base Test', - async () => { - const user = await createTestUser(); - await user.addItemsToBank({ items: new Bank().add('Coal', 100) }); - await roboChimpSyncData(user); - expect(user.fetchStats({ cl_array: true, cl_array_length: true })).resolves.toMatchObject({ - cl_array: [], - cl_array_length: 0 - }); +test('CL Updates', async () => { + const user = await createTestUser(); + await user.addItemsToBank({ items: new Bank().add('Coal', 100) }); + await roboChimpSyncData(user); + expect(await user.fetchStats({ cl_array: true, cl_array_length: true })).toMatchObject({ + cl_array: [], + cl_array_length: 0 + }); - await user.addItemsToBank({ items: new Bank().add('Egg', 100), collectionLog: true }); - await roboChimpSyncData(user); - expect(user.fetchStats({ cl_array: true, cl_array_length: true })).resolves.toMatchObject({ - cl_array: [itemID('Egg')], - cl_array_length: 1 - }); + await user.addItemsToBank({ items: new Bank().add('Egg', 100), collectionLog: true }); + await roboChimpSyncData(user); + expect(await user.fetchStats({ cl_array: true, cl_array_length: true })).toMatchObject({ + cl_array: [itemID('Egg')], + cl_array_length: 1 + }); - await user.addItemsToBank({ items: new Bank().add('Egg', 100), collectionLog: true }); - await roboChimpSyncData(user); - expect(user.fetchStats({ cl_array: true, cl_array_length: true })).resolves.toMatchObject({ - cl_array: [itemID('Egg')], - cl_array_length: 1 - }); + await user.addItemsToBank({ items: new Bank().add('Egg', 100), collectionLog: true }); + await roboChimpSyncData(user); + expect(await user.fetchStats({ cl_array: true, cl_array_length: true })).toMatchObject({ + cl_array: [itemID('Egg')], + cl_array_length: 1 + }); - await user.addItemsToBank({ items: new Bank().add('Trout', 100), collectionLog: true }); - await roboChimpSyncData(user); - expect(user.fetchStats({ cl_array: true, cl_array_length: true })).resolves.toMatchObject({ - cl_array: [itemID('Trout'), itemID('Egg')], - cl_array_length: 2 - }); - }, - { - timeout: Time.Minute * 10 - } -); + await user.addItemsToBank({ items: new Bank().add('Trout', 100), collectionLog: true }); + await roboChimpSyncData(user); + expect(await user.fetchStats({ cl_array: true, cl_array_length: true })).toMatchObject({ + cl_array: [itemID('Trout'), itemID('Egg')], + cl_array_length: 2 + }); +}); diff --git a/tests/integration/misc.test.ts b/tests/integration/misc.test.ts index 2d113c1b15..eae4db6a9a 100644 --- a/tests/integration/misc.test.ts +++ b/tests/integration/misc.test.ts @@ -1,4 +1,3 @@ -import type { UserEvent } from '@prisma/client'; import { randArrItem } from 'e'; import { describe, expect, test } from 'vitest'; @@ -18,30 +17,14 @@ describe('Integration Misc', () => { expect(await global.prisma!.analytic.count()).toBeGreaterThanOrEqual(1); }); test('fetchCLLeaderboard', async () => { + const cl = randArrItem(allCollectionLogsFlat); for (const ironManOnly of [true, false]) { - for (const method of ['cl_array', 'raw_cl'] as const) { - for (const userEvents of [ - [ - { - id: 'asdf', - date: new Date(), - user_id: '123', - type: 'CLCompletion', - skill: null, - collection_log_name: 'giant mole' - } as UserEvent - ], - null - ]) { - await fetchCLLeaderboard({ - ironmenOnly: ironManOnly, - method, - userEvents, - resultLimit: 100, - items: randArrItem(allCollectionLogsFlat).items - }); - } - } + await fetchCLLeaderboard({ + ironmenOnly: ironManOnly, + resultLimit: 100, + items: cl.items, + clName: cl.name + }); } await Promise.all([fetchCLLeaderboard]); }); diff --git a/tests/integration/redis.test.ts b/tests/integration/redis.test.ts index 7584d1bd67..1407d57583 100644 --- a/tests/integration/redis.test.ts +++ b/tests/integration/redis.test.ts @@ -15,7 +15,7 @@ test('Should add patron badge', async () => { const user = await createTestUser(); expect(user.user.badges).not.includes(BadgesEnum.Patron); const _redis = makeSender(); - _redis.publish({ + await _redis.publish({ type: 'patron_tier_change', discord_ids: [user.id], new_tier: 1, @@ -31,7 +31,7 @@ test('Should remove patron badge', async () => { const user = await createTestUser(undefined, { badges: [BadgesEnum.Patron] }); expect(user.user.badges).includes(BadgesEnum.Patron); const _redis = makeSender(); - _redis.publish({ + await _redis.publish({ type: 'patron_tier_change', discord_ids: [user.id], new_tier: 0, @@ -52,7 +52,7 @@ test('Should add to cache', async () => { })) }); const _redis = makeSender(); - _redis.publish({ + await _redis.publish({ type: 'patron_tier_change', discord_ids: users.map(u => u.id), new_tier: 5, @@ -76,7 +76,7 @@ test('Should remove from cache', async () => { })) }); const _redis = makeSender(); - _redis.publish({ + await _redis.publish({ type: 'patron_tier_change', discord_ids: users.map(u => u.id), new_tier: 0, diff --git a/tests/integration/rolesTask.test.ts b/tests/integration/rolesTask.test.ts index 252bee8faf..dcd69d0dcc 100644 --- a/tests/integration/rolesTask.test.ts +++ b/tests/integration/rolesTask.test.ts @@ -43,7 +43,7 @@ describe.skip('Roles Task', async () => { duration: 10_000 } }); - const result = await runRolesTask(); + const result = await runRolesTask(true); expect(result).toBeTruthy(); expect(result).includes('Roles'); }); diff --git a/yarn.lock b/yarn.lock index be7b9c09c5..5dfb8015f6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -646,9 +646,9 @@ __metadata: languageName: node linkType: hard -"@oldschoolgg/toolkit@git+https://github.com/oldschoolgg/toolkit.git#2813f25327093fcf2cb12bee7d4c85ce629069a0": +"@oldschoolgg/toolkit@git+https://github.com/oldschoolgg/toolkit.git#cd7c6865229ca7dc4a66b3816586f2d3f4a4fbed": version: 0.0.24 - resolution: "@oldschoolgg/toolkit@https://github.com/oldschoolgg/toolkit.git#commit=2813f25327093fcf2cb12bee7d4c85ce629069a0" + resolution: "@oldschoolgg/toolkit@https://github.com/oldschoolgg/toolkit.git#commit=cd7c6865229ca7dc4a66b3816586f2d3f4a4fbed" dependencies: decimal.js: "npm:^10.4.3" deep-object-diff: "npm:^1.1.9" @@ -664,7 +664,7 @@ __metadata: peerDependencies: discord.js: ^14.15.3 oldschooljs: ^2.5.9 - checksum: 10c0/c83f2188e18ac1e7d79edd9ab06b7ab0f96f8774a404a26fd766d22606bd65a8def0c0a74d0f681c5bde0d7b5b5bbb16f829e751b4e75f6821a1baf5c62b2580 + checksum: 10c0/42eaec1c99c671adab7b56ca7e11d37bf5a0e07d0f0da0a892cdf477a78c061ea131a43b1c578d09f1c6b02e05d1ce47db9586ad9a8de62679cc492c847c3fca languageName: node linkType: hard @@ -3972,6 +3972,15 @@ __metadata: languageName: node linkType: hard +"remeda@npm:^2.7.0": + version: 2.7.0 + resolution: "remeda@npm:2.7.0" + dependencies: + type-fest: "npm:^4.21.0" + checksum: 10c0/4e7d0dc616f00961653244ea9df3f297720fc9346ac8ec7502abf4c434741af4a4750d5bd83ea9938ee406089b37e3a2270b8f022d48b345ba83218e47dd8918 + languageName: node + linkType: hard + "require-directory@npm:^2.1.1": version: 2.1.1 resolution: "require-directory@npm:2.1.1" @@ -4113,7 +4122,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#2813f25327093fcf2cb12bee7d4c85ce629069a0" + "@oldschoolgg/toolkit": "git+https://github.com/oldschoolgg/toolkit.git#cd7c6865229ca7dc4a66b3816586f2d3f4a4fbed" "@prisma/client": "npm:^5.17.0" "@sapphire/ratelimits": "npm:^2.4.9" "@sapphire/snowflake": "npm:^3.5.3" @@ -4148,6 +4157,7 @@ __metadata: prettier: "npm:^3.3.2" prisma: "npm:^5.17.0" random-js: "npm:^2.1.0" + remeda: "npm:^2.7.0" simple-statistics: "npm:^7.8.3" sonic-boom: "npm:^4.0.1" tsx: "npm:^4.16.2" @@ -4613,6 +4623,13 @@ __metadata: languageName: node linkType: hard +"type-fest@npm:^4.21.0": + version: 4.23.0 + resolution: "type-fest@npm:4.23.0" + checksum: 10c0/c42bb14e99329ab37983d1f188e307bf0cc705a23807d9b2268d8fb2ae781d610ac6e2058dde8f9ea2b1b8ddc77ceb578d157fa81f69f8f70aef1d42fb002996 + languageName: node + linkType: hard + "typescript@npm:^5.2.2, typescript@npm:^5.5.3": version: 5.5.3 resolution: "typescript@npm:5.5.3"