diff --git a/.dockerignore b/.dockerignore index 24983d9bf6..5e4edee99e 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,4 +1,8 @@ .env icon_cache logs -dist \ No newline at end of file +dist +node_modules +coverage +.yarn +.tests \ No newline at end of file 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/.env.test b/.env.test index d3c4fd9c3c..63f5c51628 100644 --- a/.env.test +++ b/.env.test @@ -2,4 +2,5 @@ TZ="UTC" CLIENT_ID=111398433321891634 BOT_TOKEN=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA TEST=true -CI=true \ No newline at end of file +CI=true +YARN_ENABLE_HARDENED_MODE=0 \ No newline at end of file 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/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml index 89ec39950c..118047b3da 100644 --- a/.github/workflows/unit_tests.yml +++ b/.github/workflows/unit_tests.yml @@ -11,7 +11,7 @@ jobs: test: name: Node v${{ matrix.node_version }} - ${{ matrix.os }} runs-on: ${{ matrix.os }} - timeout-minutes: 10 + timeout-minutes: 5 strategy: matrix: node_version: [20.15.0] @@ -43,8 +43,4 @@ jobs: - name: Build run: yarn build:tsc - name: Test - run: | - yarn test:unit - yarn test:lint - tsc -p tests/integration && tsc -p tests/unit - npm i -g dpdm && dpdm --exit-code circular:1 --progress=false --warning=false --tree=false ./dist/index.js + run: yarn test:ci:unit diff --git a/.yarnrc.yml b/.yarnrc.yml index 55d6c720ef..9e33970694 100644 --- a/.yarnrc.yml +++ b/.yarnrc.yml @@ -2,3 +2,4 @@ nodeLinker: node-modules telemetryInterval: 999999999999 enableTelemetry: false checksumBehavior: "update" +enableHardenedMode: false diff --git a/SETUP.md b/SETUP.md index 87c7f5d7a9..ee08f7bdf0 100644 --- a/SETUP.md +++ b/SETUP.md @@ -15,7 +15,7 @@ This assumes you are using VSCode as your IDE. If you have errors or issues, you 2. Install [Postgres 16](https://www.postgresql.org/download/) and PGAdmin4 for interacting with postgres (optional, but helpful) 3. Install Yarn using: `npm i -g yarn` 4. Clone the repo: `git clone https://github.com/oldschoolgg/oldschoolbot.git` -5. Run `corepack enable` and `yarn` in the root of the repo. +5. Run the following commands in the root of the repo: `corepack enable`, `yarn`, `npx prisma db push` and `npx prisma db push --schema ./prisma/robochimp.prisma` ### Configuration diff --git a/docker-compose.yml b/docker-compose.yml index 36de7bd1b2..26ea7e6f3c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -19,8 +19,8 @@ services: depends_on: - db environment: - ROBOCHIMP_DATABASE_URL: postgresql://postgres:postgres@db:5435/robochimp_integration_test?connection_limit=500&pool_timeout=0&schema=public - DATABASE_URL: postgresql://postgres:postgres@db:5435/osb_integration_test?connection_limit=500&pool_timeout=0&schema=public + ROBOCHIMP_DATABASE_URL: postgresql://postgres:postgres@db:5435/robochimp_integration_test?connection_limit=10&pool_timeout=0&schema=public + DATABASE_URL: postgresql://postgres:postgres@db:5435/osb_integration_test?connection_limit=10&pool_timeout=0&schema=public WAIT_HOSTS: db:5435 volumes: diff --git a/fish.test.ts b/fish.test.ts new file mode 100644 index 0000000000..58984166d7 --- /dev/null +++ b/fish.test.ts @@ -0,0 +1,94 @@ +import { Bank } from 'oldschooljs'; +import { describe, it } from 'vitest'; + +import { Gear } from '../../../src/lib/structures/Gear'; +import { fishCommand } from '../../../src/mahoji/commands/fish'; +import { testRunCmd } from '../utils'; + +describe('Fish Command', () => { + it('should handle insufficient fishing level', () => { + testRunCmd({ + cmd: fishCommand, + opts: { name: 'Trout/Salmon', quantity: 1 }, + result: '<:minion:778418736180494347> Your minion needs 20 Fishing to fish Trout/Salmon.' + }); + }); + + it('should handle insufficient QP', () => { + testRunCmd({ + cmd: fishCommand, + opts: { name: 'karambwanji', quantity: 1 }, + user: { skills_fishing: 9_999_999, QP: 0 }, + result: 'You need 15 qp to catch those!' + }); + }); + + it('should handle invalid fish', () => { + testRunCmd({ + cmd: fishCommand, + opts: { name: 'asdf' }, + result: 'Thats not a valid fish to catch.' + }); + }); + + it('should handle insufficient barb fishing levels', () => { + testRunCmd({ + cmd: fishCommand, + opts: { name: 'Barbarian fishing' }, + user: { skills_fishing: 1 }, + result: '<:minion:778418736180494347> Your minion needs 48 Fishing to fish Barbarian fishing.' + }); + }); + + it('should fish', () => { + testRunCmd({ + cmd: fishCommand, + opts: { name: 'Shrimps/Anchovies' }, + result: "<:minion:778418736180494347> Your minion is now fishing Shrimps/Anchovies, it'll take around 30 minutes to finish" + }); + }); + + it('should catch insufficient feathers', () => { + testRunCmd({ + cmd: fishCommand, + opts: { name: 'Barbarian fishing' }, + user: { + skills_fishing: 999_999, + skills_agility: 999_999, + skills_strength: 999_999, + meleeGear: new Gear({ weapon: 'Pearl barbarian rod' }) + }, + result: 'You need Feather to fish Barbarian fishing!' + }); + }); + + it('should boost', () => { + testRunCmd({ + cmd: fishCommand, + opts: { name: 'Barbarian fishing' }, + user: { + skills_fishing: 999_999, + skills_agility: 999_999, + skills_strength: 999_999, + bank: new Bank().add('Feather', 1000) + }, + result: `<:minion:778418736180494347> Your minion is now fishing 100x Barbarian fishing, it'll take around 6 minutes, 1 second to finish. + +**Boosts:** 5% for Pearl barbarian rod.` + }); + }); + + it('should fish barrel boost', () => { + testRunCmd({ + cmd: fishCommand, + opts: { name: 'shrimps' }, + user: { + skills_fishing: 999_999, + meleeGear: new Gear({ cape: 'Fish sack barrel' }) + }, + result: `<:minion:778418736180494347> Your minion is now fishing Shrimps/Anchovies, it'll take around 39 minutes, 1 second to finish. + + **Boosts:** +9 trip minutes and +28 inventory slots for having a Fish sack barrel.` + }); + }); +}); diff --git a/fish.ts b/fish.ts new file mode 100644 index 0000000000..552dfa698b --- /dev/null +++ b/fish.ts @@ -0,0 +1,398 @@ +import { stringMatches } from '@oldschoolgg/toolkit'; +import type { CommandRunOptions } from '@oldschoolgg/toolkit'; +import { ApplicationCommandOptionType } from 'discord.js'; +import { Time } from 'e'; +import { Bank } from 'oldschooljs'; +import TzTokJad from 'oldschooljs/dist/simulation/monsters/special/TzTokJad'; +import { WildernessDiary, userhasDiaryTier } from '../../lib/diaries'; + +import Fishing from '../../lib/skilling/skills/fishing'; +import { Fish, SkillsEnum } from '../../lib/skilling/types'; +import type { FishingActivityTaskOptions } from '../../lib/types/minions'; +//import { formatDuration, itemID, itemNameFromID } from '../../lib/util'; +import { formatDuration, itemNameFromID } from '../../lib/util'; +import addSubTaskToActivityTask from '../../lib/util/addSubTaskToActivityTask'; +import { calcMaxTripLength } from '../../lib/util/calcMaxTripLength'; +import type { OSBMahojiCommand } from '../lib/util'; +import { MUserClass } from '../../lib/MUser'; + + + +function radasBlessing(user: MUser) { + const blessingBoosts = [ + ["Rada's blessing 4", 8], + ["Rada's blessing 3", 6], + ["Rada's blessing 2", 4], + ["Rada's blessing 1", 2] + ]; + + for (const [itemName, boostPercent] of blessingBoosts) { + if (user.hasEquipped(itemName)) { + return { blessingEquipped: true, blessingChance: boostPercent as number }; + } + } + return { blessingEquipped: false, blessingChance: 0 }; +} + + + +function rollExtraLoot( + lootAmount: number, + flakesUsed: number, + currentInv: number, + blessingChance: number, + spirit_flakes: boolean, + flakesQuantity: number +): [number, number, number] { + currentInv++; + if (Math.random() < blessingChance / 100) { + lootAmount++; + currentInv++; + } + if (spirit_flakes && flakesUsed < flakesQuantity && Math.random() < 0.5) { + lootAmount++; + flakesUsed++; + currentInv++; + } + // Return updated values + return [lootAmount, flakesUsed, currentInv]; +} + + +function determineFishingTime( + quantity: number, + tripTicks: number, + powerfish: boolean, + spirit_flakes: boolean, + fish: Fish, + user: MUserClass, + invSlots: number, + blessingChance: number, + flakesQuantity: number, + harpoonBoost: number +) { + let ticksElapsed = 0; + let catches1 = 0, catches2 = 0, catches3 = 0; + let lootAmount1 = 0, lootAmount2 = 0, lootAmount3 = 0; + let flakesUsed = 0; + let currentInv = 0; + + let fishLvl = user.skillLevel(SkillsEnum.Fishing); + let effFishLvl = fishLvl; + if (fishLvl > 68) { + if (fish.name === 'Shark' || fish.name === 'Mackerel/Cod/Bass' || fish.name === 'Lobster') { + effFishLvl += 7; // fishing guild boost + } else if (fish.name === 'Tuna/Swordfish' && !powerfish) { + effFishLvl += 7; // can't 2t in the guild + } + } + + // probabilities of catching a fish at the user's fishing lvl + let p1 = harpoonBoost * (fish.chance1Lvl99! - (99 - effFishLvl) * (fish.chance1Lvl99! - fish.chance1Lvl1!) / (99 - 1)); + let p2 = fish.id2 === undefined ? 0 : harpoonBoost * (fish.chance2Lvl99! - (99 - effFishLvl) * (fish.chance2Lvl99! - fish.chance2Lvl1!) / (99 - 1)); + let p3 = fish.id3 === undefined ? 0 : harpoonBoost * (fish.chance3Lvl99! - (99 - effFishLvl) * (fish.chance3Lvl99! - fish.chance2Lvl1!) / (99 - 1)); + + let ticksPerRoll = fish.ticksPerRoll!; + let lostTicks = fish.lostTicks!; + let bankingTime = fish.bankingTime; + + if (fish.name === 'Barbarian fishing') { + if (powerfish) { + ticksPerRoll = 3; + lostTicks = 0.02; // more focused + } + if (user.allItemsOwned.has('Fishing cape') || user.allItemsOwned.has('Fishing cape (t)') || user.allItemsOwned.has('Max cape')) { + bankingTime = 20; + } + } else if (fish.name === 'Trout/Salmon') { + if (powerfish) { + ticksPerRoll = 3; + lostTicks = 0.03; + } + } else if (fish.name === 'Tuna/Swordfish') { + if (powerfish) { + ticksPerRoll = 2; + lostTicks = 0.01; + } + } + + if (powerfish) { + while (ticksElapsed < tripTicks) { + if (p3 != 0 && Math.random() < p3) { + catches3++; // roll for the highest lvl fish first + } else if (p2 != 0 && Math.random() < p2) { + catches2++; // then the second only if first one wasn't caught + } else if (Math.random() < p1) { + catches1++; + } + ticksElapsed += ticksPerRoll! * (1 + lostTicks!); // only part of the code that's not exactly how it works in osrs approximate + + if (catches1 + catches2 + catches3 >= quantity) { + break; + } + } + } else { + while (ticksElapsed < tripTicks) { + if (p3 != 0 && Math.random() < p3) { + catches3++; + lootAmount3++; + [lootAmount3, flakesUsed, currentInv] = rollExtraLoot(lootAmount3, flakesUsed, currentInv, blessingChance, spirit_flakes, flakesQuantity); + } else if (p2 != 0 && Math.random() < p2) { + catches2++; + lootAmount2++; + [lootAmount2, flakesUsed, currentInv] = rollExtraLoot(lootAmount2, flakesUsed, currentInv, blessingChance, spirit_flakes, flakesQuantity); + } else if (Math.random() < p1) { + catches1++; + lootAmount1++; + [lootAmount1, flakesUsed, currentInv] = rollExtraLoot(lootAmount1, flakesUsed, currentInv, blessingChance, spirit_flakes, flakesQuantity); + } + + ticksElapsed += ticksPerRoll! * (1 + lostTicks!); + + if (catches1 + catches2 + catches3 >= quantity) { + break; + } + + if (currentInv >= invSlots) { + ticksElapsed += bankingTime!; + currentInv = 0; + } + + } + } + + return { catches1, catches2, catches3, lootAmount1, lootAmount2, lootAmount3, ticksElapsed, flakesUsed }; +} + + + + +export const fishCommand: OSBMahojiCommand = { + name: 'fish', + description: 'Send your minion to fish fish.', + attributes: { + requiresMinion: true, + requiresMinionNotBusy: true, + examples: ['/fish name:Shrimp'] + }, + options: [ + { + type: ApplicationCommandOptionType.String, + name: 'name', + description: 'The thing you want to fish.', + required: true, + autocomplete: async (value: string) => { + return Fishing.Fishes.filter(i => + !value ? true : i.name.toLowerCase().includes(value.toLowerCase()) + ).map(i => ({ + name: i.name, + value: i.name + })); + } + }, + { + type: ApplicationCommandOptionType.Integer, + name: 'minutes', + description: 'Trip length in minutes (optional).', + required: false, + min_value: 1 + }, + { + type: ApplicationCommandOptionType.Boolean, + name: 'powerfish', + description: 'Set this to true to powerfish. Higher xp/hour, no loot (default false, optional).', + required: false + }, + { + type: ApplicationCommandOptionType.Boolean, + name: 'spirit_flakes', + description: 'Set this to true to use spirit flakes (default false, optional).', + required: false + } + ], + run: async ({ + options, + userID, + channelID + }: CommandRunOptions<{ + name: string; + quantity?: number, + powerfish?: boolean, + spirit_flakes?: boolean + }>) => { + const user = await mUserFetch(userID); + const fish = Fishing.Fishes.find( + fish => + stringMatches(fish.id, options.name) || + stringMatches(fish.name, options.name) || + fish.alias?.some(alias => stringMatches(alias, options.name)) + ); + if (!fish) return 'Thats not a valid fish to catch.'; + + let { quantity, powerfish, spirit_flakes } = options; + + quantity = quantity ?? 3000; + powerfish = powerfish ?? false; + if (powerfish) { + spirit_flakes = false; // don't use flakes if power fishing + } + spirit_flakes = spirit_flakes ?? false; + + // requirement checks + if (user.skillLevel(SkillsEnum.Fishing) < fish.level) { + return `${user.minionName} needs ${fish.level} Fishing to fish ${fish.name}.`; + } + + if (fish.qpRequired) { + if (user.QP < fish.qpRequired) { + return `You need ${fish.qpRequired} qp to catch those!`; + } + } + + if ( + fish.name === 'Barbarian fishing' && + (user.skillLevel(SkillsEnum.Agility) < 15 || user.skillLevel(SkillsEnum.Strength) < 15) + ) { + return 'You need at least 15 Agility and Strength to do Barbarian Fishing.'; + } + + if (fish.name === 'Infernal eel') { + const jadKC = await user.getKC(TzTokJad.id); + if (jadKC === 0) { + return 'You are not worthy JalYt. Before you can fish Infernal Eels, you need to have defeated the mighty TzTok-Jad!'; + } + } + + const anglerOutfit = Object.keys(Fishing.anglerItems).map(i => itemNameFromID(Number.parseInt(i))); + if (fish.name === 'Minnow' && anglerOutfit.some(test => !user.hasEquippedOrInBank(test!))) { + return 'You need to own the Angler Outfit to fish for Minnows.'; + } + + // boosts + const boosts = []; + if (fish.name === 'Tuna/Swordfish' || fish.name === 'Shark') { + if (user.hasEquipped('Crystal harpoon')) { + boosts.push('35% for Crystal harpoon'); + } else if (user.hasEquipped('Dragon harpoon')) { + boosts.push('20% for Dragon harpoon'); + } else if (user.hasEquipped('Infernal harpoon')) { + boosts.push('20% for Infernal harpoon'); + } + } + + if (powerfish) { + boosts.push('**Powerfishing**'); + } + + if (!powerfish) { + if (user.allItemsOwned.has('Fish sack barrel') || user.allItemsOwned.has('Fish barrel')) { + if (fish.name === 'Minnow' || fish.name === 'Karambwanji' || fish.name === 'Infernal eel') { + boosts.push(`+9 trip minutes for having a ${user.allItemsOwned.has('Fish sack barrel') ? 'Fish sack barrel' : 'Fish barrel'}`); + } else { + boosts.push(`+9 trip minutes and +28 inventory slots for having a ${user.allItemsOwned.has('Fish sack barrel') ? 'Fish sack barrel' : 'Fish barrel'}`); + } + } + } + + if (fish.name === 'Dark crab') { + const [hasWildyElite] = await userhasDiaryTier(user, WildernessDiary.elite); + if (hasWildyElite) { + fish.chance1Lvl1 = 0.0961; + fish.chance1Lvl99 = 0.3439; + boosts.push('Increased dark crab catch rate from having the Elite Wilderness Diary'); + } + } + + if (spirit_flakes) { + if (!user.bank.has('Spirit flakes')) { + return 'You need to have at least one spirit flake!'; + } + + boosts.push(`\n50% more fish from using spirit flakes`); + } + + const { blessingEquipped, blessingChance } = radasBlessing(user); + if (blessingEquipped) { + boosts.push(`\nYour Rada's Blessing gives ${blessingChance}% chance of extra fish`); + } + + + let harpoonBoost = 1.0; + if (fish.name === 'Tuna/Swordfish' || fish.name === 'Shark') { + if (user.hasEquipped("Dragon harpoon") || user.hasEquipped("Infernal harpoon")) { + harpoonBoost = 1.2; + } else if (user.hasEquipped("Crystal harpoon")) { + harpoonBoost = 1.35; + } + } + + let invSlots = 26; + if (user.allItemsOwned.has('Fish sack barrel') || user.allItemsOwned.has('Fish barrel')) { + invSlots += 28; + } + + let maxTripLength = calcMaxTripLength(user, 'Fishing'); + if (!powerfish && (user.allItemsOwned.has('Fish sack barrel') || user.allItemsOwned.has('Fish barrel'))) { + maxTripLength += Time.Minute * 9; + } + let tripTicks = maxTripLength / (Time.Second * 0.6); + + let flakesQuantity = user.bank.amount('Spirit flakes'); + + if (fish.bait) { + const baseCost = new Bank().add(fish.bait); + const maxCanDo = user.bank.fits(baseCost); + if (maxCanDo === 0) { + return `You need ${itemNameFromID(fish.bait)} to fish ${fish.name}!`; + } + + if (maxCanDo < quantity) { + quantity = maxCanDo; + } + } + + // determining fish time and quantities + const { catches1: Qty1, catches2: Qty2, catches3: Qty3, lootAmount1: loot1, lootAmount2: loot2, lootAmount3: loot3, ticksElapsed: tripLength, flakesUsed: flakesToRemove } = determineFishingTime( + quantity, + tripTicks, + powerfish, + spirit_flakes, + fish, + user, + invSlots, + blessingChance, + flakesQuantity, + harpoonBoost + ); + + + let duration = Time.Second * 0.6 * tripLength; + + await addSubTaskToActivityTask({ + fishID: fish.id, + userID: user.id, + channelID: channelID.toString(), + duration: duration, + quantity: quantity, + Qty1: Qty1, + Qty2: Qty2, + Qty3: Qty3, + loot1: loot1, + loot2: loot2, + loot3: loot3, + flakesToRemove: flakesToRemove, + powerfish: powerfish, + spirit_flakes: spirit_flakes, + type: 'Fishing' + }); + + let response = `${user.minionName} is now fishing ${fish.name}, it'll take around ${formatDuration(duration)} to finish.`; + + if (boosts.length > 0) { + response += `\n\n**Boosts:** ${boosts.join(', ')}.`; + } + + return response; + } +}; + diff --git a/fishing.ts b/fishing.ts new file mode 100644 index 0000000000..28b0d1cfbd --- /dev/null +++ b/fishing.ts @@ -0,0 +1,368 @@ +import { Emoji } from '../../constants'; +import itemID from '../../util/itemID'; +import type { Fish } from '../types'; +import { SkillsEnum } from '../types'; + +const fishes: Fish[] = [ + { + name: 'Shrimps/Anchovies', + level: 1, + xp: 10, + id: itemID('Raw shrimps'), + chance1Lvl1: 0.1373, // catch chance for fish 1 at lvl 1 + chance1Lvl99: 1.0000, + + level2: 15, + xp2: 40, + id2: itemID('Raw anchovies'), + chance2Lvl1: 0.0937, + chance2Lvl99: 0.5039, + + petChance: 435_165, + clueScrollChance: 870_330, + lostTicks: 0.05, // percentage of ticks spent moving/dropping, + bankingTime: 30, + ticksPerRoll: 6 + }, + { + name: 'Sardine/Herring', + level: 5, + xp: 20, + id: itemID('Raw sardine'), + chance1Lvl1: 0.1267, + chance1Lvl99: 0.7539, + + level2: 10, + xp2: 30, + id2: itemID('Raw herring'), + chance2Lvl1: 0.1273, + chance2Lvl99: 0.5039, + + bait: itemID('Fishing bait'), + petChance: 528_000, + clueScrollChance: 1_056_000, + lostTicks: 0.05, + bankingTime: 30, + ticksPerRoll: 5 + }, + { + name: 'Karambwanji', + level: 5, + xp: 20, + id: itemID('Raw karambwanji'), + chance1Lvl1: 0.3945, + chance1Lvl99: 0.9805, + + petChance: 443_697, + qpRequired: 15, + clueScrollChance: 443_697, + lostTicks: 0.01, + bankingTime: 0, + ticksPerRoll: 6 + }, + { + name: 'Mackerel/Cod/Bass', + level: 16, + xp: 20, + id: itemID('Raw mackerel'), + chance1Lvl1: 0.0645, + chance1Lvl99: 0.2897, + + level2: 23, + xp2: 45, + id2: itemID('Raw cod'), + chance2Lvl1: 0.0173, + chance2Lvl99: 0.2188, + + level3: 46, + xp3: 100, + id3: itemID('Raw bass'), + bigFish: itemID('Big bass'), + bigFishRate: 1000, + chance3Lvl1: 0.0156, + chance3Lvl99: 0.1602, + + petChance: 382_609, + clueScrollChance: 1_147_827, + lostTicks: 0.05, + bankingTime: 25, + ticksPerRoll: 6 + }, + { + name: 'Trout/Salmon', + level: 20, + xp: 50, + id: itemID('Raw trout'), + chance1Lvl1: 0.0174, + chance1Lvl99: 0.7538, + + level2: 30, + xp2: 70, + id2: itemID('Raw salmon'), + chance2Lvl1: 0.0683, + chance2Lvl99: 0.3789, + + petChance: 461_808, + bait: itemID('Feather'), + clueScrollChance: 923_616, + lostTicks: 0.05, + bankingTime: 30, + ticksPerRoll: 5 + }, + { + name: 'Pike', + level: 25, + xp: 60, + id: itemID('Raw pike'), + chance1Lvl1: 0.0685, + chance1Lvl99: 0.3789, + + petChance: 305_792, + bait: itemID('Fishing bait'), + clueScrollChance: 305_792, + lostTicks: 0.05, + bankingTime: 30, + ticksPerRoll: 5 + }, + { + name: 'Tuna/Swordfish', + alias: ['sword, sf'], + level: 35, + xp: 80, + id: itemID('Raw tuna'), + chance1Lvl1: 0.0326, + chance1Lvl99: 0.2539, + + level2: 50, + xp2: 100, + id2: itemID('Raw swordfish'), + bigFish: itemID('Big swordfish'), + bigFishRate: 2500, + chance2Lvl1: 0.0196, + chance2Lvl99: 0.1914, + + petChance: 128_885, + clueScrollChance: 257_770, + lostTicks: 0.05, + bankingTime: 25, + ticksPerRoll: 6 + }, + { + name: 'Cave eel', + level: 38, + xp: 80, + id: itemID('Raw cave eel'), + chance1Lvl1: 0.1900, + chance1Lvl99: 0.3164, + lostTicks: 0.05, + bankingTime: 40, + ticksPerRoll: 5 + }, + { + name: 'Lobster', + alias: ['lobs'], + level: 40, + xp: 90, + id: itemID('Raw lobster'), + chance1Lvl1: 0.0247, + chance1Lvl99: 0.3750, + petChance: 116_129, + clueScrollChance: 116_129, + lostTicks: 0.05, + bankingTime: 25, + ticksPerRoll: 6 + }, + { + name: 'Monkfish', + alias: ['monk'], + level: 62, + xp: 120, + id: itemID('Raw monkfish'), + chance1Lvl1: 0.1900, + chance1Lvl99: 0.3555, + petChance: 138_583, + qpRequired: 100, + clueScrollChance: 138_583, + lostTicks: 0.10, + bankingTime: 20, + ticksPerRoll: 6 + }, + { + name: 'Karambwan', + alias: ['karam'], + level: 65, + xp: 50, + id: itemID('Raw karambwan'), + chance1Lvl1: 0.0210, + chance1Lvl99: 0.6289, + petChance: 170_874, + bait: itemID('Raw karambwanji'), + clueScrollChance: 170_874, + lostTicks: 0.00, // fishing spots never moves + bankingTime: 25, + ticksPerRoll: 4 + }, + { + name: 'Shark', + alias: ['shark'], + level: 76, + xp: 110, + id: itemID('Raw shark'), + chance1Lvl1: 0.0102, + chance1Lvl99: 0.1602, + petChance: 82_243, + bigFish: itemID('Big shark'), + bigFishRate: 5000, + clueScrollChance: 82_243, + lostTicks: 0.05, + bankingTime: 25, + ticksPerRoll: 6 + }, + { + name: 'Anglerfish', + alias: ['angler'], + level: 82, + xp: 120, + id: itemID('Raw anglerfish'), + chance1Lvl1: 0.0096, + chance1Lvl99: 0.1445, + petChance: 78_649, + bait: itemID('Sandworms'), + qpRequired: 40, + clueScrollChance: 78_649, + lostTicks: 0.05, + bankingTime: 30, + ticksPerRoll: 5 + }, + { + name: 'Minnow', + alias: ['minnows'], + level: 82, + xp: 26.1, + id: itemID('Minnow'), + chance1Lvl1: 0.6666, // no info on catch chance + chance1Lvl99: 0.9259, // handpicked to match wiki rates + petChance: 977_778, + qpRequired: 1, + clueScrollChance: 977_778, + lostTicks: 0.25, + bankingTime: 0, // stackable + ticksPerRoll: 2 + }, + { + name: 'Dark crab', + alias: ['crab', 'dark'], + level: 85, + xp: 130, + id: itemID('Raw dark crab'), + chance1Lvl1: 0.0230, + chance1Lvl99: 0.1602, + petChance: 149_434, + bait: itemID('Dark fishing bait'), + clueScrollChance: 149_434, + lostTicks: 0.05, + bankingTime: 0, + ticksPerRoll: 6 + }, + { + name: 'Barbarian fishing', + alias: ['barb', 'barbarian'], + level: 48, + xp: 50, + id: itemID('Leaping trout'), + chance1Lvl1: 32 / 255, + chance1Lvl99: 192 / 255, + + level2: 58, + xp2: 70, + id2: itemID('Leaping salmon'), + chance2Lvl1: 16 / 255, + chance2Lvl99: 96 / 255, + + level3: 70, + xp3: 80, + id3: itemID('Leaping sturgeon'), + chance3Lvl1: 8 / 255, + chance3Lvl99: 64 / 255, + + petChance: 426_954, + bait: itemID('Feather'), + clueScrollChance: 1_280_862, + lostTicks: 0.05, + bankingTime: 40, + ticksPerRoll: 5 + }, + { + name: 'Infernal eel', + level: 80, + xp: 95, + id: itemID('Infernal eel'), + petChance: 160_000, + bait: itemID('Fishing bait'), + clueScrollChance: 165_000, + chance1Lvl1: 0.1253, + chance1Lvl99: 0.3672, + lostTicks: 0.10, + bankingTime: 0, + ticksPerRoll: 5 + } +]; + +// Types of fish in camdozaal +const camdozaalFishes: Fish[] = [ + { + level: 7, + xp: 8, + id: itemID('Raw guppy'), + name: 'Raw guppy', + petChance: 257_770, + timePerFish: 5.5, + clueScrollChance: 257_770 + }, + { + level: 20, + xp: 16, + id: itemID('Raw cavefish'), + name: 'Raw cavefish', + petChance: 257_770, + timePerFish: 5.5, + clueScrollChance: 257_770 + }, + { + level: 33, + xp: 24, + id: itemID('Raw tetra'), + name: 'Raw tetra', + petChance: 257_770, + timePerFish: 5.5, + clueScrollChance: 257_770 + }, + { + level: 46, + xp: 33, + id: itemID('Raw catfish'), + name: 'Raw catfish', + petChance: 257_770, + timePerFish: 5.5, + clueScrollChance: 257_770 + } +]; + +const anglerItems: { [key: number]: number } = { + [itemID('Angler hat')]: 0.4, + [itemID('Angler top')]: 0.8, + [itemID('Angler waders ')]: 0.6, + [itemID('Angler boots')]: 0.2 +}; + +const Fishing = { + aliases: ['fishing'], + Fishes: fishes, + camdozaalFishes, + id: SkillsEnum.Fishing, + emoji: Emoji.Fishing, + anglerItems, + name: 'Fishing' +}; + +export default Fishing; diff --git a/fishingActivity.ts b/fishingActivity.ts new file mode 100644 index 0000000000..37ee34fd82 --- /dev/null +++ b/fishingActivity.ts @@ -0,0 +1,211 @@ +//import { calcPercentOfNum, percentChance, randInt } from 'e'; +import { calcPercentOfNum } from 'e'; +import { Bank } from 'oldschooljs'; +import { z } from 'zod'; +//import { Time } from 'e'; +import { Emoji, Events } from '../../lib/constants'; +import addSkillingClueToLoot from '../../lib/minions/functions/addSkillingClueToLoot'; +import Fishing from '../../lib/skilling/skills/fishing'; +import { SkillsEnum } from '../../lib/skilling/types'; +import type { FishingActivityTaskOptions } from '../../lib/types/minions'; +import { roll, skillingPetDropRate } from '../../lib/util'; +import { handleTripFinish } from '../../lib/util/handleTripFinish'; +// itemID from '../../lib/util/itemID'; +import { anglerBoostPercent } from '../../mahoji/mahojiSettings'; + +const allFishIDs = Fishing.Fishes.flatMap(fish => [fish.id, fish.id2, fish.id3]); + +export const fishingTask: MinionTask = { + type: 'Fishing', + dataSchema: z.object({ + type: z.literal('Fishing'), + fishID: z.number().refine(fishID => allFishIDs.includes(fishID), { + message: 'Invalid fish ID' + }), + quantity: z.number().min(1) + }), + async run(data: FishingActivityTaskOptions) { + let { fishID, userID, channelID, duration, spirit_flakes, + Qty1, Qty2 = 0, Qty3 = 0, loot1 = 0, loot2 = 0, loot3 = 0, flakesToRemove } = data; + + spirit_flakes = spirit_flakes ?? false; + + const user = await mUserFetch(userID); + const fishLvl = user.skillLevel(SkillsEnum.Fishing); + + const minnowQuantity: { [key: number]: number[] } = { + 99: [10, 14], + 95: [11, 13], + 90: [10, 13], + 85: [10, 11], + 1: [10, 10] + }; + + let baseMinnow = [10, 10]; + for (const [level, quantities] of Object.entries(minnowQuantity).reverse()) { + if (fishLvl >= Number.parseInt(level)) { + baseMinnow = quantities; + break; + } + } + + const baseKarambwanji = 1 + Math.floor(fishLvl / 5); + + let xpReceived = 0; + let agilityXpReceived = 0; + let strengthXpReceived = 0; + + const fish = Fishing.Fishes.find(fish => fish.id === fishID)!; + + // adding xp and loot + + xpReceived += fish.xp * Qty1; + if (Qty2 != 0) xpReceived += fish.xp2! * Qty2; + if (Qty3 != 0) xpReceived += fish.xp3! * Qty3; + + if (fish.name === 'Barbarian fishing') { + agilityXpReceived += 7 * Qty3 + 6 * Qty2 + 5 * Qty1; + strengthXpReceived += 7 * Qty3 + 6 * Qty2 + 5 * Qty1; + } + + + // If they have the entire angler outfit, give an extra 0.5% xp bonus + let bonusXP = 0; + if ( + user.gear.skilling.hasEquipped( + Object.keys(Fishing.anglerItems).map(i => Number.parseInt(i)), + true + ) + ) { + const amountToAdd = Math.floor(xpReceived * (2.5 / 100)); + xpReceived += amountToAdd; + bonusXP += amountToAdd; + } else { + // For each angler item, check if they have it, give its' XP boost if so. + for (const [itemID, bonus] of Object.entries(Fishing.anglerItems)) { + if (user.hasEquipped(Number.parseInt(itemID))) { + const amountToAdd = Math.floor(xpReceived * (bonus / 100)); + xpReceived += amountToAdd; + bonusXP += amountToAdd; + } + } + } + + + let xpRes = await user.addXP({ + skillName: SkillsEnum.Fishing, + amount: xpReceived, + duration + }); + xpRes += + agilityXpReceived > 0 + ? await user.addXP({ + skillName: SkillsEnum.Agility, + amount: agilityXpReceived, + duration + }) + : ''; + xpRes += + strengthXpReceived > 0 + ? await user.addXP({ + skillName: SkillsEnum.Strength, + amount: strengthXpReceived, + duration + }) + : ''; + + + const loot = new Bank(); + loot.add(fish.id3!, loot3); + loot.add(fish.id2!, loot2); + + // handling stackable fish + if (fish.name === 'Minnow') { + let sum = 0; + for (let i = 0; i < loot1; i++) { + sum += Math.floor(Math.random() * (baseMinnow[1] - baseMinnow[0] + 1)) + baseMinnow[0]; + } + loot1 = sum; + } else if (fish.name === 'Karambwanji') { + loot1 *= baseKarambwanji; + } + loot.add(fish!.id, loot1); + + let str = '' + + const totalCatches = Qty1 + Qty2 + Qty3; + str = `${user}, ${user.minionName} finished fishing ${totalCatches} ${fish.name}. ${xpRes}`; + + + const cost = new Bank(); + if (spirit_flakes) { + cost.add('Spirit flakes', flakesToRemove); + } + + if (fish.bait) { + cost.add(fish.bait, totalCatches); + } + + await user.removeItemsFromBank(cost); + + // Add clue scrolls + if (fish.clueScrollChance) { + addSkillingClueToLoot(user, SkillsEnum.Fishing, totalCatches, fish.clueScrollChance, loot); + } + + const xpBonusPercent = anglerBoostPercent(user); + if (xpBonusPercent > 0) { + bonusXP += Math.ceil(calcPercentOfNum(xpBonusPercent, xpReceived)); + } + + if (bonusXP > 0) { + str += `\n\n**Bonus XP:** ${bonusXP.toLocaleString()}`; + } + + // Roll for pet + if (fish.petChance) { + const { petDropRate } = skillingPetDropRate(user, SkillsEnum.Fishing, fish.petChance); + for (let i = 0; i < totalCatches; i++) { + if (roll(petDropRate)) { + loot.add('Heron'); + str += "\nYou have a funny feeling you're being followed..."; + globalClient.emit( + Events.ServerNotification, + `${Emoji.Fishing} **${user.badgedUsername}'s** minion, ${user.minionName}, just received a Heron while fishing ${fish.name} at level ${fishLvl} Fishing!` + ); + } + } + } + + // bigFishQuantity add this + if (fish.bigFishRate && fish.bigFish) { + let bigFishQuantity = 0; + if (fish.name === 'Shark') { + bigFishQuantity = Qty1; + } + if (fish.name === 'Tuna/Swordfish') { + bigFishQuantity = Qty2; + } + if (fish.name === 'Mackerel/Cod/Bass') { + bigFishQuantity = Qty3; + } + for (let i = 0; i < bigFishQuantity!; i++) { + if (roll(fish.bigFishRate)) { + loot.add(fish.bigFish); + } + } + } + + + await transactItems({ + userID: user.id, + collectionLog: true, + itemsToAdd: loot + }); + + str += `\n\nYou received: ${loot}.`; + + handleTripFinish(user, channelID, str, undefined, data, loot); + } +} + diff --git a/minionStatus.ts b/minionStatus.ts new file mode 100644 index 0000000000..0782428c44 --- /dev/null +++ b/minionStatus.ts @@ -0,0 +1,668 @@ +import { toTitleCase } from '@oldschoolgg/toolkit'; +import { increaseNumByPercent, reduceNumByPercent } from 'e'; +import { SkillsEnum } from 'oldschooljs/dist/constants'; + +import { collectables } from '../../mahoji/lib/abstracted_commands/collectCommand'; +import { shades, shadesLogs } from '../../mahoji/lib/abstracted_commands/shadesOfMortonCommand'; +import { ClueTiers } from '../clues/clueTiers'; +import { Emoji } from '../constants'; +import killableMonsters from '../minions/data/killableMonsters'; +import { Planks } from '../minions/data/planks'; +import { quests } from '../minions/data/quests'; +import Agility from '../skilling/skills/agility'; +import Constructables from '../skilling/skills/construction/constructables'; +import Cooking from '../skilling/skills/cooking/cooking'; +import LeapingFish from '../skilling/skills/cooking/leapingFish'; +import Crafting from '../skilling/skills/crafting'; +import Farming from '../skilling/skills/farming'; +import Firemaking from '../skilling/skills/firemaking'; +import Fishing from '../skilling/skills/fishing'; +import Herblore from '../skilling/skills/herblore/herblore'; +import Hunter from '../skilling/skills/hunter/hunter'; +import { Castables } from '../skilling/skills/magic/castables'; +import { Enchantables } from '../skilling/skills/magic/enchantables'; +import Mining from '../skilling/skills/mining'; +import Prayer from '../skilling/skills/prayer'; +import Runecraft from '../skilling/skills/runecraft'; +import Smithing from '../skilling/skills/smithing'; +import { stealables } from '../skilling/skills/thieving/stealables'; +import Woodcutting from '../skilling/skills/woodcutting/woodcutting'; +import type { + ActivityTaskOptionsWithQuantity, + AgilityActivityTaskOptions, + AlchingActivityTaskOptions, + BuryingActivityTaskOptions, + ButlerActivityTaskOptions, + CastingActivityTaskOptions, + ClueActivityTaskOptions, + CollectingOptions, + ColoTaskOptions, + ConstructionActivityTaskOptions, + CookingActivityTaskOptions, + CraftingActivityTaskOptions, + CutLeapingFishActivityTaskOptions, + DarkAltarOptions, + EnchantingActivityTaskOptions, + FarmingActivityTaskOptions, + FightCavesActivityTaskOptions, + FiremakingActivityTaskOptions, + FishingActivityTaskOptions, + FletchingActivityTaskOptions, + GauntletOptions, + GroupMonsterActivityTaskOptions, + HerbloreActivityTaskOptions, + HunterActivityTaskOptions, + InfernoOptions, + KourendFavourActivityTaskOptions, + MinigameActivityTaskOptionsWithNoChanges, + MiningActivityTaskOptions, + MonsterActivityTaskOptions, + MotherlodeMiningActivityTaskOptions, + NexTaskOptions, + NightmareActivityTaskOptions, + OfferingActivityTaskOptions, + PickpocketActivityTaskOptions, + PlunderActivityTaskOptions, + RaidsOptions, + RunecraftActivityTaskOptions, + SawmillActivityTaskOptions, + ScatteringActivityTaskOptions, + SepulchreActivityTaskOptions, + ShadesOfMortonOptions, + SmeltingActivityTaskOptions, + SmithingActivityTaskOptions, + SpecificQuestOptions, + TOAOptions, + TheatreOfBloodTaskOptions, + TiaraRunecraftActivityTaskOptions, + WoodcuttingActivityTaskOptions, + ZalcanoActivityTaskOptions +} from '../types/minions'; +import { formatDuration, itemNameFromID, randomVariation, stringMatches } from '../util'; +import { getActivityOfUser } from './minionIsBusy'; + +export function minionStatus(user: MUser) { + const currentTask = getActivityOfUser(user.id); + const name = user.minionName; + if (!currentTask) { + return `${name} is currently doing nothing.`; + } + + const durationRemaining = currentTask.finishDate - Date.now(); + const formattedDuration = `${formatDuration(durationRemaining)} remaining.`; + + switch (currentTask.type) { + case 'MonsterKilling': { + const data = currentTask as MonsterActivityTaskOptions; + const monster = killableMonsters.find(mon => mon.id === data.monsterID); + + return `${name} is currently killing ${data.quantity}x ${monster?.name}. ${formattedDuration}`; + } + + case 'GroupMonsterKilling': { + const data = currentTask as GroupMonsterActivityTaskOptions; + const monster = killableMonsters.find(mon => mon.id === data.monsterID); + + return `${name} is currently killing ${data.quantity}x ${monster?.name} with a party of ${data.users.length + }. ${formattedDuration}`; + } + + case 'ClueCompletion': { + const data = currentTask as ClueActivityTaskOptions; + + const clueTier = ClueTiers.find(tier => tier.id === data.clueID); + + return `${name} is currently completing ${data.quantity}x ${clueTier?.name} clues. ${formattedDuration}`; + } + + case 'Crafting': { + const data = currentTask as CraftingActivityTaskOptions; + const craftable = Crafting.Craftables.find(item => item.id === data.craftableID); + + return `${name} is currently crafting ${data.quantity}x ${craftable?.name}. ${formattedDuration} Your ${Emoji.Crafting + } Crafting level is ${user.skillLevel(SkillsEnum.Crafting)}`; + } + + case 'Agility': { + const data = currentTask as AgilityActivityTaskOptions; + + const course = Agility.Courses.find(course => course.name === data.courseID); + + return `${name} is currently running ${data.quantity}x ${course?.name} laps. ${formattedDuration} Your ${Emoji.Agility + } Agility level is ${user.skillLevel(SkillsEnum.Agility)}`; + } + + case 'Cooking': { + const data = currentTask as CookingActivityTaskOptions; + + const cookable = Cooking.Cookables.find(cookable => cookable.id === data.cookableID); + + return `${name} is currently cooking ${data.quantity}x ${cookable?.name}. ${formattedDuration} Your ${Emoji.Cooking + } Cooking level is ${user.skillLevel(SkillsEnum.Cooking)}`; + } + + case 'Fishing': { + const data = currentTask as FishingActivityTaskOptions; + + const fish = Fishing.Fishes.find(fish => fish.id === data.fishID); + + return `${name} is currently fishing ${fish?.name}. ${formattedDuration} Your ${Emoji.Fishing + } Fishing level is ${user.skillLevel(SkillsEnum.Fishing)}`; + } + + case 'Mining': { + const data = currentTask as MiningActivityTaskOptions; + + const ore = Mining.Ores.find(ore => ore.id === data.oreID); + + return `${name} is currently mining ${ore?.name}. ${data.fakeDurationMax === data.fakeDurationMin + ? formattedDuration + : `approximately ${formatDuration( + randomVariation(reduceNumByPercent(durationRemaining, 25), 20) + )} **to** ${formatDuration( + randomVariation(increaseNumByPercent(durationRemaining, 25), 20) + )} remaining.` + } Your ${Emoji.Mining} Mining level is ${user.skillLevel(SkillsEnum.Mining)}`; + } + + case 'MotherlodeMining': { + const data = currentTask as MotherlodeMiningActivityTaskOptions; + + return `${name} is currently mining at the Motherlode Mine. ${data.fakeDurationMax === data.fakeDurationMin + ? formattedDuration + : `approximately ${formatDuration( + randomVariation(reduceNumByPercent(durationRemaining, 25), 20) + )} **to** ${formatDuration( + randomVariation(increaseNumByPercent(durationRemaining, 25), 20) + )} remaining.` + } Your ${Emoji.Mining} Mining level is ${user.skillLevel(SkillsEnum.Mining)}`; + } + + case 'Smelting': { + const data = currentTask as SmeltingActivityTaskOptions; + + const bar = Smithing.Bars.find(bar => bar.id === data.barID); + + return `${name} is currently smelting ${data.quantity}x ${bar?.name}. ${formattedDuration} Your ${Emoji.Smithing + } Smithing level is ${user.skillLevel(SkillsEnum.Smithing)}`; + } + + case 'Smithing': { + const data = currentTask as SmithingActivityTaskOptions; + + const SmithableItem = Smithing.SmithableItems.find(item => item.id === data.smithedBarID); + + return `${name} is currently smithing ${data.quantity}x ${SmithableItem?.name}. ${formattedDuration} Your ${Emoji.Smithing + } Smithing level is ${user.skillLevel(SkillsEnum.Smithing)}`; + } + + case 'Offering': { + const data = currentTask as OfferingActivityTaskOptions; + + const bones = Prayer.Bones.find(bones => bones.inputId === data.boneID); + + return `${name} is currently offering ${data.quantity}x ${bones?.name}. ${formattedDuration} Your ${Emoji.Prayer + } Prayer level is ${user.skillLevel(SkillsEnum.Prayer)}`; + } + + case 'Burying': { + const data = currentTask as BuryingActivityTaskOptions; + + const bones = Prayer.Bones.find(bones => bones.inputId === data.boneID); + + return `${name} is currently burying ${data.quantity}x ${bones?.name}. ${formattedDuration} Your ${Emoji.Prayer + } Prayer level is ${user.skillLevel(SkillsEnum.Prayer)}`; + } + + case 'Scattering': { + const data = currentTask as ScatteringActivityTaskOptions; + + const ashes = Prayer.Ashes.find(ashes => ashes.inputId === data.ashID); + + return `${name} is currently scattering ${data.quantity}x ${ashes?.name}. ${formattedDuration} Your ${Emoji.Prayer + } Prayer level is ${user.skillLevel(SkillsEnum.Prayer)}`; + } + + case 'Firemaking': { + const data = currentTask as FiremakingActivityTaskOptions; + + const burn = Firemaking.Burnables.find(burn => burn.inputLogs === data.burnableID); + + return `${name} is currently lighting ${data.quantity}x ${burn?.name}. ${formattedDuration} Your ${Emoji.Firemaking + } Firemaking level is ${user.skillLevel(SkillsEnum.Firemaking)}`; + } + + case 'Questing': { + return `${name} is currently Questing. ${formattedDuration} Your current Quest Point count is: ${user.QP}.`; + } + + case 'Woodcutting': { + const data = currentTask as WoodcuttingActivityTaskOptions; + + const log = Woodcutting.Logs.find(log => log.id === data.logID); + + return `${name} is currently chopping ${log?.name}. ${data.fakeDurationMax === data.fakeDurationMin + ? formattedDuration + : `approximately ${formatDuration( + randomVariation(reduceNumByPercent(durationRemaining, 25), 20) + )} **to** ${formatDuration( + randomVariation(increaseNumByPercent(durationRemaining, 25), 20) + )} remaining.` + } Your ${Emoji.Woodcutting} Woodcutting level is ${user.skillLevel(SkillsEnum.Woodcutting)}`; + } + case 'Runecraft': { + const data = currentTask as RunecraftActivityTaskOptions; + + const rune = Runecraft.Runes.find(_rune => _rune.id === data.runeID); + + return `${name} is currently turning ${data.essenceQuantity}x Essence into ${rune?.name + }. ${formattedDuration} Your ${Emoji.Runecraft} Runecraft level is ${user.skillLevel( + SkillsEnum.Runecraft + )}`; + } + + case 'TiaraRunecraft': { + const data = currentTask as TiaraRunecraftActivityTaskOptions; + const tiara = Runecraft.Tiaras.find(_tiara => _tiara.id === data.tiaraID); + + return `${name} is currently crafting ${data.tiaraQuantity} ${tiara?.name}. ${formattedDuration} Your ${Emoji.Runecraft + } Runecraft level is ${user.skillLevel(SkillsEnum.Runecraft)}`; + } + + case 'FightCaves': { + const data = currentTask as FightCavesActivityTaskOptions; + const durationRemaining = data.finishDate - data.duration + data.fakeDuration - Date.now(); + return `${name} is currently attempting the ${Emoji.AnimatedFireCape} **Fight caves** ${Emoji.TzRekJad + }. If they're successful and don't die, the trip should take ${formatDuration(durationRemaining)}.`; + } + case 'TitheFarm': { + return `${name} is currently farming at the **Tithe Farm**. ${formattedDuration}`; + } + + case 'Fletching': { + const data = currentTask as FletchingActivityTaskOptions; + + return `${name} is currently fletching ${data.quantity}x ${data.fletchableName + }. ${formattedDuration} Your ${Emoji.Fletching} Fletching level is ${user.skillLevel( + SkillsEnum.Fletching + )}`; + } + case 'Herblore': { + const data = currentTask as HerbloreActivityTaskOptions; + const mixable = Herblore.Mixables.find(i => i.item.id === data.mixableID); + + return `${name} is currently mixing ${data.quantity}x ${mixable?.item.name}. ${formattedDuration} Your ${Emoji.Herblore + } Herblore level is ${user.skillLevel(SkillsEnum.Herblore)}`; + } + case 'CutLeapingFish': { + const data = currentTask as CutLeapingFishActivityTaskOptions; + const barbarianFish = LeapingFish.find(item => item.item.id === data.id); + + return `${name} is currently cutting ${data.quantity}x ${barbarianFish?.item.name + }. ${formattedDuration} Your ${Emoji.Cooking} Cooking level is ${user.skillLevel(SkillsEnum.Cooking)}`; + } + case 'Wintertodt': { + const data = currentTask as ActivityTaskOptionsWithQuantity; + return `${name} is currently fighting Wintertodt ${data.quantity}x times. ${formattedDuration}`; + } + case 'Tempoross': { + return `${name} is currently fighting Tempoross. ${formattedDuration}`; + } + + case 'Alching': { + const data = currentTask as AlchingActivityTaskOptions; + + return `${name} is currently alching ${data.quantity}x ${itemNameFromID( + data.itemID + )}. ${formattedDuration}`; + } + + case 'Farming': { + const data = currentTask as FarmingActivityTaskOptions; + + const plants = Farming.Plants.find(plants => plants.name === data.plantsName); + + return `${name} is currently farming ${data.quantity}x ${plants?.name}. ${formattedDuration} Your ${Emoji.Farming + } Farming level is ${user.skillLevel(SkillsEnum.Farming)}.`; + } + + case 'Sawmill': { + const data = currentTask as SawmillActivityTaskOptions; + const plank = Planks.find(_plank => _plank.outputItem === data.plankID)!; + return `${name} is currently creating ${data.plankQuantity}x ${itemNameFromID( + plank.outputItem + )}s. ${formattedDuration}`; + } + + case 'Nightmare': { + const data = currentTask as NightmareActivityTaskOptions; + return `${name} is currently killing The Nightmare ${data.method === 'solo' ? 'solo' : 'in a team' + }. ${formattedDuration}`; + } + + case 'AnimatedArmour': { + return `${name} is currently fighting animated armour in the Warriors' Guild. ${formattedDuration}`; + } + + case 'Cyclops': { + return `${name} is currently fighting cyclopes in the Warriors' Guild. ${formattedDuration}`; + } + + case 'CamdozaalFishing': { + return `${name} is currently Fishing in the Ruins of Camdozaal. ${formattedDuration}`; + } + + case 'CamdozaalMining': { + return `${name} is currently Mining in the Ruins of Camdozaal. ${formattedDuration}`; + } + + case 'CamdozaalSmithing': { + return `${name} is currently Smithing in the Ruins of Camdozaal. ${formattedDuration}`; + } + + case 'Sepulchre': { + const data = currentTask as SepulchreActivityTaskOptions; + + return `${name} is currently doing ${data.quantity}x laps of the Hallowed Sepulchre. ${formattedDuration}`; + } + + case 'Plunder': { + const data = currentTask as PlunderActivityTaskOptions; + + return `${name} is currently doing Pyramid Plunder x ${data.quantity}x times. ${formattedDuration}`; + } + + case 'FishingTrawler': { + const data = currentTask as ActivityTaskOptionsWithQuantity; + return `${name} is currently aboard the Fishing Trawler, doing ${data.quantity}x trips. ${formattedDuration}`; + } + + case 'Zalcano': { + const data = currentTask as ZalcanoActivityTaskOptions; + return `${name} is currently killing Zalcano ${data.quantity}x times. ${formattedDuration}`; + } + + case 'Pickpocket': { + const data = currentTask as PickpocketActivityTaskOptions; + const obj = stealables.find(_obj => _obj.id === data.monsterID); + return `${name} is currently ${obj?.type === 'pickpockable' ? 'pickpocketing' : 'stealing'} from ${obj?.name + } ${data.quantity}x times. ${formattedDuration}`; + } + + case 'BarbarianAssault': { + const data = currentTask as MinigameActivityTaskOptionsWithNoChanges; + return `${name} is currently doing ${data.quantity} waves of Barbarian Assault. ${formattedDuration}`; + } + + case 'AgilityArena': { + return `${name} is currently doing the Brimhaven Agility Arena. ${formattedDuration}`; + } + + case 'ChampionsChallenge': { + return `${name} is currently doing the **Champion's Challenge**. ${formattedDuration}`; + } + + case 'Hunter': { + const data = currentTask as HunterActivityTaskOptions; + + const creature = Hunter.Creatures.find(creature => + creature.aliases.some( + alias => + stringMatches(alias, data.creatureName) || stringMatches(alias.split(' ')[0], data.creatureName) + ) + ); + const crystalImpling = creature?.name === 'Crystal impling'; + return `${name} is currently hunting ${crystalImpling ? creature?.name : `${data.quantity}x ${creature?.name}` + }. ${formattedDuration}`; + } + + case 'Birdhouse': { + return `${name} is currently doing a bird house run. ${formattedDuration}`; + } + + case 'AerialFishing': { + return `${name} is currently aerial fishing. ${formattedDuration}`; + } + + case 'DriftNet': { + return `${name} is currently drift net fishing. ${formattedDuration}`; + } + + case 'Construction': { + const data = currentTask as ConstructionActivityTaskOptions; + const pohObject = Constructables.find(i => i.id === data.objectID); + if (!pohObject) throw new Error(`No POH object found with ID ${data.objectID}.`); + return `${name} is currently building ${data.quantity}x ${pohObject.name}. ${formattedDuration}`; + } + + case 'Butler': { + const data = currentTask as ButlerActivityTaskOptions; + const plank = Planks.find(_plank => _plank.outputItem === data.plankID)!; + return `${name} is currently creating ${data.plankQuantity}x ${itemNameFromID( + plank.outputItem + )}s. ${formattedDuration}`; + } + + case 'MahoganyHomes': { + return `${name} is currently doing Mahogany Homes. ${formattedDuration}`; + } + + case 'Enchanting': { + const data = currentTask as EnchantingActivityTaskOptions; + const enchantable = Enchantables.find(i => i.id === data.itemID); + return `${name} is currently enchanting ${data.quantity}x ${enchantable?.name}. ${formattedDuration}`; + } + + case 'Casting': { + const data = currentTask as CastingActivityTaskOptions; + const spell = Castables.find(i => i.id === data.spellID); + return `${name} is currently casting ${data.quantity}x ${spell?.name}. ${formattedDuration}`; + } + + case 'GloryCharging': { + const data = currentTask as ActivityTaskOptionsWithQuantity; + return `${name} is currently charging ${data.quantity}x inventories of glories at the Fountain of Rune. ${formattedDuration}`; + } + + case 'WealthCharging': { + const data = currentTask as ActivityTaskOptionsWithQuantity; + return `${name} is currently charging ${data.quantity}x inventories of rings of wealth at the Fountain of Rune. ${formattedDuration}`; + } + + case 'GnomeRestaurant': { + return `${name} is currently doing Gnome Restaurant deliveries. ${formattedDuration}`; + } + + case 'SoulWars': { + const data = currentTask as MinigameActivityTaskOptionsWithNoChanges; + return `${name} is currently doing ${data.quantity}x games of Soul Wars. ${formattedDuration}`; + } + + case 'RoguesDenMaze': { + return `${name} is currently attempting the Rogues' Den maze. ${formattedDuration}`; + } + + case 'Gauntlet': { + const data = currentTask as GauntletOptions; + return `${name} is currently doing ${data.quantity}x ${data.corrupted ? 'Corrupted' : 'Normal' + } Gauntlet. ${formattedDuration}`; + } + + case 'CastleWars': { + const data = currentTask as MinigameActivityTaskOptionsWithNoChanges; + return `${name} is currently doing ${data.quantity}x Castle Wars games. ${formattedDuration}`; + } + + case 'MageArena': { + return `${name} is currently doing the Mage Arena. ${formattedDuration}`; + } + + case 'Raids': { + const data = currentTask as RaidsOptions; + return `${name} is currently doing the Chambers of Xeric${data.challengeMode ? ' in Challenge Mode' : '' + }, ${data.users.length === 1 ? 'as a solo.' : `with a team of ${data.users.length} minions.` + } ${formattedDuration}`; + } + + case 'Collecting': { + const data = currentTask as CollectingOptions; + const collectable = collectables.find(c => c.item.id === data.collectableID)!; + return `${name} is currently collecting ${data.quantity * collectable.quantity}x ${collectable.item.name + }. ${formattedDuration}`; + } + + case 'MageTrainingArena': { + return `${name} is currently training at the Mage Training Arena. ${formattedDuration}`; + } + + case 'MageArena2': { + return `${name} is currently attempting the Mage Arena II. ${formattedDuration}`; + } + + case 'BigChompyBirdHunting': { + return `${name} is currently hunting Chompy Birds! ${formattedDuration}`; + } + + case 'DarkAltar': { + const data = currentTask as DarkAltarOptions; + return `${name} is currently runecrafting ${toTitleCase( + data.rune + )} runes at the Dark Altar. ${formattedDuration}`; + } + case 'Trekking': { + return `${name} is currently Temple Trekking. ${formattedDuration}`; + } + case 'PestControl': { + const data = currentTask as MinigameActivityTaskOptionsWithNoChanges; + return `${name} is currently doing ${data.quantity} games of Pest Control. ${formattedDuration}`; + } + case 'VolcanicMine': { + const data = currentTask as ActivityTaskOptionsWithQuantity; + return `${name} is currently doing ${data.quantity} games of Volcanic Mine. ${formattedDuration}`; + } + case 'TearsOfGuthix': { + return `${name} is currently doing Tears Of Guthix. ${formattedDuration}`; + } + case 'KourendFavour': { + const data = currentTask as KourendFavourActivityTaskOptions; + return `${name} is currently doing ${data.favour} Favour tasks. ${formattedDuration}`; + } + case 'Inferno': { + const data = currentTask as InfernoOptions; + const durationRemaining = data.finishDate - data.duration + data.fakeDuration - Date.now(); + return `${name} is currently attempting the Inferno, if they're successful and don't die, the trip should take ${formatDuration( + durationRemaining + )}.`; + } + case 'TheatreOfBlood': { + const data = currentTask as TheatreOfBloodTaskOptions; + const durationRemaining = data.finishDate - data.duration + data.fakeDuration - Date.now(); + + return `${name} is currently attempting the Theatre of Blood, if your team is successful and doesn't die, the trip should take ${formatDuration( + durationRemaining + )}.`; + } + case 'LastManStanding': { + const data = currentTask as MinigameActivityTaskOptionsWithNoChanges; + + return `${name} is currently doing ${data.quantity + } Last Man Standing matches, the trip should take ${formatDuration(durationRemaining)}.`; + } + case 'BirthdayEvent': { + return `${name} is currently doing the Birthday Event! The trip should take ${formatDuration( + durationRemaining + )}.`; + } + case 'TokkulShop': { + return `${name} is currently shopping at Tzhaar stores. The trip should take ${formatDuration( + durationRemaining + )}.`; + } + case 'Nex': { + const data = currentTask as NexTaskOptions; + const durationRemaining = data.finishDate - data.duration + data.fakeDuration - Date.now(); + return `${name} is currently killing Nex ${data.quantity} times with a team of ${data.users.length + }. The trip should take ${formatDuration(durationRemaining)}.`; + } + case 'TroubleBrewing': { + const data = currentTask as MinigameActivityTaskOptionsWithNoChanges; + return `${name} is currently doing ${data.quantity + }x games of Trouble Brewing. The trip should take ${formatDuration(durationRemaining)}.`; + } + case 'PuroPuro': { + return `${name} is currently hunting in Puro-Puro. The trip should take ${formatDuration( + durationRemaining + )}.`; + } + case 'ShootingStars': { + return `${name} is currently mining a Crashed Star. The trip should take ${formatDuration( + durationRemaining + )}.`; + } + case 'GiantsFoundry': { + const data = currentTask as MinigameActivityTaskOptionsWithNoChanges; + return `${name} is currently creating ${data.quantity + }x giant weapons for Kovac in the Giants' Foundry minigame. The trip should take ${formatDuration( + durationRemaining + )}.`; + } + case 'GuardiansOfTheRift': { + return `${name} is currently helping the Great Guardian to close the rift. The trip should take ${formatDuration( + durationRemaining + )}.`; + } + case 'NightmareZone': { + return `${name} is currently killing Monsters in the Nightmare Zone. The trip should take ${formatDuration( + durationRemaining + )}.`; + } + case 'ShadesOfMorton': { + const data = currentTask as ShadesOfMortonOptions; + const log = shadesLogs.find(i => i.normalLog.id === data.logID)!; + const shade = shades.find(i => i.shadeName === data.shadeID)!; + return `${name} is currently doing ${data.quantity} trips of Shades of Mort'ton, cremating ${shade.shadeName + } remains with ${log.oiledLog.name}! The trip should take ${formatDuration(durationRemaining)}.`; + } + case 'TombsOfAmascut': { + const data = currentTask as TOAOptions; + const durationRemaining = data.finishDate - data.duration + data.fakeDuration - Date.now(); + + return `${name} is currently attempting the Tombs of Amascut, if your team is successful and doesn't die, the trip should take ${formatDuration( + durationRemaining + )}.`; + } + case 'UnderwaterAgilityThieving': { + return `${name} is currently doing Underwater Agility and Thieving. ${formattedDuration}`; + } + case 'StrongholdOfSecurity': { + return `${name} is currently doing the Stronghold of Security! The trip should take ${formatDuration( + durationRemaining + )}.`; + } + case 'CombatRing': { + return `${name} is currently fighting in the Combat Ring! The trip should take ${formatDuration( + durationRemaining + )}.`; + } + case 'SpecificQuest': { + const data = currentTask as SpecificQuestOptions; + return `${name} is currently doing the ${quests.find(i => i.id === data.questID)?.name + }! The trip should take ${formatDuration(durationRemaining)}.`; + } + case 'Colosseum': { + const data = currentTask as ColoTaskOptions; + const durationRemaining = data.finishDate - data.duration + data.fakeDuration - Date.now(); + + return `${name} is currently attempting the Colosseum, if they are successful, the trip should take ${formatDuration( + durationRemaining + )}.`; + } + case 'HalloweenEvent': { + return `${name} is doing the Halloween event! The trip should take ${formatDuration(durationRemaining)}.`; + } + case 'Easter': + case 'BlastFurnace': { + throw new Error('Removed'); + } + } +} diff --git a/minions.ts b/minions.ts new file mode 100644 index 0000000000..edeb891466 --- /dev/null +++ b/minions.ts @@ -0,0 +1,622 @@ +import type { CropUpgradeType } from '@prisma/client'; + +import type { ItemBank } from '.'; +import type { NMZStrategy, TwitcherGloves, UnderwaterAgilityThievingTrainingSkill } from '../constants'; +import type { IPatchData } from '../minions/farming/types'; +import type { MinigameName } from '../settings/minigames'; +import type { RaidLevel } from '../simulation/toa'; +import type { Peak } from '../tickers'; +import type { BirdhouseData } from './../skilling/skills/hunter/defaultBirdHouseTrap'; + +export interface ActivityTaskOptions { + userID: string; + duration: number; + id: number; + finishDate: number; + channelID: string; +} + +export interface ActivityTaskOptionsWithNoChanges extends ActivityTaskOptions { + type: + | 'Questing' + | 'Wintertodt' + | 'Cyclops' + | 'GloryCharging' + | 'WealthCharging' + | 'BarbarianAssault' + | 'AgilityArena' + | 'ChampionsChallenge' + | 'AerialFishing' + | 'DriftNet' + | 'SoulWars' + | 'RoguesDenMaze' + | 'CastleWars' + | 'MageArena' + | 'MageTrainingArena' + | 'BlastFurnace' + | 'MageArena2' + | 'BigChompyBirdHunting' + | 'PestControl' + | 'VolcanicMine' + | 'TearsOfGuthix' + | 'LastManStanding' + | 'BirthdayEvent' + | 'TroubleBrewing' + | 'Easter' + | 'ShootingStars' + | 'HalloweenEvent' + | 'StrongholdOfSecurity' + | 'CombatRing'; +} + +export interface ActivityTaskOptionsWithQuantity extends ActivityTaskOptions { + type: + | 'VolcanicMine' + | 'Cyclops' + | 'ShootingStars' + | 'DriftNet' + | 'WealthCharging' + | 'GloryCharging' + | 'AerialFishing' + | 'FishingTrawler' + | 'CamdozaalFishing' + | 'CamdozaalMining' + | 'CamdozaalSmithing'; + quantity: number; + // iQty is 'input quantity.' This is the number specified at command time, so we can accurately repeat such trips. + iQty?: number; +} + +export interface ShootingStarsOptions extends ActivityTaskOptions { + type: 'ShootingStars'; + size: number; + usersWith: number; + totalXp: number; + lootItems: ItemBank; +} +interface ActivityTaskOptionsWithUsers extends ActivityTaskOptions { + users: string[]; +} + +export interface RunecraftActivityTaskOptions extends ActivityTaskOptions { + type: 'Runecraft'; + runeID: number; + essenceQuantity: number; + imbueCasts: number; + useStaminas?: boolean; + daeyaltEssence?: boolean; +} + +export interface TiaraRunecraftActivityTaskOptions extends ActivityTaskOptions { + type: 'TiaraRunecraft'; + tiaraID: number; + tiaraQuantity: number; +} + +export interface DarkAltarOptions extends ActivityTaskOptions { + type: 'DarkAltar'; + quantity: number; + hasElite: boolean; + rune: 'blood' | 'soul'; +} + +export interface AgilityActivityTaskOptions extends ActivityTaskOptions { + type: 'Agility'; + courseID: string; + quantity: number; + alch: { + itemID: number; + quantity: number; + } | null; +} + +export interface CookingActivityTaskOptions extends ActivityTaskOptions { + type: 'Cooking'; + cookableID: number; + quantity: number; +} + +export interface ConstructionActivityTaskOptions extends ActivityTaskOptions { + type: 'Construction'; + objectID: number; + quantity: number; +} + +export interface MonsterActivityTaskOptions extends ActivityTaskOptions { + type: 'MonsterKilling'; + monsterID: number; + quantity: number; + iQty?: number; + usingCannon?: boolean; + cannonMulti?: boolean; + chinning?: boolean; + burstOrBarrage?: number; + died?: boolean; + pkEncounters?: number; + hasWildySupplies?: boolean; + isInWilderness?: boolean; +} + +export interface ClueActivityTaskOptions extends ActivityTaskOptions { + type: 'ClueCompletion'; + + clueID: number; + quantity: number; + implingID?: number; + implingClues?: number; +} + +export interface FishingActivityTaskOptions extends ActivityTaskOptions { + type: 'Fishing'; + fishID: number; + quantity?: number; + Qty1: number; + Qty2?: number; + Qty3?: number; + loot1?: number; + loot2?: number; + loot3?: number; + flakesToRemove?: number; + powerfish?: boolean; + spirit_flakes?: boolean; +} + + +export interface MiningActivityTaskOptions extends ActivityTaskOptions { + type: 'Mining'; + fakeDurationMax: number; + fakeDurationMin: number; + oreID: number; + quantity: number; + powermine: boolean; + iQty?: number; +} + +export interface MotherlodeMiningActivityTaskOptions extends ActivityTaskOptions { + type: 'MotherlodeMining'; + fakeDurationMax: number; + fakeDurationMin: number; + quantity: number; + iQty?: number; +} + +export interface SmeltingActivityTaskOptions extends ActivityTaskOptions { + type: 'Smelting'; + barID: number; + quantity: number; + blastf: boolean; +} + +export interface SmithingActivityTaskOptions extends ActivityTaskOptions { + type: 'Smithing'; + smithedBarID: number; + quantity: number; +} + +export interface FiremakingActivityTaskOptions extends ActivityTaskOptions { + type: 'Firemaking'; + burnableID: number; + quantity: number; +} + +export interface WoodcuttingActivityTaskOptions extends ActivityTaskOptions { + type: 'Woodcutting'; + fakeDurationMax: number; + fakeDurationMin: number; + powerchopping: boolean; + forestry?: boolean; + twitchers?: TwitcherGloves; + logID: number; + quantity: number; + iQty?: number; +} + +export interface CraftingActivityTaskOptions extends ActivityTaskOptions { + type: 'Crafting'; + craftableID: number; + quantity: number; +} + +export interface FletchingActivityTaskOptions extends ActivityTaskOptions { + type: 'Fletching'; + fletchableName: string; + quantity: number; +} + +export interface EnchantingActivityTaskOptions extends ActivityTaskOptions { + type: 'Enchanting'; + itemID: number; + quantity: number; +} + +export interface CastingActivityTaskOptions extends ActivityTaskOptions { + type: 'Casting'; + spellID: number; + quantity: number; +} +export interface PickpocketActivityTaskOptions extends ActivityTaskOptions { + type: 'Pickpocket'; + monsterID: number; + quantity: number; + xpReceived: number; + successfulQuantity: number; + damageTaken: number; +} + +export interface BuryingActivityTaskOptions extends ActivityTaskOptions { + type: 'Burying'; + boneID: number; + quantity: number; +} + +export interface ScatteringActivityTaskOptions extends ActivityTaskOptions { + type: 'Scattering'; + ashID: number; + quantity: number; +} + +export interface OfferingActivityTaskOptions extends ActivityTaskOptions { + type: 'Offering'; + boneID: number; + quantity: number; +} + +export interface AnimatedArmourActivityTaskOptions extends ActivityTaskOptions { + type: 'AnimatedArmour'; + armourID: string; + quantity: number; +} + +export interface HerbloreActivityTaskOptions extends ActivityTaskOptions { + type: 'Herblore'; + mixableID: number; + quantity: number; + zahur: boolean; + wesley: boolean; +} + +export interface CutLeapingFishActivityTaskOptions extends ActivityTaskOptions { + type: 'CutLeapingFish'; + fishID: number; + quantity: number; +} + +export interface HunterActivityTaskOptions extends ActivityTaskOptions { + type: 'Hunter'; + creatureName: string; + quantity: number; + usingHuntPotion: boolean; + wildyPeak: Peak | null; + usingStaminaPotion: boolean; +} + +export interface AlchingActivityTaskOptions extends ActivityTaskOptions { + type: 'Alching'; + itemID: number; + quantity: number; + alchValue: number; +} + +export interface FightCavesActivityTaskOptions extends ActivityTaskOptions { + type: 'FightCaves'; + jadDeathChance: number; + preJadDeathChance: number; + preJadDeathTime: number | null; + fakeDuration: number; + quantity: number; +} +export interface InfernoOptions extends ActivityTaskOptions { + type: 'Inferno'; + zukDeathChance: number; + preZukDeathChance: number; + deathTime: number | null; + fakeDuration: number; + diedZuk: boolean; + diedPreZuk: boolean; + cost: ItemBank; +} + +export interface FarmingActivityTaskOptions extends ActivityTaskOptions { + type: 'Farming'; + pid?: number; + plantsName: string | null; + quantity: number; + upgradeType: CropUpgradeType | null; + payment?: boolean; + patchType: IPatchData; + planting: boolean; + currentDate: number; + autoFarmed: boolean; +} + +export interface BirdhouseActivityTaskOptions extends ActivityTaskOptions { + type: 'Birdhouse'; + birdhouseName: string | null; + placing: boolean; + gotCraft: boolean; + birdhouseData: BirdhouseData; + currentDate: number; +} + +interface MinigameActivityTaskOptions extends ActivityTaskOptions { + minigameID: MinigameName; + quantity: number; +} + +export interface MinigameActivityTaskOptionsWithNoChanges extends MinigameActivityTaskOptions { + type: + | 'Wintertodt' + | 'TroubleBrewing' + | 'TearsOfGuthix' + | 'SoulWars' + | 'RoguesDenMaze' + | 'MageTrainingArena' + | 'LastManStanding' + | 'BigChompyBirdHunting' + | 'FishingTrawler' + | 'PestControl' + | 'BarbarianAssault' + | 'ChampionsChallenge' + | 'CastleWars' + | 'AgilityArena' + | 'GiantsFoundry'; +} + +export interface MahoganyHomesActivityTaskOptions extends MinigameActivityTaskOptions { + type: 'MahoganyHomes'; + xp: number; + quantity: number; + points: number; + tier: number; +} + +export interface NightmareActivityTaskOptions extends ActivityTaskOptions { + type: 'Nightmare'; + method: 'solo' | 'mass'; + quantity: number; + isPhosani?: boolean; +} + +export interface TemporossActivityTaskOptions extends MinigameActivityTaskOptions { + type: 'Tempoross'; + quantity: number; + rewardBoost: number; +} + +export interface TitheFarmActivityTaskOptions extends MinigameActivityTaskOptions { + type: 'TitheFarm'; +} + +export interface SepulchreActivityTaskOptions extends MinigameActivityTaskOptions { + type: 'Sepulchre'; + floors: number[]; +} + +export interface PlunderActivityTaskOptions extends MinigameActivityTaskOptions { + type: 'Plunder'; + rooms: number[]; +} + +export interface ZalcanoActivityTaskOptions extends ActivityTaskOptions { + type: 'Zalcano'; + isMVP: boolean; + performance: number; + quantity: number; +} + +export interface TempleTrekkingActivityTaskOptions extends MinigameActivityTaskOptions { + type: 'Trekking'; + difficulty: string; +} + +export interface SawmillActivityTaskOptions extends ActivityTaskOptions { + type: 'Sawmill'; + plankID: number; + plankQuantity: number; +} + +export interface ButlerActivityTaskOptions extends ActivityTaskOptions { + type: 'Butler'; + plankID: number; + plankQuantity: number; +} + +export interface GnomeRestaurantActivityTaskOptions extends MinigameActivityTaskOptions { + type: 'GnomeRestaurant'; + gloriesRemoved: number; +} + +export interface GauntletOptions extends ActivityTaskOptions { + type: 'Gauntlet'; + corrupted: boolean; + quantity: number; +} + +export interface GroupMonsterActivityTaskOptions extends Omit { + type: 'GroupMonsterKilling'; + leader: string; + users: string[]; +} + +export interface RaidsOptions extends ActivityTaskOptionsWithUsers { + type: 'Raids'; + leader: string; + users: string[]; + challengeMode: boolean; + quantity?: number; +} + +export interface TheatreOfBloodTaskOptions extends ActivityTaskOptionsWithUsers { + type: 'TheatreOfBlood'; + leader: string; + users: string[]; + hardMode: boolean; + fakeDuration: number; + wipedRooms: (null | number)[]; + deaths: number[][][]; + quantity: number; + solo?: boolean; +} + +export interface ColoTaskOptions extends ActivityTaskOptions { + type: 'Colosseum'; + fakeDuration: number; + diedAt?: number; + loot?: ItemBank; + maxGlory: number; +} + +type UserID = string; +type Points = number; +type RoomIDsDiedAt = number[]; + +type TOAUser = [UserID, Points[], RoomIDsDiedAt[]]; +export interface TOAOptions extends ActivityTaskOptionsWithUsers { + type: 'TombsOfAmascut'; + leader: string; + detailedUsers: TOAUser[] | [UserID, Points, RoomIDsDiedAt][][]; + raidLevel: RaidLevel; + fakeDuration: number; + wipedRoom: null | number | (number | null)[]; + quantity: number; +} + +export interface NexTaskOptions extends ActivityTaskOptionsWithUsers { + type: 'Nex'; + quantity: number; + leader: string; + userDetails: [string, number, number[]][]; + fakeDuration: number; + wipedKill: number | null; +} + +export interface CollectingOptions extends ActivityTaskOptions { + type: 'Collecting'; + collectableID: number; + quantity: number; + noStaminas?: boolean; +} + +export interface KourendFavourActivityTaskOptions extends ActivityTaskOptions { + type: 'KourendFavour'; + favour: string; + quantity: number; +} + +export interface TokkulShopOptions extends ActivityTaskOptions { + type: 'TokkulShop'; + itemID: number; + quantity: number; +} + +export interface UnderwaterAgilityThievingTaskOptions extends ActivityTaskOptions { + type: 'UnderwaterAgilityThieving'; + trainingSkill: UnderwaterAgilityThievingTrainingSkill; + quantity: number; + noStams: boolean; +} + +export interface PuroPuroActivityTaskOptions extends MinigameActivityTaskOptions { + type: 'PuroPuro'; + quantity: number; + darkLure: boolean; + implingTier: number | null; +} + +export interface GiantsFoundryActivityTaskOptions extends MinigameActivityTaskOptions { + type: 'GiantsFoundry'; + alloyID: number; + quantity: number; + metalScore: number; +} + +export interface GuardiansOfTheRiftActivityTaskOptions extends MinigameActivityTaskOptions { + type: 'GuardiansOfTheRift'; + minedFragments: number; + barrierAndGuardian: number; + rolls: number; + combinationRunes: boolean; +} + +export interface NightmareZoneActivityTaskOptions extends MinigameActivityTaskOptions { + type: 'NightmareZone'; + strategy: NMZStrategy; + quantity: number; +} + +export interface ShadesOfMortonOptions extends MinigameActivityTaskOptions { + type: 'ShadesOfMorton'; + shadeID: string; + logID: number; +} +export interface SpecificQuestOptions extends ActivityTaskOptions { + type: 'SpecificQuest'; + questID: number; +} + +export type ActivityTaskData = + | MonsterActivityTaskOptions + | WoodcuttingActivityTaskOptions + | CollectingOptions + | RaidsOptions + | GauntletOptions + | CastingActivityTaskOptions + | EnchantingActivityTaskOptions + | ConstructionActivityTaskOptions + | HunterActivityTaskOptions + | ZalcanoActivityTaskOptions + | SawmillActivityTaskOptions + | ButlerActivityTaskOptions + | FarmingActivityTaskOptions + | HerbloreActivityTaskOptions + | FletchingActivityTaskOptions + | RunecraftActivityTaskOptions + | TempleTrekkingActivityTaskOptions + | TemporossActivityTaskOptions + | PuroPuroActivityTaskOptions + | KourendFavourActivityTaskOptions + | AgilityActivityTaskOptions + | InfernoOptions + | TOAOptions + | NexTaskOptions + | ZalcanoActivityTaskOptions + | TheatreOfBloodTaskOptions + | GuardiansOfTheRiftActivityTaskOptions + | GiantsFoundryActivityTaskOptions + | NightmareZoneActivityTaskOptions + | ShadesOfMortonOptions + | UnderwaterAgilityThievingTaskOptions + | PickpocketActivityTaskOptions + | BuryingActivityTaskOptions + | ScatteringActivityTaskOptions + | OfferingActivityTaskOptions + | AnimatedArmourActivityTaskOptions + | CookingActivityTaskOptions + | CraftingActivityTaskOptions + | FiremakingActivityTaskOptions + | FishingActivityTaskOptions + | MiningActivityTaskOptions + | MotherlodeMiningActivityTaskOptions + | PlunderActivityTaskOptions + | SmithingActivityTaskOptions + | SmeltingActivityTaskOptions + | TiaraRunecraftActivityTaskOptions + | ClueActivityTaskOptions + | AlchingActivityTaskOptions + | DarkAltarOptions + | GroupMonsterActivityTaskOptions + | MahoganyHomesActivityTaskOptions + | NightmareActivityTaskOptions + | TitheFarmActivityTaskOptions + | SepulchreActivityTaskOptions + | GnomeRestaurantActivityTaskOptions + | SpecificQuestOptions + | ActivityTaskOptionsWithNoChanges + | TokkulShopOptions + | BirdhouseActivityTaskOptions + | FightCavesActivityTaskOptions + | ActivityTaskOptionsWithQuantity + | MinigameActivityTaskOptionsWithNoChanges + | CutLeapingFishActivityTaskOptions + | ColoTaskOptions; + diff --git a/package.json b/package.json index 9673ae32e9..a11ef3d90e 100644 --- a/package.json +++ b/package.json @@ -1,47 +1,54 @@ { "scripts": { - "watch": "nodemon -e ts -w src --exec 'yarn buildandrun'", + "watch": "nodemon --delay 1ms -e ts -w src --exec 'yarn buildandrun'", "build": "tsx ./src/scripts/build.ts", "fix": "tsx ./src/scripts/troubleshooter.ts", "start": "yarn build && node --enable-source-maps dist/", "gen": "concurrently --raw \"prisma generate --no-hints\" \"prisma generate --no-hints --schema prisma/robochimp.prisma\" && echo \"Generated Prisma Client\"", "prettify": "prettier --use-tabs \"./**/*.{md,yml}\" --write", - "lint": "concurrently --raw --kill-others-on-fail \"biome check --write --unsafe --diagnostic-level=error\" \"yarn prettify\"", + "lint": "concurrently --raw --kill-others-on-fail \"biome check --write --unsafe --diagnostic-level=error\" \"yarn prettify\" \"prisma format --schema ./prisma/robochimp.prisma\" \"prisma format --schema ./prisma/schema.prisma\"", "build:tsc": "tsc -p src", + "watch:tsc": "tsc -w -p src", "wipedist": "node -e \"try { require('fs').rmSync('dist', { recursive: true }) } catch(_){}\"", - "dev": "yarn && yarn wipedist && yarn lint && yarn build && yarn test && npm i -g dpdm && dpdm --exit-code circular:1 --progress=false --warning=false --tree=false ./dist/index.js", - "test": "concurrently --raw --kill-others-on-fail \"tsc -p src\" \"yarn test:lint\" \"yarn test:unit\" \"tsc -p tests/integration\" \"tsc -p tests/unit\"", + "dev": "concurrently --raw --kill-others-on-fail \"yarn\" \"yarn wipedist\" \"yarn lint\" && yarn build && yarn test", + "test": "concurrently --raw --kill-others-on-fail \"tsc -p src && yarn test:circular\" \"yarn test:lint\" \"yarn test:unit\" \"tsc -p tests/integration --noEmit\" \"tsc -p tests/unit --noEmit\"", "test:lint": "biome check --diagnostic-level=error", "test:unit": "vitest run --coverage --config vitest.unit.config.mts", + "test:docker": "docker compose up --build --abort-on-container-exit --remove-orphans && docker compose down --volumes --remove-orphans", "test:watch": "vitest --config vitest.unit.config.mts --coverage", - "test:integration": "tsx ./src/scripts/integration-tests.ts", - "buildandrun": "yarn build:esbuild && node dist", + "buildandrun": "yarn build:esbuild && node --enable-source-maps dist", "build:esbuild": "concurrently --raw \"yarn build:main\" \"yarn build:workers\"", - "build:main": "esbuild src/index.ts src/lib/workers/index.ts --minify --legal-comments=none --outdir=./dist --log-level=error --bundle --platform=node --loader:.node=file --external:@napi-rs/canvas --external:@prisma/robochimp --external:@prisma/client --external:zlib-sync --external:bufferutil --external:oldschooljs --external:discord.js --external:node-fetch --external:piscina", - "build:workers": "esbuild src/lib/workers/kill.worker.ts src/lib/workers/finish.worker.ts src/lib/workers/casket.worker.ts --log-level=error --bundle --minify --legal-comments=none --outdir=./dist/lib/workers --platform=node --loader:.node=file --external:@napi-rs/canvas --external:@prisma/robochimp --external:@prisma/client --external:zlib-sync --external:bufferutil --external:oldschooljs --external:discord.js --external:node-fetch --external:piscina" + "build:main": "esbuild src/index.ts src/lib/workers/index.ts --sourcemap=inline --minify --legal-comments=none --outdir=./dist --log-level=error --bundle --platform=node --loader:.node=file --external:@napi-rs/canvas --external:@prisma/robochimp --external:@prisma/client --external:zlib-sync --external:bufferutil --external:oldschooljs --external:discord.js --external:node-fetch --external:piscina", + "build:workers": "esbuild src/lib/workers/kill.worker.ts src/lib/workers/finish.worker.ts src/lib/workers/casket.worker.ts --sourcemap=inline --log-level=error --bundle --minify --legal-comments=none --outdir=./dist/lib/workers --platform=node --loader:.node=file --external:@napi-rs/canvas --external:@prisma/robochimp --external:@prisma/client --external:zlib-sync --external:bufferutil --external:oldschooljs --external:discord.js --external:node-fetch --external:piscina", + "test:circular": "dpdm --exit-code circular:1 --progress=false --warning=false --tree=false ./dist/index.js", + "test:ci:unit": "concurrently --raw --kill-others-on-fail \"yarn test:unit\" \"yarn test:lint\" \"tsc -p tests/integration\" \"tsc -p tests/unit\" \"yarn test:circular\"" }, "dependencies": { "@napi-rs/canvas": "^0.1.53", - "@oldschoolgg/toolkit": "git+https://github.com/oldschoolgg/toolkit.git#62f9f71274c6d36db6c56a2228ae3df9ab090dbd", - "@prisma/client": "^5.16.1", + "@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", "@sapphire/time-utilities": "^1.6.0", + "@sapphire/timer-manager": "^1.0.2", "@sentry/node": "^8.15.0", "ascii-table3": "^0.9.0", "bufferutil": "^4.0.8", "discord.js": "^14.15.3", "dotenv": "^16.4.5", "e": "0.2.33", + "exit-hook": "^4.0.0", "fast-deep-equal": "^3.1.3", "lodash": "^4.17.21", "lru-cache": "^10.3.0", "murmurhash": "^2.0.1", "node-cron": "^3.0.3", "node-fetch": "^2.6.7", - "oldschooljs": "^2.5.9", + "oldschooljs": "^2.5.10", "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", @@ -53,16 +60,17 @@ "@types/node": "^20.14.9", "@types/node-cron": "^3.0.7", "@types/node-fetch": "^2.6.1", - "@vitest/coverage-v8": "^1.6.0", + "@vitest/coverage-v8": "^2.0.3", "concurrently": "^8.2.2", + "dpdm": "^3.14.0", "esbuild": "0.21.5", "fast-glob": "^3.3.2", "nodemon": "^3.1.4", "prettier": "^3.3.2", - "prisma": "^5.16.1", + "prisma": "^5.17.0", "tsx": "^4.16.2", "typescript": "^5.5.3", - "vitest": "^1.6.0" + "vitest": "^2.0.3" }, "engines": { "node": "20.15.0" diff --git a/prisma/robochimp.prisma b/prisma/robochimp.prisma index f3e6cabce0..25d6275b83 100644 --- a/prisma/robochimp.prisma +++ b/prisma/robochimp.prisma @@ -1,93 +1,109 @@ generator client { - provider = "prisma-client-js" - previewFeatures = ["fullTextSearch"] - output = "../node_modules/@prisma/robochimp" + provider = "prisma-client-js" + previewFeatures = ["fullTextSearch"] + output = "../node_modules/@prisma/robochimp" } datasource db { - provider = "postgresql" - url = env("ROBOCHIMP_DATABASE_URL") + provider = "postgresql" + url = env("ROBOCHIMP_DATABASE_URL") } model TriviaQuestion { - id Int @id @unique @default(autoincrement()) - question String @db.VarChar() - answers String[] @db.VarChar() + id Int @id @unique @default(autoincrement()) + question String @db.VarChar() + answers String[] @db.VarChar() - @@map("trivia_question") + @@map("trivia_question") } enum BlacklistedEntityType { - guild - user + guild + user } model BlacklistedEntity { - id BigInt @id @unique - type BlacklistedEntityType - reason String? - date DateTime @default(now()) @db.Timestamp(6) + id BigInt @id @unique + type BlacklistedEntityType + reason String? + date DateTime @default(now()) @db.Timestamp(6) - @@map("blacklisted_entity") + @@map("blacklisted_entity") } model User { - id BigInt @id @unique - bits Int[] - github_id Int? - patreon_id String? - migrated_user_id BigInt? + id BigInt @id @unique + bits Int[] + github_id Int? + patreon_id String? - leagues_completed_tasks_ids Int[] - leagues_points_balance_osb Int @default(0) - leagues_points_balance_bso Int @default(0) - leagues_points_total Int @default(0) + migrated_user_id BigInt? - react_emoji_id String? + leagues_completed_tasks_ids Int[] + leagues_points_balance_osb Int @default(0) + leagues_points_balance_bso Int @default(0) + leagues_points_total Int @default(0) - osb_total_level Int? - bso_total_level Int? - osb_total_xp BigInt? - bso_total_xp BigInt? - osb_cl_percent Float? - bso_cl_percent Float? - osb_mastery Float? - bso_mastery Float? + react_emoji_id String? - tag Tag[] + osb_total_level Int? + bso_total_level Int? + osb_total_xp BigInt? + bso_total_xp BigInt? + osb_cl_percent Float? + bso_cl_percent Float? + osb_mastery Float? + bso_mastery Float? - store_bitfield Int[] + store_bitfield Int[] - testing_points Float @default(0) - testing_points_balance Float @default(0) + testing_points Float @default(0) + testing_points_balance Float @default(0) - @@map("user") + perk_tier Int @default(0) + premium_balance_tier Int? + premium_balance_expiry_date BigInt? + + user_group_id String? @db.Uuid + userGroup UserGroup? @relation(fields: [user_group_id], references: [id]) + + tag Tag[] + + @@map("user") } model PingableRole { - role_id String @id - name String @unique @db.VarChar(32) + role_id String @id + name String @unique @db.VarChar(32) - @@map("pingable_role") + @@map("pingable_role") } model Tag { - id Int @id @unique @default(autoincrement()) - name String @unique @db.VarChar(32) - content String @db.VarChar(2000) + id Int @id @unique @default(autoincrement()) + name String @unique @db.VarChar(32) + content String @db.VarChar(2000) - user_id BigInt - creator User @relation(fields: [user_id], references: [id]) + user_id BigInt + creator User @relation(fields: [user_id], references: [id]) - @@map("tag") + @@map("tag") } model StoreCode { - product_id Int - code String @id @unique + product_id Int + code String @id @unique + + redeemed_at DateTime? + redeemed_by_user_id String? @db.VarChar(19) + + @@map("store_code") +} + +model UserGroup { + id String @id @default(uuid()) @db.Uuid - redeemed_at DateTime? - redeemed_by_user_id String? @db.VarChar(19) + users User[] - @@map("store_code") + @@map("user_group") } diff --git a/prisma/schema.prisma b/prisma/schema.prisma index c2f03ebe69..0c894e7942 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -18,7 +18,7 @@ model Activity { group_activity Boolean type activity_type_enum channel_id BigInt - data Json @db.Json + data Json @db.JsonB pinnedTrip PinnedTrip[] @@index([user_id, finish_date]) @@ -60,9 +60,6 @@ model Analytic { model ClientStorage { id String @id @db.VarChar(19) - userBlacklist String[] @db.VarChar(19) - guildBlacklist String[] @db.VarChar(19) - commandStats Json @default("{}") @db.Json totalCommandsUsed Int @default(0) prices Json @default("{}") @db.Json sold_items_bank Json @default("{}") @db.Json @@ -125,7 +122,7 @@ model ClientStorage { degraded_items_cost Json @default("{}") @db.Json tks_cost Json @default("{}") @db.Json tks_loot Json @default("{}") @db.Json - disabled_commands String[] @db.VarChar(32) + disabled_commands String[] @default([]) @db.VarChar(32) gp_tax_balance BigInt @default(0) gotr_cost Json @default("{}") @db.Json gotr_loot Json @default("{}") @db.Json @@ -206,7 +203,7 @@ model Giveaway { message_id String @db.VarChar(19) reaction_id String? @db.VarChar(19) - users_entered String[] + users_entered String[] @default([]) @@index([completed, finish_date]) @@map("giveaway") @@ -214,11 +211,9 @@ model Giveaway { model Guild { id String @id @db.VarChar(19) - disabledCommands String[] - jmodComments String? @db.VarChar(19) + disabledCommands String[] @default([]) petchannel String? @db.VarChar(19) - tweetchannel String? @db.VarChar(19) - staffOnlyChannels String[] @db.VarChar(19) + staffOnlyChannels String[] @default([]) @db.VarChar(19) @@map("guilds") } @@ -309,19 +304,17 @@ model User { bank Json @default("{}") @db.Json collectionLogBank Json @default("{}") @db.JsonB blowpipe Json @default("{\"scales\":0,\"dartID\":null,\"dartQuantity\":0}") @db.Json - ironman_alts String[] - main_account String? - slayer_unlocks Int[] @map("slayer.unlocks") - slayer_blocked_ids Int[] @map("slayer.blocked_ids") + slayer_unlocks Int[] @default([]) @map("slayer.unlocks") + slayer_blocked_ids Int[] @default([]) @map("slayer.blocked_ids") slayer_last_task Int @default(0) @map("slayer.last_task") - badges Int[] - bitfield Int[] + badges Int[] @default([]) + bitfield Int[] @default([]) temp_cl Json @default("{}") @db.Json last_temp_cl_reset DateTime? @db.Timestamp(6) minion_equippedPet Int? @map("minion.equippedPet") minion_farmingContract Json? @map("minion.farmingContract") @db.Json minion_birdhouseTraps Json? @map("minion.birdhouseTraps") @db.Json - finished_quest_ids Int[] + finished_quest_ids Int[] @default([]) // Relations farmedCrops FarmedCrop[] @@ -332,19 +325,19 @@ model User { // Configs/Settings minion_defaultCompostToUse CropUpgradeType @default(compost) @map("minion.defaultCompostToUse") auto_farm_filter AutoFarmFilterEnum @default(AllFarm) - favoriteItems Int[] - favorite_alchables Int[] - favorite_food Int[] - favorite_bh_seeds Int[] + favoriteItems Int[] @default([]) + favorite_alchables Int[] @default([]) + favorite_food Int[] @default([]) + favorite_bh_seeds Int[] @default([]) minion_defaultPay Boolean @default(false) @map("minion.defaultPay") minion_icon String? @map("minion.icon") minion_name String? @map("minion.name") bank_bg_hex String? bankBackground Int @default(1) - attack_style String[] - combat_options Int[] + attack_style String[] @default([]) + combat_options Int[] @default([]) slayer_remember_master String? @map("slayer.remember_master") - slayer_autoslay_options Int[] @map("slayer.autoslay_options") + slayer_autoslay_options Int[] @default([]) @map("slayer.autoslay_options") bank_sort_method String? @db.VarChar(16) bank_sort_weightings Json @default("{}") @db.Json gambling_lockout_expiry DateTime? @@ -407,11 +400,10 @@ model User { zeal_tokens Int @default(0) slayer_points Int @default(0) @map("slayer.points") - completed_ca_task_ids Int[] + completed_ca_task_ids Int[] @default([]) - store_bitfield Int[] + 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 @@ -434,6 +426,8 @@ model User { cached_networth_value BigInt? + username String? @db.VarChar(32) + geListings GEListing[] bingo_participant BingoParticipant[] bingo Bingo[] @@ -441,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") } @@ -583,14 +579,12 @@ 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) - flags Json? - inhibited Boolean? @default(false) + command_name command_name_enum + is_continue Boolean @default(false) + inhibited Boolean? @default(false) is_mention_command Boolean @default(false) @@ -670,7 +664,7 @@ model EconomyTransaction { model StashUnit { stash_id Int user_id BigInt - items_contained Int[] + items_contained Int[] @default([]) has_built Boolean @@unique([stash_id, user_id]) @@ -698,7 +692,7 @@ model UserStats { farming_plant_cost_bank Json @default("{}") @db.Json farming_harvest_loot_bank Json @default("{}") @db.Json - cl_array Int[] + cl_array Int[] @default([]) cl_array_length Int @default(0) buy_cost_bank Json @default("{}") @db.Json @@ -751,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) @@ -854,13 +848,6 @@ model HistoricalData { @@map("historical_data") } -enum command_usage_status { - Unknown - Success - Error - Inhibited -} - enum activity_type_enum { Agility Cooking @@ -1056,14 +1043,14 @@ model Bingo { is_global Boolean @default(false) - organizers String[] + organizers String[] @default([]) start_date DateTime @default(now()) @db.Timestamp(6) duration_days Int team_size Int title String notifications_channel_id String @db.VarChar(19) ticket_price BigInt - bingo_tiles Json[] + bingo_tiles Json[] @default([]) was_finalized Boolean @default(false) guild_id String @db.VarChar(19) @@ -1157,3 +1144,273 @@ model UserEvent { @@map("user_event") } + +enum command_name_enum { + testpotato + achievementdiary + activities + admin + aerialfish + agilityarena + alch + amrod + ash + ask + autoequip + autoslay + bal + bank + bankbg + barbassault + bgcolor + bingo + birdhouse + blastfurnace + blowpipe + bossrecords + botleagues + botstats + bs + bso + build + bury + buy + ca + cancel + capegamble + cash + casket + cast + castlewars + cd + championchallenge + channel + chargeglories + chargewealth + checkmasses + checkpatch + chompyhunt + choose + chop + christmas + cl + claim + clbank + clue + clues + cmd + collect + collectionlog + combat + combatoptions + compostbin + config + cook + cox + cracker + craft + create + daily + darkaltar + data + decant + defaultfarming + defender + diary + dice + dicebank + disable + dmm + driftnet + drop + drycalc + drystreak + duel + easter + economybank + emotes + enable + enchant + equip + eval + fake + fakearma + fakebandos + fakeely + fakepm + fakesara + fakescythe + fakezammy + faq + farm + farming + farmingcontract + favalch + favfood + favorite + favour + fightcaves + finish + fish + fishingtrawler + fletch + gamble + gauntlet + ge + gear + gearpresets + gearstats + gift + github + giveaway + gnomerestaurant + gp + groupkill + halloween + hans + harvest + hcim + hcimdeaths + help + hiscores + hunt + inbank + inferno + info + invite + ironman + is + itemtrivia + jmodcomments + jmodtweets + k + kc + kcgains + kill + lamp + lapcount + laps + lastmanstanding + lb + leaderboard + leagues + light + lms + loot + love + luckyimp + luckypick + lvl + m + magearena + magearena2 + mahoganyhomes + mass + mclue + mine + minigames + minion + minionstats + mix + monster + mostdrops + mta + mygiveaways + mypets + news + nightmare + offer + open + osrskc + patreon + pay + pestcontrol + pet + petmessages + petrate + petroll + pickpocket + ping + players + plunder + poh + poll + polls + prefix + price + pvp + quest + raid + randomevents + randquote + ranks + rc + redeem + reload + resetrng + revs + roguesden + roles + roll + rp + runecraft + runelite + s + sacrifice + sacrificedbank + sacrificegp + sacrificelog + sawmill + seedpack + sell + sellto + sendtoabutton + sepulchre + server + setrsn + shutdownlock + simulate + skillcape + slayer + slayershop + slayertask + smelt + smith + soulwars + stats + steal + streamertweets + support + tag + tearsofguthix + tempoross + tithefarm + tithefarmshop + tob + tokkulshop + tools + trade + train + trek + trekshop + trickortreat + trivia + tweets + uim + unequip + unequipall + use + user + virtualstats + volcanicmine + warriorsguild + wiki + wintertodt + world + wt + wyson + xp + xpgains + xpto99 + zalcano +} diff --git a/repeatStoredTrip.ts b/repeatStoredTrip.ts new file mode 100644 index 0000000000..329a168b0a --- /dev/null +++ b/repeatStoredTrip.ts @@ -0,0 +1,716 @@ +import type { Activity, Prisma } from '@prisma/client'; +import { activity_type_enum } from '@prisma/client'; +import type { ButtonInteraction } from 'discord.js'; +import { ButtonBuilder, ButtonStyle } from 'discord.js'; +import { Time } from 'e'; + +import { autocompleteMonsters } from '../../mahoji/commands/k'; +import type { PvMMethod } from '../constants'; +import { SlayerActivityConstants } from '../minions/data/combatConstants'; +import { darkAltarRunes } from '../minions/functions/darkAltarCommand'; +import { convertStoredActivityToFlatActivity } from '../settings/prisma'; +import { runCommand } from '../settings/settings'; +import type { + ActivityTaskOptionsWithQuantity, + AgilityActivityTaskOptions, + AlchingActivityTaskOptions, + AnimatedArmourActivityTaskOptions, + BuryingActivityTaskOptions, + ButlerActivityTaskOptions, + CastingActivityTaskOptions, + ClueActivityTaskOptions, + CollectingOptions, + ConstructionActivityTaskOptions, + CookingActivityTaskOptions, + CraftingActivityTaskOptions, + CutLeapingFishActivityTaskOptions, + DarkAltarOptions, + EnchantingActivityTaskOptions, + FarmingActivityTaskOptions, + FiremakingActivityTaskOptions, + FishingActivityTaskOptions, + FletchingActivityTaskOptions, + GauntletOptions, + GiantsFoundryActivityTaskOptions, + GroupMonsterActivityTaskOptions, + GuardiansOfTheRiftActivityTaskOptions, + HerbloreActivityTaskOptions, + HunterActivityTaskOptions, + MahoganyHomesActivityTaskOptions, + MiningActivityTaskOptions, + MonsterActivityTaskOptions, + MotherlodeMiningActivityTaskOptions, + NexTaskOptions, + NightmareActivityTaskOptions, + OfferingActivityTaskOptions, + PickpocketActivityTaskOptions, + PuroPuroActivityTaskOptions, + RaidsOptions, + RunecraftActivityTaskOptions, + SawmillActivityTaskOptions, + ScatteringActivityTaskOptions, + ShadesOfMortonOptions, + SmeltingActivityTaskOptions, + SmithingActivityTaskOptions, + TOAOptions, + TempleTrekkingActivityTaskOptions, + TheatreOfBloodTaskOptions, + TiaraRunecraftActivityTaskOptions, + WoodcuttingActivityTaskOptions, + ZalcanoActivityTaskOptions +} from '../types/minions'; +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'; + +const taskCanBeRepeated = (activity: Activity) => { + if (activity.type === activity_type_enum.ClueCompletion) { + const realActivity = convertStoredActivityToFlatActivity(activity) as ClueActivityTaskOptions; + return realActivity.implingID !== undefined; + } + return !( + [ + activity_type_enum.TearsOfGuthix, + activity_type_enum.ShootingStars, + activity_type_enum.BirthdayEvent, + activity_type_enum.BlastFurnace, + activity_type_enum.Easter, + activity_type_enum.TokkulShop, + activity_type_enum.Birdhouse, + activity_type_enum.StrongholdOfSecurity, + activity_type_enum.CombatRing + ] as activity_type_enum[] + ).includes(activity.type); +}; + +const tripHandlers = { + [activity_type_enum.ClueCompletion]: { + commandName: 'clue', + args: (data: ClueActivityTaskOptions) => ({ tier: data.clueID, implings: getOSItem(data.implingID!).name }) + }, + [activity_type_enum.SpecificQuest]: { + commandName: 'm', + args: () => ({}) + }, + [activity_type_enum.HalloweenEvent]: { + commandName: 'm', + args: () => ({}) + }, + [activity_type_enum.Birdhouse]: { + commandName: 'm', + args: () => ({}) + }, + [activity_type_enum.StrongholdOfSecurity]: { + commandName: 'm', + args: () => ({}) + }, + [activity_type_enum.CombatRing]: { + commandName: 'm', + args: () => ({}) + }, + [activity_type_enum.TearsOfGuthix]: { + commandName: 'm', + args: () => ({}) + }, + [activity_type_enum.TokkulShop]: { + commandName: 'm', + args: () => ({}) + }, + [activity_type_enum.ShootingStars]: { + commandName: 'm', + args: () => ({}) + }, + [activity_type_enum.BirthdayEvent]: { + commandName: 'm', + args: () => ({}) + }, + [activity_type_enum.BlastFurnace]: { + commandName: 'm', + args: () => ({}) + }, + [activity_type_enum.Easter]: { + commandName: 'm', + args: () => ({}) + }, + [activity_type_enum.Revenants]: { + commandName: 'm', + args: () => ({}) + }, + [activity_type_enum.KourendFavour]: { + commandName: 'm', + args: () => ({}) + }, + [activity_type_enum.AerialFishing]: { + commandName: 'activities', + args: () => ({ aerial_fishing: {} }) + }, + [activity_type_enum.Agility]: { + commandName: 'laps', + args: (data: AgilityActivityTaskOptions) => ({ + name: data.courseID, + quantity: data.quantity, + alch: Boolean(data.alch) + }) + }, + [activity_type_enum.AgilityArena]: { + commandName: 'minigames', + args: () => ({ agility_arena: { start: {} } }) + }, + [activity_type_enum.Alching]: { + commandName: 'activities', + args: (data: AlchingActivityTaskOptions) => ({ + alch: { quantity: data.quantity, item: itemNameFromID(data.itemID) } + }) + }, + [activity_type_enum.AnimatedArmour]: { + commandName: 'activities', + args: (data: AnimatedArmourActivityTaskOptions) => ({ + warriors_guild: { action: 'tokens', quantity: data.quantity } + }) + }, + [activity_type_enum.CamdozaalMining]: { + commandName: 'activities', + args: (data: ActivityTaskOptionsWithQuantity) => ({ + camdozaal: { action: 'mining', quantity: data.iQty } + }) + }, + [activity_type_enum.CamdozaalSmithing]: { + commandName: 'activities', + args: (data: ActivityTaskOptionsWithQuantity) => ({ + camdozaal: { action: 'smithing', quantity: data.quantity } + }) + }, + [activity_type_enum.CamdozaalFishing]: { + commandName: 'activities', + args: (data: ActivityTaskOptionsWithQuantity) => ({ + camdozaal: { action: 'fishing', quantity: data.iQty } + }) + }, + [activity_type_enum.BarbarianAssault]: { + commandName: 'minigames', + args: () => ({ barb_assault: { start: {} } }) + }, + [activity_type_enum.BigChompyBirdHunting]: { + commandName: 'activities', + args: () => ({ chompy_hunt: { action: 'start' } }) + }, + [activity_type_enum.Smelting]: { + commandName: 'smelt', + args: (data: SmeltingActivityTaskOptions) => ({ + name: itemNameFromID(data.barID), + quantity: data.quantity, + blast_furnace: data.blastf + }) + }, + [activity_type_enum.Burying]: { + commandName: 'activities', + args: (data: BuryingActivityTaskOptions) => ({ + bury: { quantity: data.quantity, name: itemNameFromID(data.boneID) } + }) + }, + [activity_type_enum.Scattering]: { + commandName: 'activities', + args: (data: ScatteringActivityTaskOptions) => ({ + scatter: { quantity: data.quantity, name: itemNameFromID(data.ashID) } + }) + }, + [activity_type_enum.Casting]: { + commandName: 'activities', + args: (data: CastingActivityTaskOptions) => ({ cast: { spell: data.spellID, quantity: data.quantity } }) + }, + [activity_type_enum.CastleWars]: { + commandName: 'minigames', + args: () => ({ castle_wars: { start: {} } }) + }, + [activity_type_enum.ChampionsChallenge]: { + commandName: 'activities', + args: () => ({ champions_challenge: {} }) + }, + [activity_type_enum.Collecting]: { + commandName: 'activities', + args: (data: CollectingOptions) => ({ + collect: { item: itemNameFromID(data.collectableID), no_stams: data.noStaminas, quantity: data.quantity } + }) + }, + [activity_type_enum.Construction]: { + commandName: 'build', + args: (data: ConstructionActivityTaskOptions) => ({ name: data.objectID, quantity: data.quantity }) + }, + [activity_type_enum.Cooking]: { + commandName: 'cook', + args: (data: CookingActivityTaskOptions) => ({ + name: itemNameFromID(data.cookableID), + quantity: data.quantity + }) + }, + [activity_type_enum.Crafting]: { + commandName: 'craft', + args: (data: CraftingActivityTaskOptions) => ({ + name: itemNameFromID(data.craftableID), + quantity: data.quantity + }) + }, + [activity_type_enum.Cyclops]: { + commandName: 'activities', + args: (data: ActivityTaskOptionsWithQuantity) => ({ + warriors_guild: { action: 'cyclops', quantity: data.quantity } + }) + }, + [activity_type_enum.DarkAltar]: { + commandName: 'runecraft', + args: (data: DarkAltarOptions) => ({ rune: `${darkAltarRunes[data.rune].item.name} (zeah)` }) + }, + [activity_type_enum.Runecraft]: { + commandName: 'runecraft', + args: (data: RunecraftActivityTaskOptions) => ({ + rune: itemNameFromID(data.runeID), + quantity: data.essenceQuantity, + daeyalt_essence: data.daeyaltEssence, + usestams: data.useStaminas + }) + }, + [activity_type_enum.TiaraRunecraft]: { + commandName: 'runecraft', + args: (data: TiaraRunecraftActivityTaskOptions) => ({ + rune: itemNameFromID(data.tiaraID), + quantity: data.tiaraQuantity + }) + }, + [activity_type_enum.Enchanting]: { + commandName: 'activities', + args: (data: EnchantingActivityTaskOptions) => ({ + enchant: { quantity: data.quantity, name: itemNameFromID(data.itemID) } + }) + }, + [activity_type_enum.Farming]: { + commandName: 'farming', + args: (data: FarmingActivityTaskOptions) => + data.autoFarmed + ? { + auto_farm: {} + } + : {} + }, + [activity_type_enum.FightCaves]: { + commandName: 'activities', + args: () => ({ fight_caves: {} }) + }, + [activity_type_enum.Firemaking]: { + commandName: 'light', + args: (data: FiremakingActivityTaskOptions) => ({ + name: itemNameFromID(data.burnableID), + quantity: data.quantity + }) + }, + [activity_type_enum.Fishing]: { + commandName: 'fish', + args: (data: FishingActivityTaskOptions) => ({ + name: data.fishID, + quantity: data.quantity, + powerfish: data.powerfish ?? false, + spirit_flakes: data.spirit_flakes ?? false + }) + + }, + [activity_type_enum.FishingTrawler]: { + commandName: 'minigames', + args: () => ({ fishing_trawler: { start: {} } }) + }, + [activity_type_enum.Fletching]: { + commandName: 'fletch', + args: (data: FletchingActivityTaskOptions) => ({ name: data.fletchableName, quantity: data.quantity }) + }, + [activity_type_enum.Gauntlet]: { + commandName: 'minigames', + args: (data: GauntletOptions) => ({ gauntlet: { start: { corrupted: data.corrupted } } }) + }, + [activity_type_enum.GloryCharging]: { + commandName: 'activities', + args: (data: ActivityTaskOptionsWithQuantity) => ({ charge: { item: 'glory', quantity: data.quantity } }) + }, + [activity_type_enum.GnomeRestaurant]: { + commandName: 'minigames', + args: () => ({ gnome_restaurant: { start: {} } }) + }, + [activity_type_enum.GroupMonsterKilling]: { + commandName: 'mass', + args: (data: GroupMonsterActivityTaskOptions) => ({ + monster: autocompleteMonsters.find(i => i.id === data.monsterID)?.name ?? data.monsterID.toString() + }) + }, + [activity_type_enum.Herblore]: { + commandName: 'mix', + args: (data: HerbloreActivityTaskOptions) => ({ + name: itemNameFromID(data.mixableID), + quantity: data.quantity, + zahur: data.zahur + }) + }, + [activity_type_enum.CutLeapingFish]: { + commandName: 'cook', + args: (data: CutLeapingFishActivityTaskOptions) => ({ + name: itemNameFromID(data.fishID), + quantity: data.quantity + }) + }, + [activity_type_enum.Hunter]: { + commandName: 'hunt', + args: (data: HunterActivityTaskOptions) => ({ + name: data.creatureName, + quantity: data.quantity, + hunter_potion: data.usingHuntPotion, + stamina_potions: data.usingStaminaPotion + }) + }, + [activity_type_enum.Inferno]: { + commandName: 'activities', + args: () => ({ inferno: { action: 'start' } }) + }, + [activity_type_enum.LastManStanding]: { + commandName: 'minigames', + args: () => ({ lms: { start: {} } }) + }, + [activity_type_enum.MageArena]: { + commandName: 'minigames', + args: () => ({ mage_arena: { start: {} } }) + }, + [activity_type_enum.MageArena2]: { + commandName: 'minigames', + args: () => ({ mage_arena_2: { start: {} } }) + }, + [activity_type_enum.MageTrainingArena]: { + commandName: 'minigames', + args: () => ({ mage_training_arena: { start: {} } }) + }, + [activity_type_enum.MahoganyHomes]: { + commandName: 'minigames', + args: (data: MahoganyHomesActivityTaskOptions) => ({ mahogany_homes: { start: { tier: data.tier } } }) + }, + [activity_type_enum.Mining]: { + commandName: 'mine', + args: (data: MiningActivityTaskOptions) => ({ + name: data.oreID, + quantity: data.iQty, + powermine: data.powermine + }) + }, + [activity_type_enum.MotherlodeMining]: { + commandName: 'mine', + args: (data: MotherlodeMiningActivityTaskOptions) => ({ + name: 'Motherlode mine', + quantity: data.iQty + }) + }, + [activity_type_enum.MonsterKilling]: { + commandName: 'k', + args: (data: MonsterActivityTaskOptions) => { + let method: PvMMethod = 'none'; + if (data.usingCannon) method = 'cannon'; + if (data.chinning) method = 'chinning'; + else if (data.burstOrBarrage === SlayerActivityConstants.IceBarrage) method = 'barrage'; + else if (data.burstOrBarrage === SlayerActivityConstants.IceBurst) method = 'burst'; + return { + name: autocompleteMonsters.find(i => i.id === data.monsterID)?.name ?? data.monsterID.toString(), + quantity: data.iQty, + method, + wilderness: data.isInWilderness + }; + } + }, + [activity_type_enum.Nex]: { + commandName: 'k', + args: (data: NexTaskOptions) => { + return { + name: 'nex', + quantity: data.quantity, + solo: data.userDetails.length === 1 + }; + } + }, + [activity_type_enum.Zalcano]: { + commandName: 'k', + args: (data: ZalcanoActivityTaskOptions) => ({ + name: 'zalcano', + quantity: data.quantity + }) + }, + [activity_type_enum.Tempoross]: { + commandName: 'k', + args: () => ({ + name: 'tempoross' + }) + }, + [activity_type_enum.Wintertodt]: { + commandName: 'k', + args: (data: ActivityTaskOptionsWithQuantity) => ({ + name: 'wintertodt', + quantity: data.quantity + }) + }, + [activity_type_enum.Nightmare]: { + commandName: 'k', + args: (data: NightmareActivityTaskOptions) => ({ + name: data.isPhosani ? 'phosani nightmare' : data.method === 'mass' ? 'mass nightmare' : 'solo nightmare', + quantity: data.quantity + }) + }, + [activity_type_enum.Offering]: { + commandName: 'offer', + args: (data: OfferingActivityTaskOptions) => ({ quantity: data.quantity, name: itemNameFromID(data.boneID) }) + }, + [activity_type_enum.PestControl]: { + commandName: 'minigames', + args: () => ({ pest_control: { start: {} } }) + }, + [activity_type_enum.Pickpocket]: { + commandName: 'steal', + args: (data: PickpocketActivityTaskOptions) => ({ name: data.monsterID, quantity: data.quantity }) + }, + [activity_type_enum.Plunder]: { + commandName: 'minigames', + args: () => ({ pyramid_plunder: {} }) + }, + [activity_type_enum.PuroPuro]: { + commandName: 'activities', + args: (data: PuroPuroActivityTaskOptions) => ({ + puro_puro: { implingTier: data.implingTier || '', dark_lure: data.darkLure } + }) + }, + [activity_type_enum.Questing]: { + commandName: 'activities', + args: () => ({ + quest: {} + }) + }, + [activity_type_enum.Raids]: { + commandName: 'raid', + args: (data: RaidsOptions) => ({ + cox: { + start: { + challenge_mode: data.challengeMode, + type: data.users.length === 1 ? 'solo' : 'mass', + quantity: data.quantity + } + } + }) + }, + [activity_type_enum.RoguesDenMaze]: { + commandName: 'minigames', + args: () => ({ + rogues_den: {} + }) + }, + [activity_type_enum.Sawmill]: { + commandName: 'activities', + args: (data: SawmillActivityTaskOptions) => ({ + plank_make: { action: 'sawmill', quantity: data.plankQuantity, type: itemNameFromID(data.plankID) } + }) + }, + [activity_type_enum.Butler]: { + commandName: 'activities', + args: (data: ButlerActivityTaskOptions) => ({ + plank_make: { action: 'butler', quantity: data.plankQuantity, type: itemNameFromID(data.plankID) } + }) + }, + [activity_type_enum.Sepulchre]: { + commandName: 'minigames', + args: () => ({ sepulchre: { start: {} } }) + }, + [activity_type_enum.Smithing]: { + commandName: 'smith', + args: (data: SmithingActivityTaskOptions) => ({ + name: itemNameFromID(data.smithedBarID), + quantity: data.quantity + }) + }, + [activity_type_enum.SoulWars]: { + commandName: 'minigames', + args: () => ({ soul_wars: { start: {} } }) + }, + [activity_type_enum.TheatreOfBlood]: { + commandName: 'raid', + args: (data: TheatreOfBloodTaskOptions) => ({ + tob: { + start: { + hard_mode: data.hardMode, + solo: data.solo + } + } + }) + }, + [activity_type_enum.TitheFarm]: { + commandName: 'farming', + args: () => ({ tithe_farm: {} }) + }, + [activity_type_enum.Trekking]: { + commandName: 'minigames', + args: (data: TempleTrekkingActivityTaskOptions) => ({ + temple_trek: { start: { difficulty: data.difficulty, quantity: data.quantity } } + }) + }, + [activity_type_enum.TroubleBrewing]: { + commandName: 'minigames', + args: () => ({ trouble_brewing: { start: {} } }) + }, + [activity_type_enum.VolcanicMine]: { + commandName: 'minigames', + args: (data: ActivityTaskOptionsWithQuantity) => ({ volcanic_mine: { start: { quantity: data.quantity } } }) + }, + [activity_type_enum.WealthCharging]: { + commandName: 'activities', + args: (data: ActivityTaskOptionsWithQuantity) => ({ charge: { item: 'wealth', quantity: data.quantity } }) + }, + [activity_type_enum.Woodcutting]: { + commandName: 'chop', + args: (data: WoodcuttingActivityTaskOptions) => ({ + name: itemNameFromID(data.logID), + quantity: data.iQty, + powerchop: data.powerchopping, + forestry_events: data.forestry, + twitchers_gloves: data.twitchers + }) + }, + [activity_type_enum.GiantsFoundry]: { + commandName: 'minigames', + args: (data: GiantsFoundryActivityTaskOptions) => ({ + giants_foundry: { + start: { name: giantsFoundryAlloys.find(i => i.id === data.alloyID)?.name, quantity: data.quantity } + } + }) + }, + [activity_type_enum.GuardiansOfTheRift]: { + commandName: 'minigames', + args: (data: GuardiansOfTheRiftActivityTaskOptions) => ({ + gotr: { + start: { combination_runes: data.combinationRunes } + } + }) + }, + [activity_type_enum.NightmareZone]: { + commandName: 'minigames', + args: (data: NightmareZoneActivityTaskOptions) => ({ + nmz: { + start: { strategy: data.strategy } + } + }) + }, + [activity_type_enum.ShadesOfMorton]: { + commandName: 'minigames', + args: (data: ShadesOfMortonOptions) => ({ + shades_of_morton: { + start: { shade: data.shadeID, logs: itemNameFromID(data.logID) } + } + }) + }, + [activity_type_enum.TombsOfAmascut]: { + commandName: 'raid', + args: (data: TOAOptions) => ({ + toa: { + start: { + raid_level: data.raidLevel, + max_team_size: data.users.length, + solo: data.users.length === 1, + quantity: data.quantity + } + } + }) + }, + [activity_type_enum.UnderwaterAgilityThieving]: { + commandName: 'activities', + args: (data: UnderwaterAgilityThievingTaskOptions) => ({ + underwater: { + agility_thieving: { + training_skill: data.trainingSkill, + minutes: Math.floor(data.duration / Time.Minute), + no_stams: data.noStams + } + } + }) + }, + [activity_type_enum.DriftNet]: { + commandName: 'activities', + args: (data: ActivityTaskOptionsWithQuantity) => ({ + underwater: { + drift_net_fishing: { minutes: Math.floor(data.duration / Time.Minute) } + } + }) + }, + [activity_type_enum.Colosseum]: { + commandName: 'k', + args: () => ({ + name: 'colosseum' + }) + } +} as const; + +for (const type of Object.values(activity_type_enum)) { + if (!tripHandlers[type]) { + throw new Error(`Missing trip handler for ${type}`); + } +} + +export async function fetchRepeatTrips(userID: string) { + const res: Activity[] = await prisma.activity.findMany({ + where: { + user_id: BigInt(userID), + finish_date: { + gt: new Date(Date.now() - Time.Day * 7) + } + }, + orderBy: { + id: 'desc' + }, + take: 20 + }); + const filtered: { + type: activity_type_enum; + data: Prisma.JsonValue; + }[] = []; + for (const trip of res) { + if (!taskCanBeRepeated(trip)) continue; + if (trip.type === activity_type_enum.Farming && !(trip.data as any as FarmingActivityTaskOptions).autoFarmed) { + continue; + } + if (!filtered.some(i => i.type === trip.type)) { + filtered.push(trip); + } + } + return filtered; +} + +export async function makeRepeatTripButtons(user: MUser) { + const trips = await fetchRepeatTrips(user.id); + const buttons: ButtonBuilder[] = []; + const limit = Math.min(user.perkTier() + 1, 5); + for (const trip of trips.slice(0, limit)) { + buttons.push( + new ButtonBuilder() + .setLabel(`Repeat ${trip.type}`) + .setCustomId(`REPEAT_TRIP_${trip.type}`) + .setStyle(ButtonStyle.Secondary) + ); + } + return buttons; +} + +export async function repeatTrip( + interaction: ButtonInteraction, + data: { data: Prisma.JsonValue; type: activity_type_enum } +) { + await deferInteraction(interaction); + const handler = tripHandlers[data.type]; + return runCommand({ + commandName: handler.commandName, + isContinue: true, + args: handler.args(data.data as any), + interaction, + guildID: interaction.guildId, + member: interaction.member, + channelID: interaction.channelId, + user: interaction.user, + continueDeltaMillis: interaction.createdAt.getTime() - interaction.message.createdTimestamp + }); +} + 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..e9cce8683f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,5 @@ import './lib/safeglobals'; import './lib/globals'; -import './lib/crons'; import './lib/MUser'; import './lib/util/transactItemsFromBank'; import './lib/geImage'; @@ -11,28 +10,25 @@ 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 { syncActivityCache } from './lib/Task'; +import { SENTRY_DSN, SupportServer } from './config'; import { BLACKLISTED_GUILDS, BLACKLISTED_USERS } from './lib/blacklists'; -import { Channel, Events, META_CONSTANTS, gitHash, globalConfig } from './lib/constants'; +import { Channel, Events, gitHash, globalConfig } from './lib/constants'; import { economyLog } from './lib/economyLogs'; import { onMessage } from './lib/events'; import { modalInteractionHook } from './lib/modals'; -import { runStartupScripts } from './lib/startupScripts'; +import { preStartup } from './lib/preStartup'; import { OldSchoolBotClient } from './lib/structures/OldSchoolBotClient'; -import { assert, runTimedLoggedFn } from './lib/util'; -import { CACHED_ACTIVE_USER_IDS, syncActiveUserIDs } from './lib/util/cachedUserIDs'; +import { CACHED_ACTIVE_USER_IDS } from './lib/util/cachedUserIDs'; import { interactionHook } from './lib/util/globalInteractions'; -import { handleInteractionError } from './lib/util/interactionReply'; +import { handleInteractionError, interactionReply } from './lib/util/interactionReply'; import { logError } from './lib/util/logError'; import { allCommands } from './mahoji/commands/allCommands'; import { onStartup } from './mahoji/lib/events'; +import { exitCleanup } from './mahoji/lib/exitHandler'; import { postCommand } from './mahoji/lib/postCommand'; import { preCommand } from './mahoji/lib/preCommand'; import { convertMahojiCommandToAbstractCommand } from './mahoji/lib/util'; -debugLog(`Starting... Git Hash ${META_CONSTANTS.GIT_HASH}`); - if (SENTRY_DSN) { init({ dsn: SENTRY_DSN, @@ -43,8 +39,6 @@ if (SENTRY_DSN) { }); } -assert(process.env.TZ === 'UTC'); - const client = new OldSchoolBotClient({ shards: 'auto', intents: [ @@ -71,7 +65,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 +89,7 @@ const client = new OldSchoolBotClient({ }); export const mahojiClient = new MahojiClient({ - developmentServerID: DEV_SERVER_ID, + developmentServerID: globalConfig.testingServerID, applicationID: globalConfig.clientID, commands: allCommands, handlers: { @@ -133,15 +130,26 @@ global.globalClient = client; client.on('messageCreate', msg => { onMessage(msg); }); +client.on('error', console.error); client.on('interactionCreate', async interaction => { - if (BLACKLISTED_USERS.has(interaction.user.id)) return; - if (interaction.guildId && BLACKLISTED_GUILDS.has(interaction.guildId)) return; - - if (!client.isReady()) { - if (interaction.isChatInputCommand()) { - await interaction.reply({ + if (globalClient.isShuttingDown) { + if (interaction.isRepliable()) { + await interactionReply(interaction, { content: - 'Old School Bot is currently down for maintenance/updates, please try again in a couple minutes! Thank you <3', + 'Old School Bot is currently shutting down for maintenance/updates, please try again in a couple minutes! Thank you <3', + ephemeral: true + }); + } + return; + } + + if ( + BLACKLISTED_USERS.has(interaction.user.id) || + (interaction.guildId && BLACKLISTED_GUILDS.has(interaction.guildId)) + ) { + if (interaction.isRepliable()) { + await interactionReply(interaction, { + content: 'You are blacklisted.', ephemeral: true }); } @@ -167,7 +175,9 @@ client.on('interactionCreate', async interaction => { client.on(Events.ServerNotification, (message: string) => { const channel = globalClient.channels.cache.get(Channel.Notifications); - if (channel) (channel as TextChannel).send(message); + if (channel) { + (channel as TextChannel).send({ content: message, allowedMentions: { parse: [], users: [], roles: [] } }); + } }); client.on(Events.EconomyLog, async (message: string) => { @@ -180,19 +190,21 @@ 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())); +client.once('ready', () => onStartup()); async function main() { - if (process.env.TEST) return; await Promise.all([ - runTimedLoggedFn('Sync Active User IDs', syncActiveUserIDs), - runTimedLoggedFn('Sync Activity Cache', syncActivityCache) + preStartup(), + import('exit-hook').then(({ asyncExitHook }) => + asyncExitHook(exitCleanup, { + wait: 2000 + }) + ) ]); - await runTimedLoggedFn('Startup Scripts', runStartupScripts); - - await runTimedLoggedFn('Log In', () => client.login(globalConfig.botToken)); + if (process.env.TEST) return; + await 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 d2ebaa8e45..5d892ba99e 100644 --- a/src/lib/DynamicButtons.ts +++ b/src/lib/DynamicButtons.ts @@ -1,5 +1,6 @@ import type { BaseMessageOptions, + ButtonInteraction, DMChannel, Message, MessageComponentInteraction, @@ -8,11 +9,12 @@ 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'; import { awaitMessageComponentInteraction, makeComponents } from './util'; +import { silentButtonAck } from './util/handleMahojiConfirmation'; import { minionIsBusy } from './util/minionIsBusy'; type DynamicButtonFn = (opts: { message: Message; interaction: MessageComponentInteraction }) => unknown; @@ -21,7 +23,7 @@ export class DynamicButtons { buttons: { name: string; id: string; - fn: DynamicButtonFn; + fn?: DynamicButtonFn; emoji: string | undefined; cantBeBusy: boolean; style?: ButtonStyle; @@ -83,9 +85,11 @@ export class DynamicButtons { ...messageOptions, components: makeComponents(buttons) }); - const collectedInteraction = await awaitMessageComponentInteraction({ + const collectedInteraction: ButtonInteraction = (await awaitMessageComponentInteraction({ message: this.message, - filter: i => { + filter: async i => { + if (!i.isButton()) return false; + await silentButtonAck(i); if (BLACKLISTED_USERS.has(i.user.id)) return false; if (this.usersWhoCanInteract.includes(i.user.id)) { return true; @@ -94,7 +98,7 @@ export class DynamicButtons { return false; }, time: this.timer ?? Time.Second * 20 - }).catch(noOp); + }).catch(noOp)) as ButtonInteraction; if (this.deleteAfterConfirm === true) { await this.message.delete().catch(noOp); } else { @@ -104,20 +108,22 @@ export class DynamicButtons { if (collectedInteraction) { for (const button of this.buttons) { if (collectedInteraction.customId === button.id) { - collectedInteraction.deferUpdate(); 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({ @@ -128,7 +134,7 @@ export class DynamicButtons { style }: { name: string; - fn: DynamicButtonFn; + fn?: DynamicButtonFn; emoji?: string; cantBeBusy?: boolean; style?: ButtonStyle; @@ -136,7 +142,7 @@ export class DynamicButtons { const id = murmurhash(name).toString(); this.buttons.push({ name, - id, + id: `DYN_${id}`, fn, emoji, cantBeBusy: cantBeBusy ?? false, 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 1135e1fa83..c9b47bac4f 100644 --- a/src/lib/MUser.ts +++ b/src/lib/MUser.ts @@ -1,5 +1,5 @@ -import { mentionCommand } from '@oldschoolgg/toolkit'; -import { UserError } from '@oldschoolgg/toolkit/dist/lib/UserError'; +import { cleanUsername, mentionCommand } from '@oldschoolgg/toolkit'; +import { UserError } from '@oldschoolgg/toolkit'; import type { GearSetupType, Prisma, User, UserStats, xp_gains_skill_enum } from '@prisma/client'; import { userMention } from 'discord.js'; import { calcWhatPercent, objectEntries, percentChance, sumArr, uniqueArr } from 'e'; @@ -9,13 +9,13 @@ import { EquipmentSlot } from 'oldschooljs/dist/meta/types'; import { resolveItems } from 'oldschooljs/dist/util/util'; import { timePerAlch } from '../mahoji/lib/abstracted_commands/alchCommand'; -import { userStatsUpdate } from '../mahoji/mahojiSettings'; +import { fetchUserStats, userStatsUpdate } from '../mahoji/mahojiSettings'; import { addXP } from './addXP'; 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'; @@ -30,7 +30,7 @@ import type { FarmingContract } from './minions/farming/types'; import type { AttackStyles } from './minions/functions'; import { blowpipeDarts, validateBlowpipeData } from './minions/functions/blowpipeCommand'; import type { AddXpParams, BlowpipeData, ClueBank } from './minions/types'; -import { getUsersPerkTier, syncPerkTierOfUser } from './perkTiers'; +import { getUsersPerkTier } from './perkTiers'; import { roboChimpUserFetch } from './roboChimp'; import type { MinigameScore } from './settings/minigames'; import { Minigames, getMinigameEntity } from './settings/minigames'; @@ -46,6 +46,7 @@ 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,13 +95,13 @@ export class MUserClass { gear!: UserFullGearSetup; skillsAsXP!: Required; skillsAsLevels!: Required; + badgesString!: string; + bitfield!: readonly BitField[]; constructor(user: User) { this.user = user; this.id = user.id; this.updateProperties(); - - syncPerkTierOfUser(this); } private updateProperties() { @@ -130,9 +131,13 @@ export class MUserClass { this.skillsAsXP = this.getSkills(false); this.skillsAsLevels = this.getSkills(true); + + this.badgesString = makeBadgeString(this.user.badges, this.isIronman); + + this.bitfield = this.user.bitfield as readonly BitField[]; } - countSkillsAtleast99() { + countSkillsAtLeast99() { return Object.values(this.skillsAsLevels).filter(lvl => lvl >= 99).length; } @@ -192,12 +197,8 @@ export class MUserClass { return Number(this.user.GP); } - get bitfield() { - return this.user.bitfield as readonly BitField[]; - } - - perkTier(noCheckOtherAccounts?: boolean | undefined) { - return getUsersPerkTier(this, noCheckOtherAccounts); + perkTier() { + return getUsersPerkTier(this); } skillLevel(skill: xp_gains_skill_enum) { @@ -213,23 +214,15 @@ export class MUserClass { } get rawUsername() { - return globalClient.users.cache.get(this.id)?.username ?? usernameCache.get(this.id) ?? 'Unknown'; + return cleanUsername(this.user.username ?? globalClient.users.cache.get(this.id)?.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.rawUsername; } get badgedUsername() { - return `${this.badgeString} ${this.usernameOrMention}`; + return `${this.badgesString} ${this.usernameOrMention}`.trim(); } toString() { @@ -274,13 +267,13 @@ export class MUserClass { } async calcActualClues() { - const result: { id: number; qty: number }[] = await prisma.$queryRawUnsafe(`SELECT (data->>'clueID')::int AS id, SUM((data->>'quantity')::int)::int AS qty + const result: { id: number; qty: number }[] = await prisma.$queryRawUnsafe(`SELECT (data->>'ci')::int AS id, SUM((data->>'q')::int)::int AS qty FROM activity WHERE type = 'ClueCompletion' AND user_id = '${this.id}'::bigint -AND data->>'clueID' IS NOT NULL +AND data->>'ci' IS NOT NULL AND completed = true -GROUP BY data->>'clueID';`); +GROUP BY data->>'ci';`); const casketsCompleted = new Bank(); for (const res of result) { const item = getItem(res.id); @@ -711,19 +704,7 @@ Charge your items using ${mentionCommand(globalClient, 'minion', 'charge')}.` } async fetchStats(selectKeys: T): Promise> { - const keysToSelect = Object.keys(selectKeys).length === 0 ? { user_id: true } : selectKeys; - const result = await prisma.userStats.upsert({ - where: { - user_id: BigInt(this.id) - }, - create: { - user_id: BigInt(this.id) - }, - update: {}, - select: keysToSelect - }); - - return result as unknown as SelectedUserStats; + return fetchUserStats(this.id, selectKeys); } get logName() { @@ -950,12 +931,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 678cbf969d..8f184a31fd 100644 --- a/src/lib/PaginatedMessage.ts +++ b/src/lib/PaginatedMessage.ts @@ -1,10 +1,11 @@ -import { UserError } from '@oldschoolgg/toolkit/dist/lib/UserError'; +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 88eeda2ba4..74492fa491 100644 --- a/src/lib/Task.ts +++ b/src/lib/Task.ts @@ -199,7 +199,8 @@ export async function processPendingActivities() { lt: new Date() } : undefined - } + }, + take: 5 }); if (activities.length > 0) { @@ -213,19 +214,17 @@ export async function processPendingActivities() { completed: true } }); + await Promise.all(activities.map(completeActivity)); } - - await Promise.all(activities.map(completeActivity)); - return activities; } -export async function syncActivityCache() { - const tasks = await prisma.activity.findMany({ where: { completed: false } }); +export const syncActivityCache = async () => { + const tasks = await prisma.activity.findMany({ where: { completed: false } }); minionActivityCache.clear(); for (const task of tasks) { activitySync(task); } -} +}; const ActivityTaskOptionsSchema = z.object({ userID: z.string(), @@ -237,15 +236,16 @@ const ActivityTaskOptionsSchema = z.object({ async function completeActivity(_activity: Activity) { const activity = convertStoredActivityToFlatActivity(_activity); - debugLog(`Attemping to complete activity ID[${activity.id}] TYPE[${activity.type}] USER[${activity.userID}]`); if (_activity.completed) { - throw new Error('Tried to complete an already completed task.'); + logError(new Error('Tried to complete an already completed task.')); + return; } const task = tasks.find(i => i.type === activity.type)!; if (!task) { - throw new Error('Missing task'); + logError(new Error('Missing task')); + return; } modifyBusyCounter(activity.userID, 1); @@ -254,7 +254,7 @@ async function completeActivity(_activity: Activity) { const schema = ActivityTaskOptionsSchema.and(task.dataSchema); const { success } = schema.safeParse(activity); if (!success) { - console.error(`Invalid activity data for ${activity.type} task: ${JSON.stringify(activity)}`); + logError(new Error(`Invalid activity data for ${activity.type} task: ${JSON.stringify(activity)}`)); } } await task.run(activity); @@ -263,7 +263,6 @@ async function completeActivity(_activity: Activity) { } finally { modifyBusyCounter(activity.userID, -1); minionActivityCacheDelete(activity.userID); - debugLog(`Finished completing activity ID[${activity.id}] TYPE[${activity.type}] USER[${activity.userID}]`); } } diff --git a/src/lib/analytics.ts b/src/lib/analytics.ts index fafe274582..ee966f8cb9 100644 --- a/src/lib/analytics.ts +++ b/src/lib/analytics.ts @@ -33,9 +33,6 @@ async function calculateMinionTaskCounts() { } export async function analyticsTick() { - debugLog('Analytics tick', { - type: 'ANALYTICS_TICK' - }); const [numberOfMinions, totalSacrificed, numberOfIronmen, totalGP] = ( await Promise.all( [ @@ -48,7 +45,7 @@ export async function analyticsTick() { ).map((result: any) => Number.parseInt(result[0].count)) as number[]; const taskCounts = await calculateMinionTaskCounts(); - const currentClientSettings = await await prisma.clientStorage.findFirst({ + const currentClientSettings = await prisma.clientStorage.upsert({ where: { id: globalConfig.clientID }, @@ -67,9 +64,12 @@ export async function analyticsTick() { gp_slots: true, gp_tax_balance: true, economyStats_dailiesAmount: true - } + }, + create: { + id: globalConfig.clientID + }, + update: {} }); - if (!currentClientSettings) throw new Error('No client settings found'); await prisma.analytic.create({ data: { guildsCount: globalClient.guilds.cache.size, diff --git a/src/lib/badges.ts b/src/lib/badges.ts index 7a08a72786..bcf839069a 100644 --- a/src/lib/badges.ts +++ b/src/lib/badges.ts @@ -24,6 +24,6 @@ export async function cacheBadges() { newCache.set(user.RSN.toLowerCase(), userBadges.join(' ')); } - globalClient._badgeCache.clear(); + globalClient._badgeCache?.clear(); globalClient._badgeCache = newCache; } diff --git a/src/lib/bankImage.ts b/src/lib/bankImage.ts index 57457c9bd4..9e1662a5b9 100644 --- a/src/lib/bankImage.ts +++ b/src/lib/bankImage.ts @@ -4,7 +4,7 @@ import * as path from 'node:path'; import type { SKRSContext2D } from '@napi-rs/canvas'; import { Canvas, GlobalFonts, Image, loadImage } from '@napi-rs/canvas'; import { cleanString, formatItemStackQuantity, generateHexColorForCashStack } from '@oldschoolgg/toolkit'; -import { UserError } from '@oldschoolgg/toolkit/dist/lib/UserError'; +import { UserError } from '@oldschoolgg/toolkit'; import { AttachmentBuilder } from 'discord.js'; import { chunk, randInt, sumArr } from 'e'; import fetch from 'node-fetch'; @@ -642,8 +642,6 @@ export class BankImageTask { let items = bank.items(); - debugLog(`Generating a bank image with ${items.length} items`, { title, userID: user?.id }); - // Sorting const favorites = user?.user.favoriteItems; const weightings = user?.user.bank_sort_weightings as ItemBank; diff --git a/src/lib/blacklists.ts b/src/lib/blacklists.ts index a5ee835942..278e74d6af 100644 --- a/src/lib/blacklists.ts +++ b/src/lib/blacklists.ts @@ -1,12 +1,11 @@ import { Time } from 'e'; -import { production } from '../config'; +import { TimerManager } from '@sapphire/timer-manager'; export const BLACKLISTED_USERS = new Set(); export const BLACKLISTED_GUILDS = new Set(); export async function syncBlacklists() { - debugLog('Syncing blacklists'); const blacklistedEntities = await roboChimpClient.blacklistedEntity.findMany(); BLACKLISTED_USERS.clear(); BLACKLISTED_GUILDS.clear(); @@ -16,6 +15,4 @@ export async function syncBlacklists() { } } -if (production) { - setInterval(syncBlacklists, Time.Minute * 10); -} +TimerManager.setInterval(syncBlacklists, Time.Minute * 10); diff --git a/src/lib/collectionLogTask.ts b/src/lib/collectionLogTask.ts index de805a1591..21520befab 100644 --- a/src/lib/collectionLogTask.ts +++ b/src/lib/collectionLogTask.ts @@ -6,12 +6,12 @@ import { calcWhatPercent, objectEntries } from 'e'; import type { Bank } from 'oldschooljs'; import { Util } from 'oldschooljs'; -import type { UserStatsDataNeededForCL } from '../lib/data/Collections'; import { allCollectionLogs, getCollection, getTotalCl } from '../lib/data/Collections'; import type { IToReturnCollection } from '../lib/data/CollectionsExport'; import { fillTextXTimesInCtx, getClippedRegion, measureTextWidth } from '../lib/util/canvasUtil'; import getOSItem from '../lib/util/getOSItem'; import type { IBgSprite } from './bankImage'; +import type { MUserStats } from './structures/MUserStats'; export const collectionLogTypes = [ { name: 'collection', description: 'Normal Collection Log' }, @@ -100,7 +100,7 @@ class CollectionLogTask { collection: string; type: CollectionLogType; flags: { [key: string]: string | number | undefined }; - stats: UserStatsDataNeededForCL | null; + stats: MUserStats | null; collectionLog?: IToReturnCollection; }): Promise { const { sprite } = bankImageGenerator.getBgAndSprite(options.user.user.bankBackground, options.user); @@ -194,8 +194,6 @@ class CollectionLogTask { ) ); - debugLog('Generating a CL image', { collection, ...flags, type, user_id: user.id }); - // Create base canvas const canvas = new Canvas(canvasWidth, canvasHeight); // Get the canvas context diff --git a/src/lib/colosseum.ts b/src/lib/colosseum.ts index 7badf5c38a..2a37ae359e 100644 --- a/src/lib/colosseum.ts +++ b/src/lib/colosseum.ts @@ -1,6 +1,6 @@ import { exponentialPercentScale, formatDuration, mentionCommand } from '@oldschoolgg/toolkit'; -import { GeneralBank, type GeneralBankType } from '@oldschoolgg/toolkit/dist/lib/GeneralBank'; -import { UserError } from '@oldschoolgg/toolkit/dist/lib/UserError'; +import { UserError } from '@oldschoolgg/toolkit'; +import { GeneralBank, type GeneralBankType } from '@oldschoolgg/toolkit'; import { Time, calcPercentOfNum, @@ -366,6 +366,9 @@ interface ColosseumResult { realDuration: number; totalDeathChance: number; deathChances: number[]; + scytheCharges: number; + venatorBowCharges: number; + bloodFuryCharges: number; } export const startColosseumRun = (options: { @@ -377,6 +380,9 @@ export const startColosseumRun = (options: { hasClaws: boolean; hasSGS: boolean; hasTorture: boolean; + scytheCharges: number; + venatorBowCharges: number; + bloodFuryCharges: number; }): ColosseumResult => { const waveTwelveKC = options.kcBank.amount(12); @@ -406,6 +412,10 @@ export const startColosseumRun = (options: { let realDuration = 0; let maxGlory = 0; + // Calculate charges used + const scytheCharges = 300; + const calculateVenCharges = () => 50; + for (const wave of colosseumWaves) { realDuration += waveDuration; const kcForThisWave = options.kcBank.amount(wave.waveNumber); @@ -422,7 +432,10 @@ export const startColosseumRun = (options: { fakeDuration, realDuration, totalDeathChance: combinedChance(deathChances), - deathChances + deathChances, + scytheCharges: options.hasScythe ? scytheCharges : 0, + venatorBowCharges: options.hasVenBow ? calculateVenCharges() : 0, + bloodFuryCharges: options.hasBF ? scytheCharges * 3 : 0 }; } addedWaveKCBank.add(wave.waveNumber); @@ -436,7 +449,11 @@ export const startColosseumRun = (options: { fakeDuration, realDuration, totalDeathChance: combinedChance(deathChances), - deathChances + deathChances, + + scytheCharges: options.hasScythe ? scytheCharges : 0, + venatorBowCharges: options.hasVenBow ? calculateVenCharges() : 0, + bloodFuryCharges: options.hasBF ? scytheCharges * 3 : 0 }; } } @@ -532,6 +549,9 @@ export async function colosseumCommand(user: MUser, channelID: string) { const hasClaws = user.hasEquippedOrInBank('Dragon claws'); const hasSGS = user.hasEquippedOrInBank('Saradomin godsword'); const hasTorture = !hasBF && user.gear.melee.hasEquipped('Amulet of torture'); + const scytheCharges = 300; + const bloodFuryCharges = scytheCharges * 3; + const venatorBowCharges = calculateVenCharges(); const res = startColosseumRun({ kcBank: new ColosseumWaveBank((await user.fetchStats({ colo_kc_bank: true })).colo_kc_bank as ItemBank), @@ -541,7 +561,10 @@ export async function colosseumCommand(user: MUser, channelID: string) { hasBF, hasClaws, hasSGS, - hasTorture + hasTorture, + scytheCharges, + venatorBowCharges, + bloodFuryCharges }); const minutes = res.realDuration / Time.Minute; @@ -556,7 +579,6 @@ export async function colosseumCommand(user: MUser, channelID: string) { return 'You need to have a Ranging potion(4) or Bastion potion(4) in your bank.'; } - const scytheCharges = 300; if (hasScythe) { messages.push('10% boost for Scythe'); chargeBank.add('scythe_of_vitur_charges', scytheCharges); @@ -578,7 +600,7 @@ export async function colosseumCommand(user: MUser, channelID: string) { cost.add('Dragon arrow', 50); } else { messages.push( - 'Missed 7% Venator bow boost. If you have one, charge it and keep it in your bank. You also need atleast 50 dragon arrows equipped.' + 'Missed 7% Venator bow boost. If you have one, charge it and keep it in your bank. You also need at least 50 dragon arrows equipped.' ); } @@ -595,7 +617,7 @@ export async function colosseumCommand(user: MUser, channelID: string) { } if (user.gear.melee.hasEquipped('Amulet of blood fury')) { - chargeBank.add('blood_fury_charges', scytheCharges * 3); + chargeBank.add('blood_fury_charges', bloodFuryCharges); messages.push('-5% death chance for blood fury'); } else { messages.push('Missed -5% death chance for blood fury. If you have one, add charges and equip it to melee.'); @@ -620,7 +642,7 @@ export async function colosseumCommand(user: MUser, channelID: string) { messages.push(`Removed ${realCost}`); await updateBankSetting('colo_cost', realCost); - await userStatsBankUpdate(user.id, 'colo_cost', realCost); + await userStatsBankUpdate(user, 'colo_cost', realCost); await trackLoot({ totalCost: realCost, id: 'colo', @@ -652,7 +674,10 @@ export async function colosseumCommand(user: MUser, channelID: string) { fakeDuration: res.fakeDuration, maxGlory: res.maxGlory, diedAt: res.diedAt ?? undefined, - loot: res.loot?.bank + loot: res.loot?.bank, + scytheCharges: res.scytheCharges, + venatorBowCharges: res.venatorBowCharges, + bloodFuryCharges: res.bloodFuryCharges }); return `${user.minionName} is now attempting the Colosseum. They will finish in around ${formatDuration( diff --git a/src/lib/combat_achievements/caUtils.ts b/src/lib/combat_achievements/caUtils.ts index a0f68d9191..fee5ce5feb 100644 --- a/src/lib/combat_achievements/caUtils.ts +++ b/src/lib/combat_achievements/caUtils.ts @@ -4,7 +4,7 @@ import type { CombatAchievement } from './combatAchievements'; export function isCertainMonsterTrip(monsterID: number) { return (data: ActivityTaskData) => - data.type === 'MonsterKilling' && (data as MonsterActivityTaskOptions).monsterID === monsterID; + data.type === 'MonsterKilling' && (data as MonsterActivityTaskOptions).mi === monsterID; } interface CombatAchievementGroup { diff --git a/src/lib/combat_achievements/combatAchievements.ts b/src/lib/combat_achievements/combatAchievements.ts index cb34e45c40..5aef785fb0 100644 --- a/src/lib/combat_achievements/combatAchievements.ts +++ b/src/lib/combat_achievements/combatAchievements.ts @@ -159,16 +159,16 @@ assert(allCATaskIDs.length === new Set(allCATaskIDs).size); assert(sumArr(Object.values(CombatAchievements).map(i => i.length)) === allCATaskIDs.length); const indexesWithRng = entries.flatMap(i => i[1].tasks.filter(t => 'rng' in t)); -export const combatAchievementTripEffect: TripFinishEffect['fn'] = async ({ data, messages }) => { +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') { @@ -183,8 +183,8 @@ export const combatAchievementTripEffect: TripFinishEffect['fn'] = async ({ data } } - const users = await Promise.all( - ('users' in data ? (data.users as string[]) : [data.userID]).map(id => mUserFetch(id)) + const users: MUser[] = await Promise.all( + 'users' in data ? (data.users as string[]).map(id => mUserFetch(id)) : [user] ); for (const user of users) { diff --git a/src/lib/combat_achievements/elite.ts b/src/lib/combat_achievements/elite.ts index 084a385769..480ef478b5 100644 --- a/src/lib/combat_achievements/elite.ts +++ b/src/lib/combat_achievements/elite.ts @@ -8,10 +8,10 @@ import { ZALCANO_ID, demonBaneWeapons } from '../constants'; -import { anyoneDiedInTOARaid } from '../simulation/toa'; import { SkillsEnum } from '../skilling/types'; import { Requirements } from '../structures/Requirements'; import type { ActivityTaskData, GauntletOptions, NightmareActivityTaskOptions, TOAOptions } from '../types/minions'; +import { anyoneDiedInTOARaid } from '../util'; import { isCertainMonsterTrip } from './caUtils'; import type { CombatAchievement } from './combatAchievements'; diff --git a/src/lib/combat_achievements/grandmaster.ts b/src/lib/combat_achievements/grandmaster.ts index 8736e99597..e0a689ed4e 100644 --- a/src/lib/combat_achievements/grandmaster.ts +++ b/src/lib/combat_achievements/grandmaster.ts @@ -2,7 +2,6 @@ import { Time } from 'e'; import { Monsters } from 'oldschooljs'; import { PHOSANI_NIGHTMARE_ID } from '../constants'; -import { anyoneDiedInTOARaid } from '../simulation/toa'; import { Requirements } from '../structures/Requirements'; import type { ActivityTaskData, @@ -13,6 +12,7 @@ import type { TOAOptions, TheatreOfBloodTaskOptions } from '../types/minions'; +import { anyoneDiedInTOARaid } from '../util'; import { isCertainMonsterTrip } from './caUtils'; import type { CombatAchievement } from './combatAchievements'; 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/master.ts b/src/lib/combat_achievements/master.ts index a364a74d4e..9078a837fa 100644 --- a/src/lib/combat_achievements/master.ts +++ b/src/lib/combat_achievements/master.ts @@ -3,7 +3,6 @@ import { Monsters } from 'oldschooljs'; import { resolveItems } from 'oldschooljs/dist/util/util'; import { NEX_ID, NIGHTMARE_ID, PHOSANI_NIGHTMARE_ID } from '../constants'; -import { anyoneDiedInTOARaid } from '../simulation/toa'; import { Requirements } from '../structures/Requirements'; import type { ActivityTaskData, @@ -14,6 +13,7 @@ import type { TOAOptions, TheatreOfBloodTaskOptions } from '../types/minions'; +import { anyoneDiedInTOARaid } from '../util'; import { isCertainMonsterTrip } from './caUtils'; import type { CombatAchievement } from './combatAchievements'; @@ -1403,7 +1403,7 @@ export const masterCombatAchievements: CombatAchievement[] = [ rng: { chancePerKill: 33, hasChance: data => - isCertainMonsterTrip(Monsters.Vorkath.id)(data) && (data as MonsterActivityTaskOptions).quantity >= 5 + isCertainMonsterTrip(Monsters.Vorkath.id)(data) && (data as MonsterActivityTaskOptions).q >= 5 } }, { 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 5998a1ca5d..cb7b989f77 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -1,5 +1,6 @@ import { execSync } from 'node:child_process'; import path from 'node:path'; +import { isMainThread } from 'node:worker_threads'; import type { Image } from '@napi-rs/canvas'; import { PerkTier, SimpleTable, StoreBitfield, dateFm } from '@oldschoolgg/toolkit'; import type { CommandOptions } from '@oldschoolgg/toolkit'; @@ -19,6 +20,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', @@ -196,7 +198,6 @@ export enum BitField { IsPatronTier4 = 5, IsPatronTier5 = 6, isModerator = 7, - isContributor = 8, BypassAgeRestriction = 9, HasHosidiusWallkit = 10, HasPermanentEventBackgrounds = 11, @@ -207,7 +208,6 @@ export enum BitField { HasDexScroll = 16, HasArcaneScroll = 17, HasTornPrayerScroll = 18, - IsWikiContributor = 19, HasSlepeyTablet = 20, IsPatronTier6 = 21, DisableBirdhouseRunButton = 22, @@ -243,9 +243,7 @@ interface BitFieldData { } export const BitFieldData: Record = { - [BitField.IsWikiContributor]: { name: 'Wiki Contributor', protected: true, userConfigurable: false }, [BitField.isModerator]: { name: 'Moderator', protected: true, userConfigurable: false }, - [BitField.isContributor]: { name: 'Contributor', protected: true, userConfigurable: false }, [BitField.HasPermanentTierOne]: { name: 'Permanent Tier 1', protected: false, userConfigurable: false }, [BitField.IsPatronTier1]: { name: 'Tier 1 Patron', protected: false, userConfigurable: false }, @@ -346,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 } = { @@ -362,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; @@ -478,8 +480,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') @@ -533,7 +533,10 @@ const globalConfigSchema = z.object({ geAdminChannelID: z.string().default(''), redisPort: z.coerce.number().int().optional(), botToken: z.string().min(1), - isCI: z.coerce.boolean().default(false) + isCI: z.coerce.boolean().default(false), + isProduction: z.coerce.boolean().default(production), + testingServerID: z.string(), + timeZone: z.literal('UTC') }); dotenv.config({ path: path.resolve(process.cwd(), process.env.TEST ? '.env.test' : '.env') }); @@ -543,14 +546,24 @@ 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 + isCI: process.env.CI, + isProduction, + testingServerID: process.env.TESTING_SERVER_ID ?? OLDSCHOOLGG_TESTING_SERVER_ID, + timeZone: process.env.TZ }); +if ((process.env.NODE_ENV === 'production') !== globalConfig.isProduction || production !== globalConfig.isProduction) { + throw new Error('The NODE_ENV and isProduction variables must match'); +} + export const ONE_TRILLION = 1_000_000_000_000; export const demonBaneWeapons = resolveItems(['Silverlight', 'Darklight', 'Arclight']); @@ -614,3 +627,9 @@ export const winterTodtPointsTable = new SimpleTable() .add(750) .add(780) .add(850); + +if (!process.env.TEST && isMainThread) { + console.log( + `Starting... Git[${gitHash}] ClientID[${globalConfig.clientID}] Production[${globalConfig.isProduction}]` + ); +} diff --git a/src/lib/crons.ts b/src/lib/crons.ts index 4480f110f7..ae104c06c5 100644 --- a/src/lib/crons.ts +++ b/src/lib/crons.ts @@ -1,7 +1,7 @@ import { schedule } from 'node-cron'; import { analyticsTick } from './analytics'; - +import { cacheGEPrices } from './marketPrices'; import { cacheCleanup } from './util/cachedUserIDs'; export function initCrons() { @@ -9,9 +9,6 @@ export function initCrons() { * Capture economy item data */ schedule('0 */6 * * *', async () => { - debugLog('Economy Item Insert', { - type: 'INSERT_ECONOMY_ITEM' - }); await prisma.$queryRawUnsafe(`INSERT INTO economy_item SELECT item_id::integer, SUM(qty)::bigint FROM ( @@ -33,7 +30,6 @@ GROUP BY item_id;`); * prescence */ schedule('0 * * * *', () => { - debugLog('Set Activity cronjob starting'); globalClient.user?.setActivity('/help'); }); @@ -41,7 +37,11 @@ GROUP BY item_id;`); * Delete all voice channels */ schedule('0 0 */1 * *', async () => { - debugLog('Cache cleanup cronjob starting'); cacheCleanup(); }); + + schedule('35 */48 * * *', async () => { + debugLog('cacheGEPrices cronjob starting'); + await cacheGEPrices(); + }); } diff --git a/src/lib/data/Collections.ts b/src/lib/data/Collections.ts index abe96d255e..7e3a4c1f29 100644 --- a/src/lib/data/Collections.ts +++ b/src/lib/data/Collections.ts @@ -21,9 +21,8 @@ import type { MinigameName } from '../settings/minigames'; import { NexNonUniqueTable, NexUniqueTable } from '../simulation/misc'; import { allFarmingItems } from '../skilling/skills/farming'; import { SkillsEnum } from '../skilling/types'; -import type { MUserStats } from '../structures/MUserStats'; +import { MUserStats } from '../structures/MUserStats'; import type { ItemBank } from '../types'; -import { fetchStatsForCL } from '../util/fetchStatsForCL'; import { shuffleRandom } from '../util/smallUtils'; import type { FormatProgressFunction, ICollection, ILeftListStatus, IToReturnCollection } from './CollectionsExport'; import { @@ -329,7 +328,8 @@ export const allCollectionLogs: ICollection = { 'Sunfire fanatic helm', 'Echo crystal', 'Tonalztics of ralos (uncharged)', - 'Sunfire splinters' + 'Sunfire splinters', + 'Uncut onyx' ]), fmtProg: ({ minigames }) => `${minigames.colosseum} KC` }, @@ -558,9 +558,9 @@ export const allCollectionLogs: ICollection = { kcActivity: { Default: async (_, minigameScores) => minigameScores.find(i => i.minigame.column === 'tombs_of_amascut')!.score, - Entry: async (_, __, { stats }) => stats.getToaKCs().entryKC, - Normal: async (_, __, { stats }) => stats.getToaKCs().normalKC, - Expert: async (_, __, { stats }) => stats.getToaKCs().expertKC + Entry: async (_, __, stats) => stats.getToaKCs().entryKC, + Normal: async (_, __, stats) => stats.getToaKCs().normalKC, + Expert: async (_, __, stats) => stats.getToaKCs().expertKC }, items: toaCL, isActivity: true, @@ -1242,22 +1242,7 @@ function getLeftList(userBank: Bank, checkCategory: string, allItems = false, re return leftList; } -export interface UserStatsDataNeededForCL { - sacrificedBank: Bank; - titheFarmsCompleted: number; - lapsScores: ItemBank; - openableScores: Bank; - kcBank: ItemBank; - highGambles: number; - gotrRiftSearches: number; - stats: MUserStats; -} - -function getBank( - user: MUser, - type: 'sacrifice' | 'bank' | 'collection' | 'temp', - userStats: UserStatsDataNeededForCL | MUserStats | null -) { +function getBank(user: MUser, type: 'sacrifice' | 'bank' | 'collection' | 'temp', userStats: MUserStats | null) { switch (type) { case 'collection': return new Bank(user.cl); @@ -1275,7 +1260,7 @@ function getBank( export function getTotalCl( user: MUser, logType: 'sacrifice' | 'bank' | 'collection' | 'temp', - userStats: UserStatsDataNeededForCL | MUserStats | null + userStats: MUserStats | null ) { return getUserClData(getBank(user, logType, userStats).bank, allCLItemsFiltered); } @@ -1365,7 +1350,7 @@ export async function getCollection(options: { if (logType === undefined) logType = 'collection'; const minigameScores = await user.fetchMinigameScores(); - const userStats = await fetchStatsForCL(user); + const userStats = await MUserStats.fromID(user.id); const userCheckBank = getBank(user, logType, userStats); let clItems = getCollectionItems(search, allItems, logType === 'sacrifice'); diff --git a/src/lib/data/CollectionsExport.ts b/src/lib/data/CollectionsExport.ts index c1881b77bd..60363b980f 100644 --- a/src/lib/data/CollectionsExport.ts +++ b/src/lib/data/CollectionsExport.ts @@ -7,8 +7,8 @@ import { resolveItems } from 'oldschooljs/dist/util/util'; import { growablePets } from '../growablePets'; import { implings } from '../implings'; import type { MinigameScore } from '../settings/minigames'; +import type { MUserStats } from '../structures/MUserStats'; import getOSItem from '../util/getOSItem'; -import type { UserStatsDataNeededForCL } from './Collections'; import { gracefulCapes, gracefulFeet, @@ -39,7 +39,7 @@ export interface IKCActivity { [key: string]: | string | string[] - | ((user: MUser, minigameScores: MinigameScore[], stats: UserStatsDataNeededForCL) => Promise); + | ((user: MUser, minigameScores: MinigameScore[], stats: MUserStats) => Promise); } export type FormatProgressFunction = ({ @@ -51,7 +51,7 @@ export type FormatProgressFunction = ({ user: MUser; getKC: (id: number) => Promise; minigames: Minigame; - stats: UserStatsDataNeededForCL; + stats: MUserStats; }) => string | string[] | Promise; interface ICollectionActivity { @@ -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/data/buyables/capes.ts b/src/lib/data/buyables/capes.ts index 480434fd71..7e672fa86c 100644 --- a/src/lib/data/buyables/capes.ts +++ b/src/lib/data/buyables/capes.ts @@ -3,6 +3,7 @@ import { Bank } from 'oldschooljs'; import { diaries, userhasDiaryTier } from '../../diaries'; import { MAX_QP } from '../../minions/data/quests'; import { musicCapeRequirements } from '../../musicCape'; +import { Requirements } from '../../structures/Requirements'; import type { Buyable } from './buyables'; export const capeBuyables: Buyable[] = [ @@ -78,7 +79,7 @@ export const capeBuyables: Buyable[] = [ }), gpCost: 99_000, customReq: async user => { - const meetsReqs = await musicCapeRequirements.check(user); + const meetsReqs = await musicCapeRequirements.check(await Requirements.fetchRequiredData(user)); if (!meetsReqs.hasAll) { return [false, `You don't meet the requirements to buy this: \n${meetsReqs.rendered}`]; } @@ -92,13 +93,12 @@ export const capeBuyables: Buyable[] = [ }), gpCost: 99_000, customReq: async user => { - const meetsReqs = await musicCapeRequirements.check(user); - if (!meetsReqs.hasAll) { - return [false, `You don't meet the requirements to buy this: \n${meetsReqs.rendered}`]; - } if (user.QP < MAX_QP) { return [false, "You can't buy this because you haven't completed all the quests!"]; } + if (!user.cl.has('Music cape')) { + return [false, 'You need to own the regular Music cape first.']; + } for (const diary of diaries.map(d => d.elite)) { const [has] = await userhasDiaryTier(user, diary); if (!has) { diff --git a/src/lib/data/buyables/skillCapeBuyables.ts b/src/lib/data/buyables/skillCapeBuyables.ts index d6061f7a9f..fddc4fd69c 100644 --- a/src/lib/data/buyables/skillCapeBuyables.ts +++ b/src/lib/data/buyables/skillCapeBuyables.ts @@ -12,7 +12,7 @@ for (const skillcape of Skillcapes) { outputItems: (user: MUser) => { const output = new Bank().add(skillcape.hood); - if (user.countSkillsAtleast99() > 1) { + if (user.countSkillsAtLeast99() > 1) { output.add(skillcape.trimmed); } else { output.add(skillcape.untrimmed); diff --git a/src/lib/data/cox.ts b/src/lib/data/cox.ts index 05a7df1fc7..912f2ecf42 100644 --- a/src/lib/data/cox.ts +++ b/src/lib/data/cox.ts @@ -240,11 +240,11 @@ export const minimumCoxSuppliesNeeded = new Bank({ export async function checkCoxTeam(users: MUser[], cm: boolean, quantity = 1): Promise { const hasHerbalist = users.some(u => u.skillLevel(SkillsEnum.Herblore) >= 78); if (!hasHerbalist) { - return 'nobody with atleast level 78 Herblore'; + return 'nobody with at least level 78 Herblore'; } const hasFarmer = users.some(u => u.skillLevel(SkillsEnum.Farming) >= 55); if (!hasFarmer) { - return 'nobody with atleast level 55 Farming'; + return 'nobody with at least level 55 Farming'; } const suppliesNeeded = minimumCoxSuppliesNeeded.clone().multiply(quantity); const userWithoutSupplies = users.find(u => !u.bank.has(suppliesNeeded)); diff --git a/src/lib/data/itemAliases.ts b/src/lib/data/itemAliases.ts index 33aa46bc96..557bd0b85f 100644 --- a/src/lib/data/itemAliases.ts +++ b/src/lib/data/itemAliases.ts @@ -1,11 +1,10 @@ -import { modifyItem } from '@oldschoolgg/toolkit'; -import { Items } from 'oldschooljs'; +import { deepMerge, modifyItem } from '@oldschoolgg/toolkit'; +import { omit } from 'lodash'; +import { EItem, Items } from 'oldschooljs'; import { allTeamCapes } from 'oldschooljs/dist/data/itemConstants'; import { itemNameMap } from 'oldschooljs/dist/structures/Items'; import { cleanString } from 'oldschooljs/dist/util/cleanString'; -import { resolveItems } from 'oldschooljs/dist/util/util'; - -import { getOSItem } from '../util/getOSItem'; +import { getItemOrThrow, resolveItems } from 'oldschooljs/dist/util/util'; export function setItemAlias(id: number, name: string | string[], rename = true) { const existingItem = Items.get(id); @@ -179,12 +178,15 @@ setItemAlias(2993, 'Chompy bird hat (dragon archer)'); setItemAlias(2994, 'Chompy bird hat (expert ogre dragon archer)'); setItemAlias(2995, 'Chompy bird hat (expert dragon archer)'); -// Item aliases +// Achievement diary lamps setItemAlias(11_137, 'Antique lamp 1'); setItemAlias(11_139, 'Antique lamp 2'); setItemAlias(11_141, 'Antique lamp 3'); setItemAlias(11_185, 'Antique lamp 4'); +// Defender of varrock quest lamp +setItemAlias(28_820, 'Antique lamp (defender of varrock)'); + // Dragonfire shields setItemAlias(11_284, 'Uncharged dragonfire shield'); setItemAlias(11_283, 'Dragonfire shield'); @@ -386,7 +388,32 @@ for (const item of allTeamCapes) { modifyItem(item.id, { price: 100 }); - if (getOSItem(item.id).price !== 100) { + if (getItemOrThrow(item.id).price !== 100) { throw new Error(`Failed to modify price of item ${item.id}`); } } + +export const itemDataSwitches = [ + { + from: 25488, + to: EItem.BELLATOR_RING + }, + { + from: 25486, + to: EItem.MAGUS_RING + }, + { + from: 25487, + to: EItem.VENATOR_RING + }, + { + from: 25485, + to: EItem.ULTOR_RING + } +]; + +for (const items of itemDataSwitches) { + const from = getItemOrThrow(items.from); + const to = getItemOrThrow(items.to); + modifyItem(to.id, deepMerge(omit(to, 'id'), omit(from, 'id'))); +} diff --git a/src/lib/degradeableItems.ts b/src/lib/degradeableItems.ts index 47807b39d8..42a0d37496 100644 --- a/src/lib/degradeableItems.ts +++ b/src/lib/degradeableItems.ts @@ -61,6 +61,13 @@ interface DegradeableItemPVMBoost { boost: number; } +interface RefundResult { + item: Item; + refundedCharges: number; + totalCharges: number; + userMessage: string; +} + export const degradeableItems: DegradeableItem[] = [ { item: getOSItem('Abyssal tentacle'), @@ -389,10 +396,8 @@ export async function degradeItem({ const chargesAfter = user.user[degItem.settingsKey]; assert(typeof chargesAfter === 'number' && chargesAfter > 0); return { - userMessage: `Your ${ - item.name - } degraded by ${chargesToDegrade} charges, and now has ${chargesAfter} remaining.${ - pennyReduction > 0 ? ` Your Ghommal's lucky penny saved ${pennyReduction} charges` : '' + userMessage: `Your ${item.name} degraded by ${chargesToDegrade} charges, and now has ${chargesAfter} remaining${ + pennyReduction > 0 ? `. Your Ghommal's lucky penny saved ${pennyReduction} charges` : '' }` }; } @@ -426,3 +431,38 @@ export async function degradeChargeBank(user: MUser, chargeBank: ChargeBank) { return results; } + +export async function refundChargeBank(user: MUser, chargeBank: ChargeBank): Promise { + const results: RefundResult[] = []; + + for (const [key, chargesToRefund] of chargeBank.entries()) { + const degItem = degradeableItems.find(i => i.settingsKey === key); + if (!degItem) { + throw new Error(`Invalid degradeable item settings key: ${key}`); + } + + const currentCharges = user.user[degItem.settingsKey]; + const newCharges = currentCharges + chargesToRefund; + + // Prepare result message + const userMessage = `Refunded ${chargesToRefund} charges for ${degItem.item.name}.`; + + // Create result object + const result: RefundResult = { + item: degItem.item, + refundedCharges: chargesToRefund, + totalCharges: newCharges, + userMessage + }; + + // Push result to results array + results.push(result); + + // Update user + await user.update({ + [degItem.settingsKey]: newCharges + }); + } + + return results; +} diff --git a/src/lib/diaries.ts b/src/lib/diaries.ts index 267bfbc58d..861361f43d 100644 --- a/src/lib/diaries.ts +++ b/src/lib/diaries.ts @@ -1,11 +1,12 @@ import { objectEntries } from 'e'; import { Monsters } from 'oldschooljs'; +import type { Minigame } from '@prisma/client'; import { resolveItems } from 'oldschooljs/dist/util/util'; import { MAX_QP } from './minions/data/quests'; import type { DiaryTier, DiaryTierName } from './minions/types'; import { DiaryID } from './minions/types'; -import type { MinigameScore } from './settings/minigames'; +import { Minigames } from './settings/minigames'; import Skillcapes from './skilling/skillcapes'; import { courses } from './skilling/skills/agility'; import { MUserStats } from './structures/MUserStats'; @@ -25,9 +26,10 @@ interface Diary { export function userhasDiaryTierSync( user: MUser, - tier: DiaryTier, - data: { stats: MUserStats; minigameScores: MinigameScore[] } -): [true] | [false, string] { + _tier: DiaryTier | [DiaryID, DiaryTierName], + data: { stats: MUserStats; minigameScores: Minigame } +): { hasDiary: boolean; reasons: string; diaryGroup: Diary; tier: DiaryTier } { + const tier = Array.isArray(_tier) ? diaries.find(d => d.id === _tier[0])![_tier[1]] : _tier; const [hasReqs] = hasSkillReqs(user, tier.skillReqs); const skills = user.skillsAsLevels; let canDo = true; @@ -69,13 +71,13 @@ export function userhasDiaryTierSync( } if (tier.minigameReqs) { - const entries = Object.entries(tier.minigameReqs); + const entries = objectEntries(tier.minigameReqs); for (const [key, neededScore] of entries) { - const thisScore = data.minigameScores.find(m => m.minigame.column === key)!; - if (thisScore.score < neededScore!) { + const thisScore = data.minigameScores[key]!; + if (thisScore < neededScore!) { canDo = false; reasons.push( - `You don't have **${neededScore}** KC in **${thisScore.minigame.name}**, you have **${thisScore.score}**` + `You don't have **${neededScore}** KC in **${Minigames.find(m => m.column === key)!.name}**, you have **${thisScore}**` ); } } @@ -114,15 +116,23 @@ export function userhasDiaryTierSync( } } - if (canDo) return [true]; - return [canDo, reasons.join('\n- ')]; + return { + hasDiary: canDo, + reasons: reasons.join('\n- '), + tier, + diaryGroup: diaries.find(d => [d.easy, d.medium, d.hard, d.elite].includes(tier))! + }; } -export async function userhasDiaryTier(user: MUser, tier: DiaryTier): Promise<[true] | [false, string]> { - return userhasDiaryTierSync(user, tier, { +export async function userhasDiaryTier( + user: MUser, + tier: [DiaryID, DiaryTierName] | DiaryTier +): Promise<[boolean, string, Diary]> { + const result = userhasDiaryTierSync(user, tier, { stats: await MUserStats.fromID(user.id), - minigameScores: await user.fetchMinigameScores() + minigameScores: await user.fetchMinigames() }); + return [result.hasDiary, result.reasons, result.diaryGroup]; } export const WesternProv: Diary = { @@ -458,13 +468,7 @@ export const FaladorDiary: Diary = { woodcutting: 71 }, qp: 32, - collectionLogReqs: resolveItems([ - 'Mind rune', - 'Prospector jacket', - 'Prospector helmet', - 'Prospector legs', - 'Prospector boots' - ]), + collectionLogReqs: resolveItems(['Mind rune', 'Prospector helmet']), monsterScores: { 'Skeletal Wyvern': 1, 'Blue Dragon': 1 @@ -1130,16 +1134,9 @@ export const diariesObject = { } as const; export const diaries = Object.values(diariesObject); -export async function userhasDiaryIDTier(user: MUser, type: DiaryID, tier: DiaryTierName) { - const diaryGroup = diaries.find(d => d.id === type)!; - const diaryTier = diaryGroup[tier]!; - const [hasDiary] = userhasDiaryTierSync(user, diaryTier, { +export async function userhasDiaryIDTier(user: MUser, diaryID: DiaryID, tier: DiaryTierName) { + return userhasDiaryTierSync(user, [diaryID, tier], { stats: await MUserStats.fromID(user.id), - minigameScores: await user.fetchMinigameScores() + minigameScores: await user.fetchMinigames() }); - return { - hasDiary, - diaryGroup, - diaryTier - }; } diff --git a/src/lib/events.ts b/src/lib/events.ts index 2eb6828f55..7a383eeb4e 100644 --- a/src/lib/events.ts +++ b/src/lib/events.ts @@ -1,11 +1,12 @@ import { channelIsSendable, mentionCommand } from '@oldschoolgg/toolkit'; -import { UserError } from '@oldschoolgg/toolkit/dist/lib/UserError'; +import { UserError } from '@oldschoolgg/toolkit'; import type { BaseMessageOptions, Message, TextChannel } from 'discord.js'; import { ButtonBuilder, ButtonStyle, EmbedBuilder, bold } from 'discord.js'; import { Time, isFunction, roll } from 'e'; import { LRUCache } from 'lru-cache'; import { Items } from 'oldschooljs'; +import { command_name_enum } from '@prisma/client'; import { SupportServer, production } from '../config'; import { untrustedGuildSettingsCache } from '../mahoji/guildSettings'; import { minionStatusCommand } from '../mahoji/lib/abstracted_commands/minionStatusCommand'; @@ -42,6 +43,8 @@ const rareRolesSrc: [string, number, string][] = [ const userCache = new LRUCache({ max: 1000 }); function rareRoles(msg: Message) { + if (!globalConfig.isProduction) return; + if (!msg.guild || msg.guild.id !== SupportServer) { return; } @@ -140,7 +143,7 @@ interface MentionCommandOptions { content: string; } interface MentionCommand { - name: string; + name: command_name_enum; aliases: string[]; description: string; run: (options: MentionCommandOptions) => Promise; @@ -148,7 +151,7 @@ interface MentionCommand { const mentionCommands: MentionCommand[] = [ { - name: 'bs', + name: command_name_enum.bs, aliases: ['bs'], description: 'Searches your bank.', run: async ({ msg, user, components, content }: MentionCommandOptions) => { @@ -167,7 +170,7 @@ const mentionCommands: MentionCommand[] = [ } }, { - name: 'bal', + name: command_name_enum.bal, aliases: ['bal', 'gp'], description: 'Shows how much GP you have.', run: async ({ msg, user, components }: MentionCommandOptions) => { @@ -178,7 +181,7 @@ const mentionCommands: MentionCommand[] = [ } }, { - name: 'is', + name: command_name_enum.is, aliases: ['is'], description: 'Searches for items.', run: async ({ msg, components, user, content }: MentionCommandOptions) => { @@ -220,7 +223,7 @@ const mentionCommands: MentionCommand[] = [ } }, { - name: 'bank', + name: command_name_enum.bank, aliases: ['b', 'bank'], description: 'Shows your bank.', run: async ({ msg, user, components }: MentionCommandOptions) => { @@ -242,7 +245,7 @@ const mentionCommands: MentionCommand[] = [ } }, { - name: 'cd', + name: command_name_enum.cd, aliases: ['cd'], description: 'Shows your cooldowns.', run: async ({ msg, user, components }: MentionCommandOptions) => { @@ -272,7 +275,7 @@ const mentionCommands: MentionCommand[] = [ } }, { - name: 'sendtoabutton', + name: command_name_enum.sendtoabutton, aliases: ['sendtoabutton'], description: 'Shows your stats.', run: async ({ msg, user }: MentionCommandOptions) => { @@ -297,7 +300,7 @@ const mentionCommands: MentionCommand[] = [ } }, { - name: 's', + name: command_name_enum.stats, aliases: ['s', 'stats'], description: 'Shows your stats.', run: async ({ msg, user, components }: MentionCommandOptions) => { @@ -331,7 +334,6 @@ export async function onMessage(msg: Message) { guild_id: msg.guildId ? BigInt(msg.guildId) : undefined, command_name: command.name, args: msgContentWithoutCommand, - flags: undefined, inhibited: false, is_mention_command: true } diff --git a/src/lib/gear/functions/generateGearImage.ts b/src/lib/gear/functions/generateGearImage.ts index c15b7ae456..3116eecbe3 100644 --- a/src/lib/gear/functions/generateGearImage.ts +++ b/src/lib/gear/functions/generateGearImage.ts @@ -79,7 +79,6 @@ export async function generateGearImage( gearType: GearSetupType | null, petID: number | null ) { - debugLog('Generating gear image', { user_id: user.id }); const bankBg = user.user.bankBackground ?? 1; const { sprite, uniqueSprite, background: userBgImage } = bankImageGenerator.getBgAndSprite(bankBg, user); @@ -246,7 +245,6 @@ export async function generateAllGearImage(user: MUser) { } = bankImageGenerator.getBgAndSprite(user.user.bankBackground ?? 1, user); const hexColor = user.user.bank_bg_hex; - debugLog('Generating all-gear image', { user_id: user.id }); const gearTemplateImage = await loadAndCacheLocalImage('./src/lib/resources/images/gear_template_compact.png'); const canvas = new Canvas((gearTemplateImage.width + 10) * 4 + 20, Number(gearTemplateImage.height) * 2 + 70); const ctx = canvas.getContext('2d'); diff --git a/src/lib/globals.ts b/src/lib/globals.ts index 8d3529dbd3..f75e85a675 100644 --- a/src/lib/globals.ts +++ b/src/lib/globals.ts @@ -1,10 +1,11 @@ import { isMainThread } from 'node:worker_threads'; -import { TSRedis } from '@oldschoolgg/toolkit/dist/lib/TSRedis'; +import { TSRedis } from '@oldschoolgg/toolkit/TSRedis'; import { PrismaClient } from '@prisma/client'; import { PrismaClient as RobochimpPrismaClient } from '@prisma/robochimp'; import { production } from '../config'; import { globalConfig } from './constants'; +import { handleDeletedPatron, handleEditPatron } from './patreonUtils'; declare global { var prisma: PrismaClient; @@ -19,12 +20,7 @@ function makePrismaClient(): PrismaClient { } return new PrismaClient({ - log: [ - { - emit: 'event', - level: 'query' - } - ] + log: ['info', 'warn', 'error'] }); } global.prisma = global.prisma || makePrismaClient(); @@ -35,7 +31,9 @@ function makeRobochimpPrismaClient(): RobochimpPrismaClient { throw new Error('Robochimp client should only be created on the main thread.'); } - return new RobochimpPrismaClient(); + return new RobochimpPrismaClient({ + log: ['info', 'warn', 'error'] + }); } global.roboChimpClient = global.roboChimpClient || makeRobochimpPrismaClient(); @@ -47,3 +45,14 @@ function makeRedisClient(): TSRedis { return new TSRedis({ mocked: !globalConfig.redisPort, port: globalConfig.redisPort }); } global.redis = global.redis || makeRedisClient(); + +global.redis.subscribe(message => { + debugLog(`Received message from Redis: ${JSON.stringify(message)}`); + if (message.type === 'patron_tier_change') { + if (message.new_tier === 0) { + return handleDeletedPatron(message.discord_ids); + } else { + return handleEditPatron(message.discord_ids); + } + } +}); diff --git a/src/lib/grandExchange.ts b/src/lib/grandExchange.ts index debfc78c78..fdbebd31a3 100644 --- a/src/lib/grandExchange.ts +++ b/src/lib/grandExchange.ts @@ -1,19 +1,19 @@ import type { GEListing, GETransaction } from '@prisma/client'; import { GEListingType } from '@prisma/client'; import { ButtonBuilder, ButtonStyle, bold, userMention } from 'discord.js'; -import { Time, calcPercentOfNum, clamp, noOp, sumArr } from 'e'; +import { Time, calcPercentOfNum, clamp, noOp, sumArr, uniqueArr } from 'e'; import { LRUCache } from 'lru-cache'; import { Bank } from 'oldschooljs'; import type { Item, ItemBank } from 'oldschooljs/dist/meta/types'; import PQueue from 'p-queue'; import { ADMIN_IDS, OWNER_IDS, production } from '../config'; -import { BLACKLISTED_USERS } from './blacklists'; import { BitField, ONE_TRILLION, PerkTier, globalConfig } from './constants'; import { marketPricemap } from './marketPrices'; import type { RobochimpUser } from './roboChimp'; import { roboChimpUserFetch } from './roboChimp'; +import { BLACKLISTED_USERS } from './blacklists'; import { fetchTableBank, makeTransactFromTableBankQueries } from './tableBank'; import { assert, generateGrandExchangeID, getInterval, itemNameFromID, makeComponents, toKMB } from './util'; import { mahojiClientSettingsFetch, mahojiClientSettingsUpdate } from './util/clientSettings'; @@ -110,6 +110,13 @@ class GrandExchangeSingleton { public locked = false; public isTicking = false; public ready = false; + public loggingEnabled = false; + + log(message: string, context?: any) { + if (this.loggingEnabled) { + debugLog(message, context); + } + } public config = { maxPricePerItem: ONE_TRILLION, @@ -164,7 +171,6 @@ class GrandExchangeSingleton { try { await this.fetchOwnedBank(); await this.extensiveVerification(); - await this.checkGECanFullFilAllListings(); } catch (err: any) { await this.lockGE(err.message); } finally { @@ -261,8 +267,6 @@ class GrandExchangeSingleton { } }); - for (const tx of allActiveListingsInTimePeriod) sanityCheckTransaction(tx); - const item = getOSItem(geListing.item_id); const buyLimit = this.getItemBuyLimit(item); const totalSold = sumArr(allActiveListingsInTimePeriod.map(listing => listing.quantity_bought)); @@ -415,7 +419,8 @@ ${type} ${toKMB(quantity)} ${item.name} for ${toKMB(price)} each, for a total of ...makeTransactFromTableBankQueries({ bankToAdd: result.cost }) ]); - debugLog(`${user.id} created ${type} listing, removing ${result.cost}, adding it to the g.e bank.`); + sanityCheckListing(listing); + this.log(`${user.id} created ${type} listing, removing ${result.cost}, adding it to the g.e bank.`); return { createdListing: listing, @@ -525,7 +530,7 @@ ${type} ${toKMB(quantity)} ${item.name} for ${toKMB(price)} each, for a total of buyerLoot.add('Coins', buyerRefund); bankToRemoveFromGeBank.add('Coins', buyerRefund); - debugLog( + this.log( `Buyer got refunded ${buyerRefund} GP due to price difference. Buyer was asking ${buyerListing.asking_price_per_item}GP for each of the ${quantityToBuy}x items, seller was asking ${sellerListing.asking_price_per_item}GP, and the post-tax price per item was ${pricePerItemAfterTax}`, logContext ); @@ -538,9 +543,9 @@ ${type} ${toKMB(quantity)} ${item.name} for ${toKMB(price)} each, for a total of buyerListing.asking_price_per_item }] SellerPrice[${ sellerListing.asking_price_per_item - }] TotalPriceBeforeTax[${totalPriceBeforeTax}] QuantityToBuy[${quantityToBuy}] TotalTaxPaid[${totalTaxPaid}] BuyerRefund[${buyerRefund}] BuyerLoot[${buyerLoot}] SellerLoot[${sellerLoot}] CurrentGEBank[${geBank}] BankToRemoveFromGeBank[${bankToRemoveFromGeBank}] ExpectedAfterBank[${geBank - .clone() - .remove(bankToRemoveFromGeBank)}]`; + }] TotalPriceBeforeTax[${totalPriceBeforeTax}] QuantityToBuy[${quantityToBuy}] TotalTaxPaid[${totalTaxPaid}] BuyerRefund[${buyerRefund}] BuyerLoot[${buyerLoot}] SellerLoot[${sellerLoot}] CurrentGEBank[${geBank}] BankToRemoveFromGeBank[${JSON.stringify(bankToRemoveFromGeBank.bank)}] ExpectedAfterBank[${ + geBank.clone().remove(bankToRemoveFromGeBank).bank + }]`; assert( bankToRemoveFromGeBank.amount('Coins') === Number(buyerListing.asking_price_per_item) * quantityToBuy, @@ -553,22 +558,22 @@ ${type} ${toKMB(quantity)} ${item.name} for ${toKMB(price)} each, for a total of const missingItems = bankGEShouldHave.clone().remove(geBank); const str = `The GE did not have enough items to cover this transaction! We tried to remove ${bankGEShouldHave} missing: ${missingItems}. ${debug}`; logError(str, logContext); - debugLog(str, logContext); + this.log(str, logContext); throw new Error(str); } - debugLog( - `Completing a transaction, removing ${bankToRemoveFromGeBank} from the GE bank, ${totalTaxPaid} in taxed gp. The current GE bank is ${geBank.toString()}. ${debug}`, + this.log( + `Completing a transaction, removing ${JSON.stringify(bankToRemoveFromGeBank.bank)} from the GE bank, ${totalTaxPaid} in taxed gp. The current GE bank is ${JSON.stringify(geBank.bank)}. ${debug}`, { totalPriceAfterTax, totalTaxPaid, totalPriceBeforeTax, bankToRemoveFromGeBank: bankToRemoveFromGeBank.toString(), - currentGEBank: geBank.toString() + currentGEBank: JSON.stringify(geBank.bank) } ); - await prisma.$transaction([ + const [newTx] = await prisma.$transaction([ prisma.gETransaction.create({ data: { buy_listing_id: buyerListing.id, @@ -618,7 +623,9 @@ ${type} ${toKMB(quantity)} ${item.name} for ${toKMB(price)} each, for a total of ...makeTransactFromTableBankQueries({ bankToRemove: bankToRemoveFromGeBank }) ]); - debugLog(`Transaction completed, the new G.E bank is ${await this.fetchOwnedBank()}.`); + sanityCheckTransaction(newTx); + + this.log(`Transaction completed, the new G.E bank is ${JSON.stringify((await this.fetchOwnedBank()).bank)}.`); const buyerUser = await mUserFetch(buyerListing.user_id); const sellerUser = await mUserFetch(sellerListing.user_id); @@ -706,29 +713,36 @@ ${type} ${toKMB(quantity)} ${item.name} for ${toKMB(price)} each, for a total of } async fetchActiveListings() { - const [buyListings, sellListings, clientStorage, currentBankRaw] = await prisma.$transaction([ - prisma.gEListing.findMany({ - where: { - type: GEListingType.Buy, - fulfilled_at: null, - cancelled_at: null, - user_id: { not: null } + const buyListings = await prisma.gEListing.findMany({ + where: { + type: GEListingType.Buy, + fulfilled_at: null, + cancelled_at: null, + user_id: { + not: null + } + }, + orderBy: [ + { + asking_price_per_item: 'desc' }, - orderBy: [ - { - asking_price_per_item: 'desc' - }, - { - created_at: 'asc' - } - ] - }), + { + created_at: 'asc' + } + ] + }); + const [sellListings, clientStorage, currentBankRaw] = await prisma.$transaction([ prisma.gEListing.findMany({ where: { type: GEListingType.Sell, fulfilled_at: null, cancelled_at: null, - user_id: { not: null } + user_id: { + not: null + }, + item_id: { + in: uniqueArr(buyListings.map(i => i.item_id)) + } }, orderBy: [ { @@ -768,19 +782,63 @@ ${type} ${toKMB(quantity)} ${item.name} for ${toKMB(price)} each, for a total of } async extensiveVerification() { - 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() - ]); - debugLog('Validated GE and found no issues.'); + await this.checkGECanFullFilAllListings(); return true; } async checkGECanFullFilAllListings() { const shouldHave = new Bank(); - const { buyListings, sellListings, currentBank } = await this.fetchActiveListings(); - + const [buyListings, sellListings, currentBankRaw] = await prisma.$transaction([ + prisma.gEListing.findMany({ + where: { + type: GEListingType.Buy, + fulfilled_at: null, + cancelled_at: null, + user_id: { + not: null + } + }, + orderBy: [ + { + asking_price_per_item: 'desc' + }, + { + created_at: 'asc' + } + ] + }), + prisma.gEListing.findMany({ + where: { + type: GEListingType.Sell, + fulfilled_at: null, + cancelled_at: null, + user_id: { + not: null + } + }, + orderBy: [ + { + asking_price_per_item: 'asc' + }, + { + created_at: 'asc' + } + ], + // Take the last purchase transaction for each sell listing + include: { + sellTransactions: { + orderBy: { + created_at: 'desc' + }, + take: 1 + } + } + }), + prisma.$queryRawUnsafe<{ bank: ItemBank }[]>( + 'SELECT json_object_agg(item_id, quantity) as bank FROM ge_bank WHERE quantity != 0;' + ) + ]); + const currentBank = new Bank(currentBankRaw[0].bank); // How much GP the g.e still has from this listing for (const listing of buyListings) { shouldHave.add('Coins', Number(listing.asking_price_per_item) * listing.quantity_remaining); @@ -790,7 +848,7 @@ ${type} ${toKMB(quantity)} ${item.name} for ${toKMB(price)} each, for a total of shouldHave.add(listing.item_id, listing.quantity_remaining); } - debugLog(`Expected G.E Bank: ${shouldHave}`); + this.log(`Expected G.E Bank: ${JSON.stringify(shouldHave.bank)}`); if (!currentBank.equals(shouldHave)) { if (!currentBank.has(shouldHave)) { throw new Error( @@ -805,11 +863,7 @@ G.E Bank Has: ${currentBank} G.E Bank Should Have: ${shouldHave} Difference: ${shouldHave.difference(currentBank)}`); } else { - debugLog( - `GE has ${currentBank}, which is enough to cover the ${ - [...buyListings, ...sellListings].length - }x active listings! Difference: ${shouldHave.difference(currentBank)}` - ); + this.log('GE has enough to cover the listings.'); return true; } } @@ -855,20 +909,35 @@ Difference: ${shouldHave.difference(currentBank)}`); private async _tick() { if (!this.ready) return; if (this.locked) return; - const { buyListings: _buyListings, sellListings: _sellListings } = await this.fetchActiveListings(); + const { buyListings, sellListings } = await this.fetchActiveListings(); - // Filter out listings from Blacklisted users: - const buyListings = _buyListings.filter(l => !BLACKLISTED_USERS.has(l.user_id!)); - const sellListings = _sellListings.filter(l => !BLACKLISTED_USERS.has(l.user_id!)); + const minimumSellPricePerItem = new Map(); + for (const sellListing of sellListings) { + const currentPrice = minimumSellPricePerItem.get(sellListing.item_id); + if (currentPrice === undefined || sellListing.asking_price_per_item < currentPrice) { + minimumSellPricePerItem.set(sellListing.item_id, Number(sellListing.asking_price_per_item)); + } + } for (const buyListing of buyListings) { + const minPrice = minimumSellPricePerItem.get(buyListing.item_id); + if (!buyListing.user_id || minPrice === undefined || buyListing.asking_price_per_item < minPrice) { + continue; + } + + if (BLACKLISTED_USERS.has(buyListing.user_id)) { + continue; + } + // These are all valid, matching sell listings we can match with this buy listing. const matchingSellListings = sellListings.filter( sellListing => sellListing.item_id === buyListing.item_id && // "Trades succeed when one player's buy offer is greater than or equal to another player's sell offer." buyListing.asking_price_per_item >= sellListing.asking_price_per_item && - buyListing.user_id !== sellListing.user_id + buyListing.user_id !== sellListing.user_id && + sellListing.user_id !== null && + !BLACKLISTED_USERS.has(sellListing.user_id) ); /** diff --git a/src/lib/handleNewCLItems.ts b/src/lib/handleNewCLItems.ts index 8001ecc628..d5e84942fa 100644 --- a/src/lib/handleNewCLItems.ts +++ b/src/lib/handleNewCLItems.ts @@ -9,9 +9,9 @@ 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 { fetchStatsForCL } from './util/fetchStatsForCL'; import { insertUserEvent } from './util/userEvents'; async function createHistoricalData(user: MUser): Promise { @@ -30,32 +30,6 @@ async function createHistoricalData(user: MUser): Promise Number(i)); - const updateObj = { - cl_array: newCLArray, - cl_array_length: newCLArray.length - } as const; - - await prisma.userStats.upsert({ - where: { - user_id: id - }, - create: { - user_id: id, - ...updateObj - }, - update: { - ...updateObj - } - }); - - return { - newCLArray - }; -} - export async function handleNewCLItems({ itemsAdded, user, @@ -76,12 +50,16 @@ 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); const previousCLRank = previousCLDetails.percent >= 80 ? await calculateOwnCLRanking(user.id) : null; - await Promise.all([roboChimpSyncData(user), clArrayUpdate(user, newCL)]); + await roboChimpSyncData(user, newCL); const newCLRank = previousCLDetails.percent >= 80 ? await calculateOwnCLRanking(user.id) : null; const newCLDetails = calcCLDetails(newCL); @@ -124,19 +102,18 @@ export async function handleNewCLItems({ getKC: (id: number) => user.getKC(id), user, minigames: await user.fetchMinigames(), - stats: await fetchStatsForCL(user) + stats: await MUserStats.fromID(user.id) })}!` : ''; - 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/mastery.ts b/src/lib/mastery.ts index 1d1764d296..ca17a8c17d 100644 --- a/src/lib/mastery.ts +++ b/src/lib/mastery.ts @@ -39,8 +39,7 @@ export async function calculateMastery(user: MUser, stats: MUserStats) { }, { name: 'Achievement Diaries', - percentage: (await calculateAchievementDiaryProgress(user, stats, await user.fetchMinigameScores())) - .percentComplete + percentage: calculateAchievementDiaryProgress(user, stats, await user.fetchMinigames()).percentComplete } ] as const; diff --git a/src/lib/metrics.ts b/src/lib/metrics.ts index ede0f2c20b..9bf21dfa47 100644 --- a/src/lib/metrics.ts +++ b/src/lib/metrics.ts @@ -69,7 +69,6 @@ export async function collectMetrics() { prisma_query_active_transactions: transformed.query_active_transactions as number }; h.reset(); - debugLog('Collected metrics', { ...metrics, type: 'COLLECT_METRICS' }); return metrics; } diff --git a/src/lib/minions/data/killableMonsters/vannakaMonsters.ts b/src/lib/minions/data/killableMonsters/vannakaMonsters.ts index d6c0feb8b6..23e6c5ff52 100644 --- a/src/lib/minions/data/killableMonsters/vannakaMonsters.ts +++ b/src/lib/minions/data/killableMonsters/vannakaMonsters.ts @@ -243,7 +243,6 @@ export const vannakaMonsters: KillableMonster[] = [ healAmountNeeded: 12, attackStyleToUse: GearStat.AttackRanged, attackStylesUsed: [GearStat.AttackMagic], - canCannon: true, pkActivityRating: 4, pkBaseDeathChance: 6, revsWeaponBoost: true diff --git a/src/lib/minions/data/quests.ts b/src/lib/minions/data/quests.ts index 70451b6ac9..91a92ec0cb 100644 --- a/src/lib/minions/data/quests.ts +++ b/src/lib/minions/data/quests.ts @@ -84,7 +84,7 @@ export const quests: Quest[] = [ }, combatLevelReq: 50, qpReq: 10, - rewards: new Bank().add(28_587).add(28_587).add(28_588).add(28_589).add(28_590).freeze(), + rewards: new Bank().add(28_587).add(28_588).add(28_589).add(28_590).freeze(), calcTime: (user: MUser) => { let duration = Time.Minute * 10; if (user.combatLevel < 90) { @@ -112,8 +112,7 @@ export const quests: Quest[] = [ }, combatLevelReq: 65, qpReq: 20, - // Awaiting item update for the lamp to be added - // rewards: new Bank().add(28_820).freeze(), + rewards: new Bank().add(28_820).freeze(), skillsRewards: { smithing: 15_000, hunter: 15_000 diff --git a/src/lib/minions/functions/announceLoot.ts b/src/lib/minions/functions/announceLoot.ts index b02fb73528..f444d52572 100644 --- a/src/lib/minions/functions/announceLoot.ts +++ b/src/lib/minions/functions/announceLoot.ts @@ -20,7 +20,6 @@ export default async function announceLoot({ }) { if (!_notifyDrops) return; const notifyDrops = _notifyDrops.flat(Number.POSITIVE_INFINITY); - const kc = await user.getKC(monsterID); const itemsToAnnounce = loot.clone().filter(i => notifyDrops.includes(i.id)); if (itemsToAnnounce.length > 0) { let notif = ''; @@ -30,6 +29,7 @@ export default async function announceLoot({ effectiveMonsters.find(m => m.id === monsterID)?.name }, **${team.lootRecipient.badgedUsername}** just received **${itemsToAnnounce}**!`; } else { + const kc = await user.getKC(monsterID); notif = `**${user.badgedUsername}'s** minion, ${minionName( user )}, just received **${itemsToAnnounce}**, their ${ diff --git a/src/lib/minions/functions/index.ts b/src/lib/minions/functions/index.ts index f3bd7b49eb..2464f0acec 100644 --- a/src/lib/minions/functions/index.ts +++ b/src/lib/minions/functions/index.ts @@ -50,7 +50,7 @@ export function resolveAttackStyles( // Automatically use magic if barrage/burst is chosen if ( params.boostMethod && - (params.boostMethod === 'barrage' || params.boostMethod === 'burst') && + (params.boostMethod.includes('barrage') || params.boostMethod.includes('burst')) && !attackStyles.includes(SkillsEnum.Magic) ) { if (attackStyles.includes(SkillsEnum.Defence)) { @@ -63,7 +63,7 @@ export function resolveAttackStyles( } export async function addMonsterXP(user: MUser, params: AddMonsterXpParams) { - const boostMethod = params.burstOrBarrage ? 'barrage' : 'none'; + const boostMethod = params.burstOrBarrage ? ['barrage'] : ['none']; const [, osjsMon, attackStyles] = resolveAttackStyles(user, { monsterID: params.monsterID, diff --git a/src/lib/minions/functions/lmsSimCommand.ts b/src/lib/minions/functions/lmsSimCommand.ts index ef34e9c1bc..ae715ec690 100644 --- a/src/lib/minions/functions/lmsSimCommand.ts +++ b/src/lib/minions/functions/lmsSimCommand.ts @@ -125,7 +125,7 @@ export async function lmsSimCommand(channel: Channel | undefined, names?: string if (filtered.size < 4) { return channel.send( - 'Please specify atleast 4 players for Last Man Standing, like so: `+lms Alex, Kyra, Magna, Rick`, or type `+lms auto` to automatically pick people from the chat.' + 'Please specify at least 4 players for Last Man Standing, like so: `+lms Alex, Kyra, Magna, Rick`, or type `+lms auto` to automatically pick people from the chat.' ); } diff --git a/src/lib/minions/functions/removeFoodFromUser.ts b/src/lib/minions/functions/removeFoodFromUser.ts index a9174c2e4a..5aabe7ebee 100644 --- a/src/lib/minions/functions/removeFoodFromUser.ts +++ b/src/lib/minions/functions/removeFoodFromUser.ts @@ -1,4 +1,4 @@ -import { UserError } from '@oldschoolgg/toolkit/dist/lib/UserError'; +import { UserError } from '@oldschoolgg/toolkit'; import { objectEntries, reduceNumByPercent } from 'e'; import type { Bank } from 'oldschooljs'; import { itemID } from 'oldschooljs/dist/util'; @@ -67,8 +67,7 @@ export default async function removeFoodFromUser({ ); } else { await transactItems({ userID: user.id, itemsToRemove: foodToRemove }); - - updateBankSetting('economyStats_PVMCost', foodToRemove); + await updateBankSetting('economyStats_PVMCost', foodToRemove); return { foodRemoved: foodToRemove, diff --git a/src/lib/minions/types.ts b/src/lib/minions/types.ts index e5efadbb39..cc81695e13 100644 --- a/src/lib/minions/types.ts +++ b/src/lib/minions/types.ts @@ -177,7 +177,7 @@ export interface AddMonsterXpParams { export interface ResolveAttackStylesParams { monsterID: number | undefined; - boostMethod?: string; + boostMethod?: string[]; } export interface BlowpipeData { diff --git a/src/lib/musicCape.ts b/src/lib/musicCape.ts index 4e18293e53..84bba310e1 100644 --- a/src/lib/musicCape.ts +++ b/src/lib/musicCape.ts @@ -3,32 +3,24 @@ import { objectEntries, partition } from 'e'; import { Bank, Monsters } from 'oldschooljs'; import { resolveItems } from 'oldschooljs/dist/util/util'; -import { getPOH } from '../mahoji/lib/abstracted_commands/pohCommand'; import { MIMIC_MONSTER_ID, NEX_ID, ZALCANO_ID } from './constants'; import { championScrolls } from './data/CollectionsExport'; import { RandomEvents } from './randomEvents'; import type { MinigameName } from './settings/minigames'; import { Minigames } from './settings/minigames'; -import { getUsersActivityCounts } from './settings/prisma'; import type { RequirementFailure } from './structures/Requirements'; import { Requirements } from './structures/Requirements'; -import { itemNameFromID } from './util'; export const musicCapeRequirements = new Requirements() .add({ - name: 'Do 20 slayer tasks', - has: async ({ user }) => { - const count = await prisma.slayerTask.count({ - where: { - user_id: user.id - } - }); - if (count >= 20) { + name: 'Reach level 50 Slayer', + has: ({ user }) => { + if (user.skillsAsLevels.slayer >= 50) { return []; } return [ { - reason: 'You need to complete 20 slayer tasks.' + reason: 'You need level 50 slayer.' } ]; } @@ -108,14 +100,8 @@ export const musicCapeRequirements = new Requirements() } }) .add({ - name: 'Runecraft all runes atleast once', - has: async ({ user }) => { - const counts = await prisma.$queryRaw<{ rune_id: string }[]>`SELECT DISTINCT(data->>'runeID') AS rune_id -FROM activity -WHERE user_id = ${BigInt(user.id)} -AND type = 'Runecraft' -AND data->>'runeID' IS NOT NULL;`; - + name: 'Runecraft all runes at least once', + has: ({ uniqueRunesCrafted }) => { const runesToCheck = resolveItems([ 'Mind rune', 'Air rune', @@ -130,10 +116,7 @@ AND data->>'runeID' IS NOT NULL;`; 'Astral rune', 'Wrath rune' ]); - const notDoneRunes = runesToCheck - .filter(i => !counts.some(c => c.rune_id === i.toString())) - .map(i => itemNameFromID(i)!) - .map(s => s.split(' ')[0]); + const notDoneRunes = runesToCheck.filter(r => !uniqueRunesCrafted.includes(r)); if (notDoneRunes.length > 0) { return [ { @@ -147,7 +130,7 @@ AND data->>'runeID' IS NOT NULL;`; }) .add({ name: 'One of Every Activity', - has: async ({ user }) => { + has: ({ uniqueActivitiesDone }) => { const typesNotRequiredForMusicCape: activity_type_enum[] = [ activity_type_enum.Easter, activity_type_enum.HalloweenEvent, @@ -159,10 +142,8 @@ AND data->>'runeID' IS NOT NULL;`; activity_type_enum.Nex, activity_type_enum.Revenants // This is now under monsterActivity ]; - const activityCounts = await getUsersActivityCounts(user); - const notDoneActivities = Object.values(activity_type_enum).filter( - type => !typesNotRequiredForMusicCape.includes(type) && activityCounts[type] < 1 + type => !typesNotRequiredForMusicCape.includes(type) && !uniqueActivitiesDone.includes(type) ); const [firstLot, secondLot] = partition(notDoneActivities, i => notDoneActivities.indexOf(i) < 5); @@ -182,7 +163,7 @@ AND data->>'runeID' IS NOT NULL;`; }) .add({ name: 'One of Every Minigame', - has: async ({ user }) => { + has: ({ minigames }) => { const results: RequirementFailure[] = []; const typesNotRequiredForMusicCape: MinigameName[] = [ 'corrupted_gauntlet', @@ -192,9 +173,8 @@ AND data->>'runeID' IS NOT NULL;`; 'champions_challenge' ]; - const minigameScores = await user.fetchMinigames(); const minigamesNotDone = Minigames.filter( - i => !typesNotRequiredForMusicCape.includes(i.column) && minigameScores[i.column] < 1 + i => !typesNotRequiredForMusicCape.includes(i.column) && minigames[i.column] < 1 ).map(i => i.name); if (minigamesNotDone.length > 0) { @@ -208,7 +188,7 @@ AND data->>'runeID' IS NOT NULL;`; }) .add({ name: 'One Random Event with a unique music track', - has: async ({ stats }) => { + has: ({ stats }) => { const results: RequirementFailure[] = []; const eventBank = stats.randomEventCompletionsBank(); const uniqueTracks = RandomEvents.filter(i => i.uniqueMusic); @@ -224,8 +204,7 @@ AND data->>'runeID' IS NOT NULL;`; }) .add({ name: 'Must Build Something in PoH', - has: async ({ user }) => { - const poh = await getPOH(user.id); + has: ({ poh }) => { for (const [key, value] of objectEntries(poh)) { if (['user_id', 'background_id'].includes(key)) continue; if (value !== null) { @@ -237,7 +216,7 @@ AND data->>'runeID' IS NOT NULL;`; }) .add({ name: 'Champions Challenge', - has: async ({ user }) => { + has: ({ user }) => { for (const scroll of championScrolls) { if (user.cl.has(scroll)) return []; } diff --git a/src/lib/party.ts b/src/lib/party.ts index ee54831090..f1736a5974 100644 --- a/src/lib/party.ts +++ b/src/lib/party.ts @@ -1,22 +1,20 @@ import { makeComponents } from '@oldschoolgg/toolkit'; -import { UserError } from '@oldschoolgg/toolkit/dist/lib/UserError'; +import { UserError } from '@oldschoolgg/toolkit'; +import { TimerManager } from '@sapphire/timer-manager'; 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(); -if (production) { - setInterval(() => { - debugLog('Clearing partylockcache'); - partyLockCache.clear(); - }, Time.Minute * 20); -} +TimerManager.setInterval(() => { + partyLockCache.clear(); +}, Time.Minute * 20); const buttons = [ { @@ -42,13 +40,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 +54,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 +75,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 +136,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 => { @@ -226,7 +224,7 @@ export async function setupParty(channel: TextChannel, leaderUser: MUser, option for (const user of usersWhoConfirmed) { partyLockCache.delete(user); } - setTimeout(() => startTrip(), 250); + TimerManager.setTimeout(() => startTrip(), 250); }); }); diff --git a/src/lib/patreonUtils.ts b/src/lib/patreonUtils.ts new file mode 100644 index 0000000000..bcc964ce44 --- /dev/null +++ b/src/lib/patreonUtils.ts @@ -0,0 +1,54 @@ +import { BadgesEnum } from './constants'; +import { populateRoboChimpCache } from './perkTier'; + +export async function handleDeletedPatron(userID: string[]) { + const users = await prisma.user.findMany({ + where: { + id: { + in: userID + } + } + }); + + for (const user of users) { + if (user.badges.includes(BadgesEnum.Patron) || user.badges.includes(BadgesEnum.LimitedPatron)) { + await prisma.user.update({ + where: { + id: user.id + }, + data: { + badges: user.badges.filter(b => b !== BadgesEnum.Patron && b !== BadgesEnum.LimitedPatron) + } + }); + } + } + + await populateRoboChimpCache(); +} + +export async function handleEditPatron(userID: string[]) { + const users = await prisma.user.findMany({ + where: { + id: { + in: userID + } + } + }); + + for (const user of users) { + if (!user.badges.includes(BadgesEnum.Patron) && !user.badges.includes(BadgesEnum.LimitedPatron)) { + await prisma.user.update({ + where: { + id: user.id + }, + data: { + badges: { + push: BadgesEnum.Patron + } + } + }); + } + } + + await populateRoboChimpCache(); +} diff --git a/src/lib/perkTier.ts b/src/lib/perkTier.ts new file mode 100644 index 0000000000..a4ccaffc4b --- /dev/null +++ b/src/lib/perkTier.ts @@ -0,0 +1,44 @@ +import { pick } from 'lodash'; +import type { RobochimpUser } from './roboChimp'; + +const robochimpCachedKeys = [ + 'bits', + 'github_id', + 'patreon_id', + 'perk_tier', + 'user_group_id', + 'premium_balance_expiry_date', + 'premium_balance_tier' +] as const; +type CachedRoboChimpUser = Pick; + +export const roboChimpCache = new Map(); + +export async function populateRoboChimpCache() { + const users = await roboChimpClient.user.findMany({ + select: { + id: true, + bits: true, + github_id: true, + patreon_id: true, + perk_tier: true, + premium_balance_expiry_date: true, + premium_balance_tier: true, + user_group_id: true + }, + where: { + perk_tier: { + not: 0 + } + } + }); + for (const user of users) { + roboChimpCache.set(user.id.toString(), user); + } + debugLog(`Populated RoboChimp cache with ${users.length} users.`); +} + +export function cacheRoboChimpUser(user: RobochimpUser) { + if (user.perk_tier === 0) return; + roboChimpCache.set(user.id.toString(), pick(user, robochimpCachedKeys)); +} diff --git a/src/lib/perkTiers.ts b/src/lib/perkTiers.ts index ea63dd3c98..e25e2db5bb 100644 --- a/src/lib/perkTiers.ts +++ b/src/lib/perkTiers.ts @@ -1,18 +1,6 @@ -import type { User } from '@prisma/client'; -import { notEmpty } from 'e'; - import { SupportServer } from '../config'; import { BitField, PerkTier, Roles } from './constants'; -import { logError } from './util/logError'; - -export const perkTierCache = new Map(); - -const tier3ElligibleBits = [ - BitField.IsPatronTier3, - BitField.isContributor, - BitField.isModerator, - BitField.IsWikiContributor -]; +import { roboChimpCache } from './perkTier'; export const allPerkBitfields: BitField[] = [ BitField.IsPatronTier6, @@ -25,83 +13,52 @@ export const allPerkBitfields: BitField[] = [ BitField.BothBotsMaxedFreeTierOnePerks ]; -export function getUsersPerkTier( - userOrBitfield: MUser | User | BitField[], - noCheckOtherAccounts?: boolean -): PerkTier | 0 { - // Check if the user has a premium balance tier - if (userOrBitfield instanceof GlobalMUserClass && userOrBitfield.user.premium_balance_tier !== null) { - const date = userOrBitfield.user.premium_balance_expiry_date; - if (date && Date.now() < date) { - return userOrBitfield.user.premium_balance_tier + 1; - } else if (date && Date.now() > date) { - userOrBitfield - .update({ - premium_balance_tier: null, - premium_balance_expiry_date: null - }) - .catch(e => { - logError(e, { user_id: userOrBitfield.id, message: 'Could not remove premium time' }); - }); - } +export function getUsersPerkTier(user: MUser): PerkTier | 0 { + if ([BitField.isModerator].some(bit => user.bitfield.includes(bit))) { + return PerkTier.Four; } - if (noCheckOtherAccounts !== true && userOrBitfield instanceof GlobalMUserClass) { - const main = userOrBitfield.user.main_account; - const allAccounts: string[] = [...userOrBitfield.user.ironman_alts, userOrBitfield.id]; - if (main) { - allAccounts.push(main); + const elligibleTiers = []; + if ( + user.bitfield.includes(BitField.IsPatronTier1) || + user.bitfield.includes(BitField.HasPermanentTierOne) || + user.bitfield.includes(BitField.BothBotsMaxedFreeTierOnePerks) + ) { + elligibleTiers.push(PerkTier.Two); + } else { + const guild = globalClient.guilds.cache.get(SupportServer); + const member = guild?.members.cache.get(user.id); + if (member && [Roles.Booster].some(roleID => member.roles.cache.has(roleID))) { + elligibleTiers.push(PerkTier.One); } + } - const allAccountTiers = allAccounts.map(id => perkTierCache.get(id)).filter(notEmpty); - - const highestAccountTier = Math.max(0, ...allAccountTiers); - return highestAccountTier; + const roboChimpCached = roboChimpCache.get(user.id); + if (roboChimpCached) { + elligibleTiers.push(roboChimpCached.perk_tier); } - const bitfield = Array.isArray(userOrBitfield) ? userOrBitfield : userOrBitfield.bitfield; + const bitfield = user.bitfield; if (bitfield.includes(BitField.IsPatronTier6)) { - return PerkTier.Seven; + elligibleTiers.push(PerkTier.Seven); } if (bitfield.includes(BitField.IsPatronTier5)) { - return PerkTier.Six; + elligibleTiers.push(PerkTier.Six); } if (bitfield.includes(BitField.IsPatronTier4)) { - return PerkTier.Five; + elligibleTiers.push(PerkTier.Five); } - if (tier3ElligibleBits.some(bit => bitfield.includes(bit))) { - return PerkTier.Four; + if (bitfield.includes(BitField.IsPatronTier3)) { + elligibleTiers.push(PerkTier.Four); } if (bitfield.includes(BitField.IsPatronTier2)) { - return PerkTier.Three; - } - - if ( - bitfield.includes(BitField.IsPatronTier1) || - bitfield.includes(BitField.HasPermanentTierOne) || - bitfield.includes(BitField.BothBotsMaxedFreeTierOnePerks) - ) { - return PerkTier.Two; - } - - if (userOrBitfield instanceof GlobalMUserClass) { - const guild = globalClient.guilds.cache.get(SupportServer); - const member = guild?.members.cache.get(userOrBitfield.id); - if (member && [Roles.Booster].some(roleID => member.roles.cache.has(roleID))) { - return PerkTier.One; - } + elligibleTiers.push(PerkTier.Three); } - return 0; -} - -export function syncPerkTierOfUser(user: MUser) { - const perkTier = getUsersPerkTier(user, true); - perkTierCache.set(user.id, perkTier); - return perkTier; + return Math.max(...elligibleTiers, 0); } diff --git a/src/lib/pohImage.ts b/src/lib/pohImage.ts index c4733464de..542c98b9f5 100644 --- a/src/lib/pohImage.ts +++ b/src/lib/pohImage.ts @@ -62,7 +62,6 @@ class PoHImage { ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.drawImage(bgImage, 0, 0, bgImage.width, bgImage.height); - debugLog('Generating a POH image'); return [canvas, ctx]; } diff --git a/src/lib/preStartup.ts b/src/lib/preStartup.ts new file mode 100644 index 0000000000..b47aefc1f0 --- /dev/null +++ b/src/lib/preStartup.ts @@ -0,0 +1,29 @@ +import { noOp } from 'e'; +import { syncCustomPrices } from '../mahoji/lib/events'; +import { syncActivityCache } from './Task'; +import { cacheBadges } from './badges'; +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'; +import { syncDisabledCommands } from './util/syncDisabledCommands'; + +export const preStartup = logWrapFn('PreStartup', async () => { + await Promise.all([ + syncActiveUserIDs(), + syncActivityCache(), + runStartupScripts(), + syncDisabledCommands(), + syncBlacklists(), + syncCustomPrices(), + cacheBadges(), + GrandExchange.init(), + populateRoboChimpCache(), + cacheGEPrices(), + prisma.$queryRawUnsafe(RawSQL.updateAllUsersCLArrays()).then(noOp) + ]); +}); diff --git a/src/lib/randomEvents.ts b/src/lib/randomEvents.ts index 0a8fbc51c4..5370f5442e 100644 --- a/src/lib/randomEvents.ts +++ b/src/lib/randomEvents.ts @@ -187,19 +187,19 @@ const cache = new LRUCache({ max: 500 }); const doesntGetRandomEvent: activity_type_enum[] = [activity_type_enum.TombsOfAmascut]; export async function triggerRandomEvent(user: MUser, type: activity_type_enum, duration: number, messages: string[]) { - if (doesntGetRandomEvent.includes(type)) return; + if (doesntGetRandomEvent.includes(type)) return {}; const minutes = Math.min(30, duration / Time.Minute); const randomEventChance = 60 - minutes; - if (!roll(randomEventChance)) return; + if (!roll(randomEventChance)) return {}; if (user.bitfield.includes(BitField.DisabledRandomEvents)) { - return; + return {}; } const prev = cache.get(user.id); // Max 1 event per 3h mins per user if (prev && Date.now() - prev < Time.Hour * 3) { - return; + return {}; } cache.set(user.id, Date.now()); @@ -214,7 +214,9 @@ export async function triggerRandomEvent(user: MUser, type: activity_type_enum, } } loot.add(event.loot.roll()); - await transactItems({ userID: user.id, itemsToAdd: loot, collectionLog: true }); - await userStatsBankUpdate(user.id, 'random_event_completions_bank', new Bank().add(event.id)); + await userStatsBankUpdate(user, 'random_event_completions_bank', new Bank().add(event.id)); messages.push(`Did ${event.name} random event and got ${loot}`); + return { + itemsToAddWithCL: loot + }; } diff --git a/src/lib/rawSql.ts b/src/lib/rawSql.ts new file mode 100644 index 0000000000..45def967e9 --- /dev/null +++ b/src/lib/rawSql.ts @@ -0,0 +1,28 @@ +import { Prisma } from '@prisma/client'; +import { logError } from './util/logError'; + +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}';` +}; + +export async function loggedRawPrismaQuery(query: string): Promise { + try { + const result = await prisma.$queryRawUnsafe(query); + return result; + } catch (err) { + logError(err, { query: query.slice(0, 100) }); + } + + return null; +} diff --git a/src/lib/roboChimp.ts b/src/lib/roboChimp.ts index 5c7e51bfcb..bfb465d5c9 100644 --- a/src/lib/roboChimp.ts +++ b/src/lib/roboChimp.ts @@ -2,15 +2,31 @@ import { formatOrdinal } from '@oldschoolgg/toolkit'; import type { TriviaQuestion, User } from '@prisma/robochimp'; import { calcWhatPercent, round, sumArr } from 'e'; import deepEqual from 'fast-deep-equal'; +import type { Bank } from 'oldschooljs'; -import { BOT_TYPE, masteryKey } from './constants'; +import { BOT_TYPE, globalConfig, masteryKey } from './constants'; import { getTotalCl } from './data/Collections'; import { calculateMastery } from './mastery'; +import { cacheRoboChimpUser } from './perkTier'; import { MUserStats } from './structures/MUserStats'; export type RobochimpUser = User; export async function getRandomTriviaQuestions(): Promise { + if (!globalConfig.isProduction) { + return [ + { + id: 1, + question: 'What is 1+1?', + answers: ['2'] + }, + { + id: 2, + question: 'What is 2+2?', + answers: ['4'] + } + ]; + } const random: TriviaQuestion[] = await roboChimpClient.$queryRaw`SELECT id, question, answers FROM trivia_question ORDER BY random() @@ -22,8 +38,29 @@ const clKey: keyof User = 'osb_cl_percent'; const levelKey: keyof User = 'osb_total_level'; const totalXPKey: keyof User = BOT_TYPE === 'OSB' ? 'osb_total_xp' : 'bso_total_xp'; -export async function roboChimpSyncData(user: MUser) { - const stats = await MUserStats.fromID(user.id); +export async function roboChimpSyncData(user: MUser, newCL?: Bank) { + const id = BigInt(user.id); + const newCLArray: number[] = Object.keys((newCL ?? user.cl).bank).map(i => Number(i)); + const clArrayUpdateObject = { + cl_array: newCLArray, + cl_array_length: newCLArray.length + } as const; + + const stats = new MUserStats( + await prisma.userStats.upsert({ + where: { + user_id: id + }, + create: { + user_id: id, + ...clArrayUpdateObject + }, + update: { + ...clArrayUpdateObject + } + }) + ); + const [totalClItems, clItems] = getTotalCl(user, 'collection', stats); const clCompletionPercentage = round(calcWhatPercent(clItems, totalClItems), 2); const totalXP = sumArr(Object.values(user.skillsAsXP)); @@ -37,7 +74,7 @@ export async function roboChimpSyncData(user: MUser) { [masteryKey]: totalMastery } as const; - const newUser = await roboChimpClient.user.upsert({ + const newUser: RobochimpUser = await roboChimpClient.user.upsert({ where: { id: BigInt(user.id) }, @@ -47,6 +84,7 @@ export async function roboChimpSyncData(user: MUser) { ...updateObj } }); + cacheRoboChimpUser(newUser); if (!deepEqual(newUser.store_bitfield, user.user.store_bitfield)) { await user.update({ store_bitfield: newUser.store_bitfield }); @@ -54,8 +92,8 @@ export async function roboChimpSyncData(user: MUser) { return newUser; } -export async function roboChimpUserFetch(userID: string) { - const result = await roboChimpClient.user.upsert({ +export async function roboChimpUserFetch(userID: string): Promise { + const result: RobochimpUser = await roboChimpClient.user.upsert({ where: { id: BigInt(userID) }, @@ -65,6 +103,8 @@ export async function roboChimpUserFetch(userID: string) { update: {} }); + cacheRoboChimpUser(result); + return result; } diff --git a/src/lib/rolesTask.ts b/src/lib/rolesTask.ts index 317fc988a6..eb76b1d692 100644 --- a/src/lib/rolesTask.ts +++ b/src/lib/rolesTask.ts @@ -1,535 +1,453 @@ -import { Prisma } from '@prisma/client'; -import { noOp, notEmpty } from 'e'; +import { noOp, notEmpty, uniqueArr } from 'e'; -import { SupportServer, production } from '../config'; -import { ClueTiers } from '../lib/clues/clueTiers'; -import { Roles, usernameCache } 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 } 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 { loggedRawPrismaQuery } from './rawSql'; import { TeamLoot } from './simulation/TeamLoot'; +import { SkillsArray } from './skilling/types'; import type { ItemBank } from './types'; +import { fetchMultipleCLLeaderboards } from './util/clLeaderboard'; +import { logError } from './util/logError'; -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 = usernameCache.get(id) ?? 'Unknown'; - 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]) => { + for (const [name, fn] of tup) { + promiseQueue.add(async () => { + const stopwatch = new Stopwatch(); try { - await fn(); - } catch (err: any) { - if (process.env.TEST) { - throw err; + const res = await fn(); + const [validResults, invalidResults] = partition(res, i => RoleResultSchema.safeParse(i).success); + results.push(...validResults); + if (invalidResults.length > 0) { + logError(`[RolesTask] Invalid results for ${name}: ${JSON.stringify(invalidResults)}`); } - failed.push(`${name} (${err.message})`); - logError(err); + } catch (err) { + logError(`[RolesTask] Error in ${name}: ${err}`); + } finally { + debugLog(`[RolesTask] Ran ${name} in ${stopwatch.stop()}`); } - }) - ); + }); + } + + await promiseQueue.onIdle(); + + debugLog(`Finished role functions, ${results.length} results`); + + const allBadgeIDs = uniqueArr(results.map(i => i.badge)).filter(notEmpty); + const allRoleIDs = uniqueArr(results.map(i => i.roleID)).filter(notEmpty); + + 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 loggedRawPrismaQuery(` +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); + } + } - const res = `**Roles** -${results.join('\n')} -${failed.length > 0 ? `Failed: ${failed.join(', ')}` : ''}`; + // 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 + } + }); + } + } + } + + 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/safeglobals.ts b/src/lib/safeglobals.ts index c1ea0174ad..3b0fbe4f80 100644 --- a/src/lib/safeglobals.ts +++ b/src/lib/safeglobals.ts @@ -1,2 +1,2 @@ -import './util/logger'; import './data/itemAliases'; +import './util/logger'; diff --git a/src/lib/settings/prisma.ts b/src/lib/settings/prisma.ts index a974743e44..4e719a2bf1 100644 --- a/src/lib/settings/prisma.ts +++ b/src/lib/settings/prisma.ts @@ -1,5 +1,5 @@ import type { Activity, Prisma } from '@prisma/client'; -import { activity_type_enum } from '@prisma/client'; +import type { activity_type_enum } from '@prisma/client'; import type { ActivityTaskData } from '../types/minions'; @@ -32,19 +32,3 @@ export async function countUsersWithItemInCl(itemID: number, ironmenOnly: boolea } return result; } - -export async function getUsersActivityCounts(user: MUser) { - const counts = await prisma.$queryRaw<{ type: activity_type_enum; count: bigint }[]>`SELECT type, COUNT(type) -FROM activity -WHERE user_id = ${BigInt(user.id)} -GROUP BY type;`; - - const result: Record = {} as Record; - for (const type of Object.values(activity_type_enum)) { - result[type] = 0; - } - for (const { count, type } of counts) { - result[type] = Number(count); - } - return result; -} diff --git a/src/lib/settings/settings.ts b/src/lib/settings/settings.ts index 98d19f3121..3201d53122 100644 --- a/src/lib/settings/settings.ts +++ b/src/lib/settings/settings.ts @@ -9,12 +9,13 @@ import type { User } from 'discord.js'; +import { isEmpty } from 'lodash'; 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 { handleInteractionError, interactionReply } from '../util/interactionReply'; +import { deferInteraction, handleInteractionError, interactionReply } from '../util/interactionReply'; import { logError } from '../util/logError'; import { convertStoredActivityToFlatActivity } from './prisma'; @@ -95,6 +96,7 @@ interface RunCommandArgs { guildID: string | undefined | null; interaction: ButtonInteraction | ChatInputCommandInteraction; continueDeltaMillis: number | null; + ephemeral?: boolean; } export async function runCommand({ commandName, @@ -106,12 +108,19 @@ 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; + await deferInteraction(interaction); + const channel = globalClient.channels.cache.get(channelID); const mahojiCommand = Array.from(globalClient.mahojiClient.commands.values()).find(c => c.name === commandName); - if (!mahojiCommand) throw new Error('No command found'); + if (!mahojiCommand || !channelIsSendable(channel)) { + await interactionReply(interaction, { + content: 'There was an error repeating your trip, I cannot find the channel you used the command in.', + ephemeral: true + }); + return null; + } const abstractCommand = convertMahojiCommandToAbstractCommand(mahojiCommand); const error: Error | null = null; @@ -129,19 +138,19 @@ export async function runCommand({ if (inhibitedReason) { inhibited = true; - if (inhibitedReason.silent) return null; + let response = + typeof inhibitedReason.reason! === 'string' ? inhibitedReason.reason : inhibitedReason.reason?.content!; + if (isEmpty(response)) { + response = 'You cannot use this command right now.'; + } await interactionReply(interaction, { - content: - typeof inhibitedReason.reason! === 'string' - ? inhibitedReason.reason - : inhibitedReason.reason?.content!, + content: response, ephemeral: true }); return null; } - if (Array.isArray(args)) throw new Error(`Had array of args for mahoji command called ${commandName}`); const result = await runMahojiCommand({ options: args, commandName, @@ -152,7 +161,14 @@ export async function runCommand({ user, interaction }); - if (result && !interaction.replied) await interactionReply(interaction, result); + if (result && !interaction.replied) { + await interactionReply( + interaction, + typeof result === 'string' + ? { content: result, ephemeral: ephemeral } + : { ...result, ephemeral: ephemeral } + ); + } return result; } catch (err: any) { await handleInteractionError(err, interaction); diff --git a/src/lib/simulation/nex.ts b/src/lib/simulation/nex.ts index a5f928a3d3..34f649c2a0 100644 --- a/src/lib/simulation/nex.ts +++ b/src/lib/simulation/nex.ts @@ -59,7 +59,7 @@ export function checkNexUser(user: MUser): [false] | [true, string] { if (!user.hasSkillReqs(minStats)) { return [true, `${tag} doesn't have the skill requirements: ${formatSkillRequirements(minStats)}.`]; } - if (user.GP < 1_000_000) return [true, `${tag} needs atleast 1m GP to cover potential deaths.`]; + if (user.GP < 1_000_000) return [true, `${tag} needs at least 1m GP to cover potential deaths.`]; const { offence, defence, rangeGear } = nexGearStats(user); if (offence < 50) { return [ diff --git a/src/lib/simulation/toa.ts b/src/lib/simulation/toa.ts index 5ac57423fe..810b071f63 100644 --- a/src/lib/simulation/toa.ts +++ b/src/lib/simulation/toa.ts @@ -247,7 +247,7 @@ const toaRequirements: { return true; }, desc: () => - `atleast ${BP_DARTS_NEEDED}x darts per raid, and using one of: ${ALLOWED_DARTS.map(i => i.name).join( + `at least ${BP_DARTS_NEEDED}x darts per raid, and using one of: ${ALLOWED_DARTS.map(i => i.name).join( ', ' )}, loaded in Blowpipe` }, @@ -278,7 +278,7 @@ const toaRequirements: { return true; }, desc: () => - `decent range gear (BiS is ${maxRangeGear.toString()}), atleast ${BOW_ARROWS_NEEDED}x arrows equipped, and one of these bows: ${REQUIRED_RANGE_WEAPONS.map( + `decent range gear (BiS is ${maxRangeGear.toString()}), at least ${BOW_ARROWS_NEEDED}x arrows equipped, and one of these bows: ${REQUIRED_RANGE_WEAPONS.map( itemNameFromID ).join(', ')}` }, @@ -335,11 +335,11 @@ const toaRequirements: { minimumSuppliesNeeded = minSuppliesWithAtkStr; } if (!user.owns(minimumSuppliesNeeded.clone().multiply(quantity))) { - return `You need atleast this much supplies: ${minimumSuppliesNeeded}.`; + return `You need at least this much supplies: ${minimumSuppliesNeeded}.`; } const bfCharges = BLOOD_FURY_CHARGES_PER_RAID * quantity; if (user.gear.melee.hasEquipped('Amulet of blood fury') && user.user.blood_fury_charges < bfCharges) { - return `You need atleast ${bfCharges} Blood fury charges to use it, otherwise it has to be unequipped: ${mentionCommand( + return `You need at least ${bfCharges} Blood fury charges to use it, otherwise it has to be unequipped: ${mentionCommand( globalClient, 'minion', 'charge' @@ -348,7 +348,7 @@ const toaRequirements: { const tumCharges = TUMEKEN_SHADOW_PER_RAID * quantity; if (user.gear.mage.hasEquipped("Tumeken's shadow") && user.user.tum_shadow_charges < tumCharges) { - return `You need atleast ${tumCharges} Tumeken's shadow charges to use it, otherwise it has to be unequipped: ${mentionCommand( + return `You need at least ${tumCharges} Tumeken's shadow charges to use it, otherwise it has to be unequipped: ${mentionCommand( globalClient, 'minion', 'charge' @@ -357,7 +357,7 @@ const toaRequirements: { return true; }, - desc: () => `Need atleast ${minimumSuppliesNeeded}` + desc: () => `Need at least ${minimumSuppliesNeeded}` }, { name: 'Rune Pouch', @@ -368,7 +368,7 @@ const toaRequirements: { } return true; }, - desc: () => `Need atleast ${minimumSuppliesNeeded}` + desc: () => `Need at least ${minimumSuppliesNeeded}` }, { name: 'Poison Protection', @@ -1044,7 +1044,7 @@ async function checkTOAUser( true, `${ user.usernameOrMention - } doesn't have enough Serpentine helm charges. You need atleast ${serpHelmCharges} charges to do a ${formatDuration( + } doesn't have enough Serpentine helm charges. You need at least ${serpHelmCharges} charges to do a ${formatDuration( duration )} TOA raid.` ]; @@ -1057,7 +1057,7 @@ async function checkTOAUser( if (kc < dividedRaidLevel) { return [ true, - `${user.usernameOrMention}, you need atleast ${dividedRaidLevel} TOA KC to ${ + `${user.usernameOrMention}, you need at least ${dividedRaidLevel} TOA KC to ${ teamSize === 2 ? 'duo' : 'solo' } a level ${raidLevel} TOA raid.` ]; @@ -1230,7 +1230,7 @@ export async function toaStartCommand( user: u }); } - await userStatsBankUpdate(u.id, 'toa_cost', realCost); + await userStatsBankUpdate(u, 'toa_cost', realCost); const effectiveCost = realCost.clone(); totalCost.add(effectiveCost); @@ -1637,21 +1637,3 @@ ${calculateBoostString(user)} return channelID === '1069176960523190292' ? { content: str, ephemeral: true } : str; } - -export function normalizeTOAUsers(data: TOAOptions) { - const _detailedUsers = data.detailedUsers; - const detailedUsers = ( - (Array.isArray(_detailedUsers[0]) ? _detailedUsers : [_detailedUsers]) as [string, number, number[]][][] - ).map(userArr => - userArr.map(user => ({ - id: user[0], - points: user[1], - deaths: user[2] - })) - ); - return detailedUsers; -} - -export function anyoneDiedInTOARaid(data: TOAOptions) { - return normalizeTOAUsers(data).some(userArr => userArr.some(user => user.deaths.length > 0)); -} diff --git a/src/lib/skilling/functions/calcsFarming.ts b/src/lib/skilling/functions/calcsFarming.ts index 170e12dd98..a234f023fe 100644 --- a/src/lib/skilling/functions/calcsFarming.ts +++ b/src/lib/skilling/functions/calcsFarming.ts @@ -1,5 +1,6 @@ import { randInt } from 'e'; +import { QuestID } from '../../minions/data/quests'; import type { Plant } from '../types'; import { SkillsEnum } from '../types'; @@ -28,6 +29,19 @@ export function calcNumOfPatches(plant: Plant, user: MUser, qp: number): [number break; } } + + if (user.user.finished_quest_ids.includes(QuestID.ChildrenOfTheSun)) { + switch (plant.seedType) { + case 'allotment': + numOfPatches += 2; + break; + case 'herb': + case 'flower': + numOfPatches += 1; + break; + } + } + return [numOfPatches]; } diff --git a/src/lib/skilling/types.ts b/src/lib/skilling/types.ts index a6b11fee6f..0fd0878070 100644 --- a/src/lib/skilling/types.ts +++ b/src/lib/skilling/types.ts @@ -331,7 +331,7 @@ export enum HunterTechniqueEnum { BoxTrapping = 'box trapping', ButterflyNetting = 'butterfly netting', DeadfallTrapping = 'deadfall trapping', - Falconry = 'falconry', + Falconry = 'hawking', MagicBoxTrapping = 'magic box trapping', NetTrapping = 'net trapping', PitfallTrapping = 'pitfall trapping', diff --git a/src/lib/slayer/slayerUtil.ts b/src/lib/slayer/slayerUtil.ts index 9a9237fe64..c1d9d5cc4e 100644 --- a/src/lib/slayer/slayerUtil.ts +++ b/src/lib/slayer/slayerUtil.ts @@ -11,8 +11,7 @@ import type { KillableMonster } from '../minions/types'; import { getNewUser } from '../settings/settings'; import { SkillsEnum } from '../skilling/types'; -import { bankHasItem, roll, stringMatches } from '../util'; -import itemID from '../util/itemID'; +import { roll, stringMatches } from '../util'; import { logError } from '../util/logError'; import { autoslayModes } from './constants'; import { slayerMasters } from './slayerMasters'; @@ -35,42 +34,53 @@ interface DetermineBoostParams { cbOpts: CombatOptionsEnum[]; user: MUser; monster: KillableMonster; - method?: PvMMethod | null; + methods?: PvMMethod[] | null; isOnTask?: boolean; wildyBurst?: boolean; } -export function determineBoostChoice(params: DetermineBoostParams) { - let boostChoice = 'none'; +export function determineCombatBoosts(params: DetermineBoostParams) { + // if EHP slayer (PvMMethod) the methods are initialized with boostMethods variable + const boostMethods = (params.methods ?? ['none']).flat().filter(method => method); - if (params.method && params.method === 'none') { - return boostChoice; + // check if user has cannon combat option turned on + if (params.cbOpts.includes(CombatOptionsEnum.AlwaysCannon)) { + boostMethods.includes('cannon') ? null : boostMethods.push('cannon'); } - if (params.method && params.method === 'chinning') { - boostChoice = 'chinning'; - } else if (params.method && params.method === 'barrage') { - boostChoice = 'barrage'; - } else if (params.method && params.method === 'burst') { - boostChoice = 'burst'; - } else if (params.method && params.method === 'cannon') { - boostChoice = 'cannon'; - } else if ( - params.cbOpts.includes(CombatOptionsEnum.AlwaysIceBarrage) && - (params.monster?.canBarrage || params.wildyBurst) - ) { - boostChoice = 'barrage'; - } else if ( - params.cbOpts.includes(CombatOptionsEnum.AlwaysIceBurst) && - (params.monster?.canBarrage || params.wildyBurst) - ) { - boostChoice = 'burst'; - } else if (params.cbOpts.includes(CombatOptionsEnum.AlwaysCannon)) { - boostChoice = 'cannon'; + + // check for special burst case under wildyBurst variable + if (params.wildyBurst) { + if (params.cbOpts.includes(CombatOptionsEnum.AlwaysIceBarrage)) { + boostMethods.includes('barrage') ? null : boostMethods.push('barrage'); + } + if (params.cbOpts.includes(CombatOptionsEnum.AlwaysIceBurst)) { + boostMethods.includes('burst') ? null : boostMethods.push('burst'); + } } - if (boostChoice === 'barrage' && params.user.skillLevel(SkillsEnum.Magic) < 94) { - boostChoice = 'burst'; + // check if the monster can be barraged + if (params.monster.canBarrage) { + // check if the monster exists in catacombs + if (params.monster.existsInCatacombs) { + if (params.cbOpts.includes(CombatOptionsEnum.AlwaysIceBarrage)) { + boostMethods.includes('barrage') ? null : boostMethods.push('barrage'); + } + if (params.cbOpts.includes(CombatOptionsEnum.AlwaysIceBurst)) { + boostMethods.includes('burst') ? null : boostMethods.push('burst'); + } + } else if (!params.monster.cannonMulti) { + // prevents cases such as: cannoning in singles but receiving multi combat bursting boost + return boostMethods; + } else { + if (params.cbOpts.includes(CombatOptionsEnum.AlwaysIceBarrage)) { + boostMethods.includes('barrage') ? null : boostMethods.push('barrage'); + } + if (params.cbOpts.includes(CombatOptionsEnum.AlwaysIceBurst)) { + boostMethods.includes('burst') ? null : boostMethods.push('burst'); + } + } } - return boostChoice; + + return boostMethods; } export async function calculateSlayerPoints(currentStreak: number, master: SlayerMaster, user: MUser) { @@ -144,7 +154,7 @@ function userCanUseTask(user: MUser, task: AssignableSlayerTask, master: SlayerM // Slayer unlock restrictions: const lmon = task.monster.name.toLowerCase(); const lmast = master.name.toLowerCase(); - if (lmon === 'grotesque guardians' && !bankHasItem(user.bank.bank, itemID('Brittle key'))) return false; + if (lmon === 'grotesque guardians' && !user.bank.has('Brittle key')) return false; if (lmon === 'lizardman' && !myUnlocks.includes(SlayerTaskUnlocksEnum.ReptileGotRipped)) return false; if (lmon === 'red dragon' && !myUnlocks.includes(SlayerTaskUnlocksEnum.SeeingRed)) return false; if (lmon === 'mithril dragon' && !myUnlocks.includes(SlayerTaskUnlocksEnum.IHopeYouMithMe)) return false; @@ -290,6 +300,9 @@ export function getCommonTaskName(task: Monster) { case Monsters.RevenantImp.id: commonName = 'Revenant'; break; + case Monsters.DagannothPrime.id: + commonName = 'Dagannoth Kings'; + break; default: } if (commonName !== 'TzHaar' && !commonName.endsWith('s')) commonName += 's'; diff --git a/src/lib/slayer/tasks/bossTasks.ts b/src/lib/slayer/tasks/bossTasks.ts index cbab364148..6ffcfb8338 100644 --- a/src/lib/slayer/tasks/bossTasks.ts +++ b/src/lib/slayer/tasks/bossTasks.ts @@ -33,7 +33,7 @@ export const bossTasks: AssignableSlayerTask[] = [ monster: Monsters.Callisto, amount: [3, 35], weight: 1, - monsters: [Monsters.Callisto.id], + monsters: [Monsters.Callisto.id, Monsters.Artio.id], isBoss: true, wilderness: true }, @@ -90,27 +90,7 @@ export const bossTasks: AssignableSlayerTask[] = [ levelRequirements: { prayer: 43 }, - monsters: [Monsters.DagannothPrime.id], - isBoss: true - }, - { - monster: Monsters.DagannothSupreme, - amount: [3, 35], - weight: 1, - levelRequirements: { - prayer: 43 - }, - monsters: [Monsters.DagannothSupreme.id], - isBoss: true - }, - { - monster: Monsters.DagannothRex, - amount: [3, 35], - weight: 1, - levelRequirements: { - prayer: 43 - }, - monsters: [Monsters.DagannothRex.id], + monsters: [Monsters.DagannothPrime.id, Monsters.DagannothSupreme.id, Monsters.DagannothRex.id], isBoss: true }, { @@ -219,7 +199,7 @@ export const bossTasks: AssignableSlayerTask[] = [ monster: Monsters.Venenatis, amount: [3, 35], weight: 1, - monsters: [Monsters.Venenatis.id], + monsters: [Monsters.Venenatis.id, Monsters.Spindel.id], isBoss: true, wilderness: true }, @@ -227,7 +207,7 @@ export const bossTasks: AssignableSlayerTask[] = [ monster: Monsters.Vetion, amount: [3, 35], weight: 1, - monsters: [Monsters.Vetion.id], + monsters: [Monsters.Vetion.id, Monsters.Calvarion.id], isBoss: true, wilderness: true }, @@ -260,7 +240,7 @@ export const wildernessBossTasks: AssignableSlayerTask[] = [ monster: Monsters.Callisto, amount: [3, 35], weight: 1, - monsters: [Monsters.Callisto.id], + monsters: [Monsters.Callisto.id, Monsters.Artio.id], isBoss: true, wilderness: true }, @@ -300,7 +280,7 @@ export const wildernessBossTasks: AssignableSlayerTask[] = [ monster: Monsters.Venenatis, amount: [3, 35], weight: 1, - monsters: [Monsters.Venenatis.id], + monsters: [Monsters.Venenatis.id, Monsters.Spindel.id], isBoss: true, wilderness: true }, @@ -308,7 +288,7 @@ export const wildernessBossTasks: AssignableSlayerTask[] = [ monster: Monsters.Vetion, amount: [3, 35], weight: 1, - monsters: [Monsters.Vetion.id], + monsters: [Monsters.Vetion.id, Monsters.Calvarion.id], isBoss: true, wilderness: true } diff --git a/src/lib/slayer/tasks/krystiliaTasks.ts b/src/lib/slayer/tasks/krystiliaTasks.ts index 40b5d56071..f32758963e 100644 --- a/src/lib/slayer/tasks/krystiliaTasks.ts +++ b/src/lib/slayer/tasks/krystiliaTasks.ts @@ -67,6 +67,8 @@ export const krystiliaTasks: AssignableSlayerTask[] = [ amount: [8, 16], weight: 4, monsters: [Monsters.BlackDragon.id], + extendedAmount: [40, 60], + extendedUnlockId: SlayerTaskUnlocksEnum.FireAndDarkness, unlocked: true, wilderness: true }, @@ -244,6 +246,8 @@ export const krystiliaTasks: AssignableSlayerTask[] = [ amount: [75, 125], weight: 5, monsters: [Monsters.GreaterNechryael.id], + extendedAmount: [200, 250], + extendedUnlockId: SlayerTaskUnlocksEnum.NechsPlease, slayerLevel: 80, unlocked: true, wilderness: true diff --git a/src/lib/startupScripts.ts b/src/lib/startupScripts.ts index f364947982..fab5ab93fe 100644 --- a/src/lib/startupScripts.ts +++ b/src/lib/startupScripts.ts @@ -1,39 +1,61 @@ import { Items } from 'oldschooljs'; - -import { logError } from './util/logError'; +import { globalConfig } from './constants'; const startupScripts: { sql: string; ignoreErrors?: true }[] = []; -const arrayColumns = [ - ['clientStorage', 'userBlacklist'], - ['clientStorage', 'guildBlacklist'], - ['guilds', 'disabledCommands'], - ['guilds', 'staffOnlyChannels'], - ['users', 'badges'], - ['users', 'bitfield'], - ['users', 'favoriteItems'], - ['users', 'favorite_alchables'], - ['users', 'favorite_food'], - ['users', 'favorite_bh_seeds'], - ['users', 'attack_style'], - ['users', 'combat_options'], - ['users', 'ironman_alts'], - ['users', 'slayer.unlocks'], - ['users', 'slayer.blocked_ids'], - ['users', 'slayer.autoslay_options'] -]; +startupScripts.push({ + sql: `CREATE OR REPLACE FUNCTION add_item_to_bank( + bank JSONB, + key TEXT, + quantity INT +) RETURNS JSONB LANGUAGE plpgsql AS $$ +BEGIN + RETURN ( + CASE + WHEN bank ? key THEN + jsonb_set( + bank, + ARRAY[key], + to_jsonb((bank->>key)::INT + quantity) + ) + ELSE + jsonb_set( + bank, + ARRAY[key], + to_jsonb(quantity) + ) + END + ); +END; +$$;` +}); -for (const [table, column] of arrayColumns) { - startupScripts.push({ - sql: `UPDATE "${table}" SET "${column}" = '{}' WHERE "${column}" IS NULL;` - }); - startupScripts.push({ - sql: ` -ALTER TABLE "${table}" - ALTER COLUMN "${column}" SET DEFAULT '{}', - ALTER COLUMN "${column}" SET NOT NULL;` - }); -} +startupScripts.push({ + sql: `CREATE OR REPLACE FUNCTION remove_item_from_bank( + bank JSONB, + key TEXT, + quantity INT +) RETURNS JSONB LANGUAGE plpgsql AS $$ +DECLARE + current_value INT; +BEGIN + IF bank ? key THEN + current_value := (bank->>key)::INT - quantity; + IF current_value > 0 THEN + RETURN jsonb_set( + bank, + ARRAY[key], + to_jsonb(current_value) + ); + ELSE + RETURN bank - key; + END IF; + ELSE + RETURN bank; + END IF; +END; +$$;` +}); interface CheckConstraint { table: string; @@ -42,29 +64,17 @@ interface CheckConstraint { body: string; } const checkConstraints: CheckConstraint[] = [ - { - table: 'users', - column: 'lms_points', - name: 'users_lms_points_min', - body: 'lms_points >= 0' - }, { table: 'users', column: '"GP"', name: 'users_gp', body: '"GP" >= 0' }, - { - table: 'users', - column: '"QP"', - name: 'users_qp', - body: '"QP" >= 0' - }, { table: 'ge_listing', column: 'asking_price_per_item', name: 'asking_price_per_item_min', - body: 'asking_price_per_item_min >= 1' + body: 'asking_price_per_item >= 1' }, { table: 'ge_listing', @@ -115,13 +125,39 @@ const checkConstraints: CheckConstraint[] = [ body: 'quantity >= 0' } ]; + for (const { table, name, body } of checkConstraints) { - startupScripts.push({ sql: `ALTER TABLE ${table} ADD CONSTRAINT ${name} CHECK (${body});`, ignoreErrors: true }); + startupScripts.push({ + sql: `DO $$ +BEGIN + IF NOT EXISTS (SELECT 1 + FROM information_schema.check_constraints + WHERE constraint_name = '${name}' + AND constraint_schema = 'public') + THEN + ALTER TABLE "${table}" ADD CONSTRAINT "${name}" CHECK (${body}); + END IF; +END$$;` + }); } + startupScripts.push({ sql: 'CREATE UNIQUE INDEX IF NOT EXISTS activity_only_one_task ON activity (user_id, completed) WHERE NOT completed;' }); +startupScripts.push({ + sql: `CREATE INDEX IF NOT EXISTS idx_ge_listing_buy_filter_sort +ON ge_listing (type, fulfilled_at, cancelled_at, user_id, asking_price_per_item DESC, created_at ASC);` +}); +startupScripts.push({ + sql: `CREATE INDEX IF NOT EXISTS idx_ge_listing_sell_filter_sort +ON ge_listing (type, fulfilled_at, cancelled_at, user_id, asking_price_per_item ASC, created_at ASC);` +}); + +startupScripts.push({ + sql: `CREATE INDEX IF NOT EXISTS ge_transaction_sell_listing_id_created_at_idx +ON ge_transaction (sell_listing_id, created_at DESC);` +}); const itemMetaDataNames = Items.map(item => `(${item.id}, '${item.name.replace(/'/g, "''")}')`).join(', '); const itemMetaDataQuery = ` INSERT INTO item_metadata (id, name) @@ -131,13 +167,10 @@ DO UPDATE SET name = EXCLUDED.name WHERE item_metadata.name IS DISTINCT FROM EXCLUDED.name; `; - -startupScripts.push({ sql: itemMetaDataQuery }); +if (globalConfig.isProduction) { + startupScripts.push({ sql: itemMetaDataQuery }); +} export async function runStartupScripts() { - for (const query of startupScripts) { - await prisma - .$queryRawUnsafe(query.sql) - .catch(err => (query.ignoreErrors ? null : logError(`Startup script failed: ${err.message} ${query.sql}`))); - } + await prisma.$transaction(startupScripts.map(query => prisma.$queryRawUnsafe(query.sql))); } diff --git a/src/lib/structures/Bank.ts b/src/lib/structures/Bank.ts index 97a5b71144..adc1d72763 100644 --- a/src/lib/structures/Bank.ts +++ b/src/lib/structures/Bank.ts @@ -1,4 +1,4 @@ -import { GeneralBank, type GeneralBankType } from '@oldschoolgg/toolkit/dist/lib/GeneralBank'; +import { GeneralBank, type GeneralBankType } from '@oldschoolgg/toolkit'; import type { DegradeableItem } from '../degradeableItems'; import { degradeableItems } from '../degradeableItems'; diff --git a/src/lib/structures/MUserStats.ts b/src/lib/structures/MUserStats.ts index 8dcab5db95..12d774918a 100644 --- a/src/lib/structures/MUserStats.ts +++ b/src/lib/structures/MUserStats.ts @@ -9,10 +9,22 @@ import { getToaKCs } from '../util/smallUtils'; export class MUserStats { userStats: UserStats; sacrificedBank: Bank; + titheFarmsCompleted: number; + lapsScores: ItemBank; + openableScores: Bank; + kcBank: ItemBank; + highGambles: number; + gotrRiftSearches: number; constructor(userStats: UserStats) { this.userStats = userStats; this.sacrificedBank = new Bank().add(this.userStats.sacrificed_bank as ItemBank); + this.titheFarmsCompleted = this.userStats.tithe_farms_completed; + this.lapsScores = userStats.laps_scores as ItemBank; + this.openableScores = new Bank().add(userStats.openable_scores as ItemBank); + this.kcBank = userStats.monster_scores as ItemBank; + this.highGambles = userStats.high_gambles; + this.gotrRiftSearches = userStats.gotr_rift_searches; } static async fromID(id: string) { @@ -28,10 +40,6 @@ export class MUserStats { return new MUserStats(userStats); } - get lapsScores() { - return this.userStats.laps_scores as ItemBank; - } - getToaKCs() { return getToaKCs(this.userStats.toa_raid_levels_bank); } diff --git a/src/lib/structures/Requirements.ts b/src/lib/structures/Requirements.ts index 180cb62241..59280d2fd8 100644 --- a/src/lib/structures/Requirements.ts +++ b/src/lib/structures/Requirements.ts @@ -1,4 +1,4 @@ -import type { Minigame } from '@prisma/client'; +import type { Minigame, PlayerOwnedHouse, activity_type_enum } from '@prisma/client'; import { calcWhatPercent, objectEntries } from 'e'; import type { Bank } from 'oldschooljs'; @@ -7,7 +7,7 @@ import { getParsedStashUnits } from '../../mahoji/lib/abstracted_commands/stashU import type { ClueTier } from '../clues/clueTiers'; import type { BitField } from '../constants'; import { BOT_TYPE, BitFieldData } from '../constants'; -import { diaries, userhasDiaryIDTier } from '../diaries'; +import { diaries, userhasDiaryTierSync } from '../diaries'; import { effectiveMonsters } from '../minions/data/killableMonsters'; import type { ClueBank, DiaryID, DiaryTierName } from '../minions/types'; import type { RobochimpUser } from '../roboChimp'; @@ -28,19 +28,12 @@ interface RequirementUserArgs { stats: MUserStats; roboChimpUser: RobochimpUser; clueCounts: ClueBank; + poh: PlayerOwnedHouse; + uniqueRunesCrafted: number[]; + uniqueActivitiesDone: activity_type_enum[]; } -type ManualHasFunction = ( - args: RequirementUserArgs -) => - | Promise - | RequirementFailure[] - | undefined - | Promise - | string - | Promise - | boolean - | Promise; +type ManualHasFunction = (args: RequirementUserArgs) => RequirementFailure[] | undefined | string | boolean; type Requirement = { name?: string; @@ -157,15 +150,12 @@ export class Requirements { return this; } - async checkSingleRequirement( - requirement: Requirement, - userArgs: RequirementUserArgs - ): Promise { + checkSingleRequirement(requirement: Requirement, userArgs: RequirementUserArgs): RequirementFailure[] { const { user, stats, minigames, clueCounts } = userArgs; const results: RequirementFailure[] = []; if ('has' in requirement) { - const result = await requirement.has(userArgs); + const result = requirement.has(userArgs); if (typeof result === 'boolean') { if (!result) { results.push({ reason: requirement.name }); @@ -278,17 +268,18 @@ export class Requirements { } if ('diaryRequirement' in requirement) { - const unmetDiaries = ( - await Promise.all( - requirement.diaryRequirement.map(async ([diary, tier]) => { - const res = await userhasDiaryIDTier(user, diary, tier); - return { - has: res.hasDiary, - tierName: `${tier} ${res.diaryGroup.name}` - }; - }) - ) - ).filter(i => !i.has); + const unmetDiaries = requirement.diaryRequirement + .map(([diary, tier]) => { + const { hasDiary, diaryGroup } = userhasDiaryTierSync(user, [diary, tier], { + stats, + minigameScores: minigames + }); + return { + has: hasDiary, + tierName: `${tier} ${diaryGroup.name}` + }; + }) + .filter(i => !i.has); if (unmetDiaries.length > 0) { results.push({ reason: `You need to finish these achievement diaries: ${unmetDiaries @@ -309,7 +300,7 @@ export class Requirements { } if ('OR' in requirement) { - const orResults = await Promise.all(requirement.OR.map(req => this.checkSingleRequirement(req, userArgs))); + const orResults = requirement.OR.map(req => this.checkSingleRequirement(req, userArgs)); if (!orResults.some(i => i.length === 0)) { results.push({ reason: `You need to meet one of these requirements:\n${orResults.map((res, index) => { @@ -322,7 +313,7 @@ export class Requirements { return results; } - async check(user: MUser) { + static async fetchRequiredData(user: MUser) { const minigames = await user.fetchMinigames(); const stashUnits = await getParsedStashUnits(user.id); const stats = await MUserStats.fromID(user.id); @@ -330,21 +321,44 @@ export class Requirements { const clueCounts = BOT_TYPE === 'OSB' ? stats.clueScoresFromOpenables() : (await user.calcActualClues()).clueCounts; - const requirementResults = this.requirements.map(async i => ({ - result: await this.checkSingleRequirement(i, { - user, - minigames, - stashUnits, - stats, - roboChimpUser, - clueCounts - }), + const [_uniqueRunesCrafted, uniqueActivitiesDone, poh] = await prisma.$transaction([ + prisma.$queryRaw<{ rune_id: string }[]>`SELECT DISTINCT(data->>'runeID') AS rune_id +FROM activity +WHERE user_id = ${BigInt(user.id)} +AND type = 'Runecraft' +AND data->>'runeID' IS NOT NULL;`, + prisma.$queryRaw<{ type: activity_type_enum }[]>`SELECT DISTINCT(type) +FROM activity +WHERE user_id = ${BigInt(user.id)} +GROUP BY type;`, + prisma.playerOwnedHouse.upsert({ where: { user_id: user.id }, update: {}, create: { user_id: user.id } }) + ]); + const uniqueRunesCrafted = _uniqueRunesCrafted.map(i => Number(i.rune_id)); + return { + user, + minigames, + stashUnits, + stats, + roboChimpUser, + clueCounts, + poh, + uniqueRunesCrafted, + uniqueActivitiesDone: uniqueActivitiesDone.map(i => i.type) + }; + } + + static async checkMany(user: MUser, requirements: Requirements[]) { + const data = await Requirements.fetchRequiredData(user); + return requirements.map(i => i.check(data)); + } + + check(data: Awaited>) { + const results = this.requirements.map(i => ({ + result: this.checkSingleRequirement(i, data), requirement: i })); - const results = await Promise.all(requirementResults); const flatReasons = results.flatMap(r => r.result); - const totalRequirements = this.requirements.length; const metRequirements = results.filter(i => i.result.length === 0).length; const completionPercentage = calcWhatPercent(metRequirements, totalRequirements); diff --git a/src/lib/systemInfo.ts b/src/lib/systemInfo.ts index 077d62c7d1..846ae04116 100644 --- a/src/lib/systemInfo.ts +++ b/src/lib/systemInfo.ts @@ -9,9 +9,7 @@ async function getPostgresVersion() { const version = result[0].version.split(',')[0]; return version; } catch (err) { - await client.$disconnect(); - console.log('Failed to execute postgres query. Is postgres running?'); - process.exit(1); + return 'UNKNOWN'; } finally { await client.$disconnect(); } diff --git a/src/lib/tickers.ts b/src/lib/tickers.ts index 9813ff8dba..d94ad99023 100644 --- a/src/lib/tickers.ts +++ b/src/lib/tickers.ts @@ -1,14 +1,15 @@ +import { Stopwatch } from '@oldschoolgg/toolkit'; import type { TextChannel } from 'discord.js'; import { ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder } from 'discord.js'; import { Time, noOp, randInt, removeFromArr, shuffleArr } from 'e'; +import { TimerManager } from '@sapphire/timer-manager'; import { production } from '../config'; import { userStatsUpdate } from '../mahoji/mahojiSettings'; import { mahojiUserSettingsUpdate } from './MUser'; import { processPendingActivities } from './Task'; import { BitField, Channel, PeakTier, informationalButtons } from './constants'; import { GrandExchange } from './grandExchange'; -import { cacheGEPrices } from './marketPrices'; import { collectMetrics } from './metrics'; import { queryCountStore } from './settings/prisma'; import { runCommand } from './settings/settings'; @@ -69,9 +70,16 @@ export interface Peak { /** * Tickers should idempotent, and be able to run at any time. */ -export const tickers: { name: string; interval: number; timer: NodeJS.Timeout | null; cb: () => Promise }[] = [ +export const tickers: { + name: string; + startupWait?: number; + interval: number; + timer: NodeJS.Timeout | null; + cb: () => Promise; +}[] = [ { name: 'giveaways', + startupWait: Time.Second * 30, interval: Time.Second * 10, timer: null, cb: async () => { @@ -109,6 +117,7 @@ export const tickers: { name: string; interval: number; timer: NodeJS.Timeout | }, { name: 'minion_activities', + startupWait: Time.Second * 10, timer: null, interval: production ? Time.Second * 5 : 500, cb: async () => { @@ -118,6 +127,7 @@ export const tickers: { name: string; interval: number; timer: NodeJS.Timeout | { name: 'daily_reminders', interval: Time.Minute * 3, + startupWait: Time.Minute, timer: null, cb: async () => { const result = await prisma.$queryRawUnsafe<{ id: string; last_daily_timestamp: bigint }[]>( @@ -197,6 +207,7 @@ WHERE bitfield && '{2,3,4,5,6,7,8,12,21,24}'::int[] AND user_stats."last_daily_t }, { name: 'farming_reminder_ticker', + startupWait: Time.Minute, interval: Time.Minute * 3.5, timer: null, cb: async () => { @@ -211,7 +222,6 @@ WHERE bitfield && '{2,3,4,5,6,7,8,12,21,24}'::int[] AND user_stats."last_daily_t BitField.IsPatronTier4, BitField.IsPatronTier5, BitField.IsPatronTier6, - BitField.isContributor, BitField.isModerator ] } @@ -317,6 +327,7 @@ WHERE bitfield && '{2,3,4,5,6,7,8,12,21,24}'::int[] AND user_stats."last_daily_t { name: 'support_channel_messages', timer: null, + startupWait: Time.Second * 22, interval: Time.Minute * 20, cb: async () => { if (!production) return; @@ -340,6 +351,7 @@ WHERE bitfield && '{2,3,4,5,6,7,8,12,21,24}'::int[] AND user_stats."last_daily_t }, { name: 'ge_channel_messages', + startupWait: Time.Second * 19, timer: null, interval: Time.Minute * 20, cb: async () => { @@ -361,19 +373,12 @@ WHERE bitfield && '{2,3,4,5,6,7,8,12,21,24}'::int[] AND user_stats."last_daily_t }, { name: 'ge_ticker', + startupWait: Time.Second * 30, timer: null, - interval: Time.Second * 3, + interval: Time.Second * 10, cb: async () => { await GrandExchange.tick(); } - }, - { - name: 'Cache g.e prices and validate', - timer: null, - interval: Time.Hour * 4, - cb: async () => { - await cacheGEPrices(); - } } ]; @@ -383,14 +388,22 @@ export function initTickers() { const fn = async () => { try { if (globalClient.isShuttingDown) return; + const stopwatch = new Stopwatch().start(); await ticker.cb(); + stopwatch.stop(); + if (stopwatch.duration > 100) { + debugLog(`Ticker ${ticker.name} took ${stopwatch}`); + } } catch (err) { logError(err); debugLog(`${ticker.name} ticker errored`, { type: 'TICKER' }); } finally { - ticker.timer = setTimeout(fn, ticker.interval); + if (ticker.timer) TimerManager.clearTimeout(ticker.timer); + ticker.timer = TimerManager.setTimeout(fn, ticker.interval); } }; - fn(); + ticker.timer = TimerManager.setTimeout(() => { + fn(); + }, ticker.startupWait ?? 1); } } diff --git a/src/lib/types/minions.ts b/src/lib/types/minions.ts index edeb891466..fb5639c86f 100644 --- a/src/lib/types/minions.ts +++ b/src/lib/types/minions.ts @@ -124,13 +124,13 @@ export interface ConstructionActivityTaskOptions extends ActivityTaskOptions { export interface MonsterActivityTaskOptions extends ActivityTaskOptions { type: 'MonsterKilling'; - monsterID: number; - quantity: number; + mi: number; + q: number; iQty?: number; usingCannon?: boolean; cannonMulti?: boolean; chinning?: boolean; - burstOrBarrage?: number; + bob?: number; died?: boolean; pkEncounters?: number; hasWildySupplies?: boolean; @@ -139,9 +139,8 @@ export interface MonsterActivityTaskOptions extends ActivityTaskOptions { export interface ClueActivityTaskOptions extends ActivityTaskOptions { type: 'ClueCompletion'; - - clueID: number; - quantity: number; + ci: number; + q: number; implingID?: number; implingClues?: number; } @@ -464,6 +463,9 @@ export interface ColoTaskOptions extends ActivityTaskOptions { diedAt?: number; loot?: ItemBank; maxGlory: number; + scytheCharges: number; + venatorBowCharges: number; + bloodFuryCharges: number; } type UserID = string; diff --git a/src/lib/util.ts b/src/lib/util.ts index 768fcf0338..e118fcc0c2 100644 --- a/src/lib/util.ts +++ b/src/lib/util.ts @@ -18,10 +18,12 @@ import { Time, objectEntries } from 'e'; import type { Bank } from 'oldschooljs'; import { bool, integer, nativeMath, nodeCrypto, real } from 'random-js'; +import type { Prisma } from '@prisma/client'; +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 { BitField, globalConfig, projectiles } from './constants'; import { getSimilarItems } from './data/similarItems'; import type { DefenceGearStat, GearSetupType, OffenceGearStat } from './gear/types'; import { GearSetupTypes, GearStat } from './gear/types'; @@ -34,10 +36,12 @@ import type { GroupMonsterActivityTaskOptions, NexTaskOptions, RaidsOptions, + TOAOptions, TheatreOfBloodTaskOptions } from './types/minions'; import { getItem } from './util/getOSItem'; import itemID from './util/itemID'; +import { makeBadgeString } from './util/makeBadgeString'; import { itemNameFromID } from './util/smallUtils'; export * from '@oldschoolgg/toolkit'; @@ -210,7 +214,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 +333,31 @@ export function skillingPetDropRate( return { petDropRate: dropRate }; } -function getBadges(user: MUser | string | bigint) { - if (typeof user === 'string' || typeof user === 'bigint') { - return badgesCache.get(user.toString()) ?? ''; - } - return user.badgeString; +const usernameWithBadgesCache = new LRUCache({ max: 2000 }); + +export async function getUsername(_id: string | bigint): Promise { + const id = _id.toString(); + const cached = usernameWithBadgesCache.get(id); + if (cached) return cached; + const user = await prisma.user.findFirst({ + where: { + id + }, + select: { + username: true, + badges: true, + minion_ironman: true + } + }); + if (!user?.username) return 'Unknown'; + const badges = makeBadgeString(user.badges, user.minion_ironman); + const newValue = `${badges ? `${badges} ` : ''}${user.username}`; + usernameWithBadgesCache.set(id, newValue); + return newValue; } -export function getUsername(id: string | bigint, withBadges = true) { - let username = usernameCache.get(id.toString()) ?? 'Unknown'; - if (withBadges) username = `${getBadges(id)} ${username}`; - return username; +export function getUsernameSync(_id: string | bigint) { + return usernameWithBadgesCache.get(_id.toString()) ?? 'Unknown'; } export function awaitMessageComponentInteraction({ @@ -366,13 +384,23 @@ export function awaitMessageComponentInteraction({ }); } -export async function runTimedLoggedFn(name: string, fn: () => Promise) { - debugLog(`Starting ${name}...`); +export async function runTimedLoggedFn(name: string, fn: () => Promise, threshholdToLog = 100): Promise { + const logger = globalConfig.isProduction ? debugLog : console.log; const stopwatch = new Stopwatch(); stopwatch.start(); - await fn(); + const result = await fn(); stopwatch.stop(); - debugLog(`Finished ${name} in ${stopwatch.toString()}`); + if (!globalConfig.isProduction || stopwatch.duration > threshholdToLog) { + logger(`Took ${stopwatch} to do ${name}`); + } + return result; +} + +export function logWrapFn Promise>( + name: string, + fn: T +): (...args: Parameters) => ReturnType { + return (...args: Parameters): ReturnType => runTimedLoggedFn(name, () => fn(...args)) as ReturnType; } export function isModOrAdmin(user: MUser) { @@ -404,3 +432,24 @@ export function checkRangeGearWeapon(gear: Gear) { ammo }; } +export function normalizeTOAUsers(data: TOAOptions) { + const _detailedUsers = data.detailedUsers; + const detailedUsers = ( + (Array.isArray(_detailedUsers[0]) ? _detailedUsers : [_detailedUsers]) as [string, number, number[]][][] + ).map(userArr => + userArr.map(user => ({ + id: user[0], + points: user[1], + deaths: user[2] + })) + ); + return detailedUsers; +} + +export function anyoneDiedInTOARaid(data: TOAOptions) { + return normalizeTOAUsers(data).some(userArr => userArr.some(user => user.deaths.length > 0)); +} + +export type JsonKeys = { + [K in keyof T]: T[K] extends Prisma.JsonValue ? K : never; +}[keyof T]; diff --git a/src/lib/util/activityInArea.ts b/src/lib/util/activityInArea.ts index c0e05fe3b3..a953d048e0 100644 --- a/src/lib/util/activityInArea.ts +++ b/src/lib/util/activityInArea.ts @@ -25,7 +25,7 @@ const WorldLocationsChecker = [ if ( activity.type === 'MonsterKilling' && [Monsters.DarkBeast.id, Monsters.PrifddinasElf.id].includes( - (activity as MonsterActivityTaskOptions).monsterID + (activity as MonsterActivityTaskOptions).mi ) ) { return true; diff --git a/src/lib/util/addSubTaskToActivityTask.ts b/src/lib/util/addSubTaskToActivityTask.ts index 21c61a6342..8e0fe2f06c 100644 --- a/src/lib/util/addSubTaskToActivityTask.ts +++ b/src/lib/util/addSubTaskToActivityTask.ts @@ -1,4 +1,4 @@ -import { UserError } from '@oldschoolgg/toolkit/dist/lib/UserError'; +import { UserError } from '@oldschoolgg/toolkit'; import { activitySync } from '../settings/settings'; import type { ActivityTaskData, ActivityTaskOptions } from '../types/minions'; @@ -39,24 +39,28 @@ export default async function addSubTaskToActivityTask`SELECT DISTINCT(user_id::text) -FROM command_usage -WHERE date > now() - INTERVAL '72 hours';`, - prisma.$queryRaw<{ 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];` - ]); +export const syncActiveUserIDs = async () => { + const users = await prisma.$queryRawUnsafe< + { user_id: string }[] + >(`SELECT DISTINCT(${Prisma.ActivityScalarFieldEnum.user_id}::text) +FROM activity +WHERE finish_date > now() - INTERVAL '48 hours'`); - for (const id of [...users.map(i => i.user_id), ...otherUsers.map(i => i.id)]) { + const perkTierUsers = await roboChimpClient.$queryRawUnsafe<{ id: string }[]>(`SELECT id::text +FROM "user" +WHERE perk_tier > 0;`); + + for (const id of [...users.map(i => i.user_id), ...perkTierUsers.map(i => i.id)]) { CACHED_ACTIVE_USER_IDS.add(id); } debugLog(`${CACHED_ACTIVE_USER_IDS.size} cached active user IDs`); -} +}; export function memoryAnalysis() { const guilds = globalClient.guilds.cache.size; @@ -97,11 +96,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 +162,5 @@ export function cacheCleanup() { } } }); - - stopwatch.stop(); - debugLog(`Cache Cleanup Finish After ${stopwatch.toString()}`, { - type: 'CACHE_CLEANUP' - }); }); } diff --git a/src/lib/util/chatHeadImage.ts b/src/lib/util/chatHeadImage.ts index 36c117262c..0aee1092b1 100644 --- a/src/lib/util/chatHeadImage.ts +++ b/src/lib/util/chatHeadImage.ts @@ -34,7 +34,7 @@ const names: Record = { santa: 'Santa', izzy: "Cap'n Izzy No-Beard", alry: 'Alry the Angler', - ketKeh: 'Tzhaar-Ket-Keh', + ketKeh: 'TzHaar-Ket-Keh', gertrude: 'Gertrude', antiSanta: 'Anti-Santa', bunny: 'Easter Bunny', diff --git a/src/lib/util/clLeaderboard.ts b/src/lib/util/clLeaderboard.ts index 43def04ef5..175a9c5596 100644 --- a/src/lib/util/clLeaderboard.ts +++ b/src/lib/util/clLeaderboard.ts @@ -1,55 +1,64 @@ -import type { UserEvent } from '@prisma/client'; - +import { stringMatches } from '@oldschoolgg/toolkit'; import { userEventsToMap } from './userEvents'; -export async function fetchCLLeaderboard({ - ironmenOnly, - items, - resultLimit, - method = 'cl_array', - userEvents -}: { - ironmenOnly: boolean; - items: number[]; - resultLimit: number; - method?: 'cl_array' | 'raw_cl'; - userEvents: UserEvent[] | null; -}) { - const userEventMap = userEventsToMap(userEvents); - const userIds = Array.from(userEventMap.keys()); - if (method === 'cl_array') { - const userIdsList = userIds.length > 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/lib/util/commandUsage.ts b/src/lib/util/commandUsage.ts index 90821b3a48..153064fcdb 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 type { Prisma, command_name_enum } from '@prisma/client'; import { getCommandArgs } from '../../mahoji/lib/util'; @@ -8,7 +7,6 @@ export function makeCommandUsage({ userID, channelID, guildID, - flags, commandName, args, isContinue, @@ -18,7 +16,6 @@ export function makeCommandUsage({ userID: string | bigint; channelID: string | bigint; guildID?: string | bigint | null; - flags: null | Record; commandName: string; args: CommandOptions; isContinue: null | boolean; @@ -27,12 +24,10 @@ export function makeCommandUsage({ }): Prisma.CommandUsageCreateInput { return { user_id: BigInt(userID), - command_name: commandName, - status: command_usage_status.Unknown, + command_name: commandName as command_name_enum, args: getCommandArgs(commandName, args), channel_id: BigInt(channelID), guild_id: guildID ? BigInt(guildID) : null, - flags: flags ? (Object.keys(flags).length > 0 ? flags : undefined) : undefined, is_continue: isContinue ?? undefined, inhibited, continue_delta_millis: continueDeltaMillis diff --git a/src/lib/util/fetchStatsForCL.ts b/src/lib/util/fetchStatsForCL.ts deleted file mode 100644 index 84573b2479..0000000000 --- a/src/lib/util/fetchStatsForCL.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { Bank } from 'oldschooljs'; -import type { UserStatsDataNeededForCL } from '../data/Collections'; -import { MUserStats } from '../structures/MUserStats'; -import type { ItemBank } from '../types'; - -export async function fetchStatsForCL(user: MUser): Promise { - const stats = await MUserStats.fromID(user.id); - const { userStats } = stats; - return { - sacrificedBank: new Bank(userStats.sacrificed_bank as ItemBank), - titheFarmsCompleted: userStats.tithe_farms_completed, - lapsScores: userStats.laps_scores as ItemBank, - openableScores: new Bank(userStats.openable_scores as ItemBank), - kcBank: userStats.monster_scores as ItemBank, - highGambles: userStats.high_gambles, - gotrRiftSearches: userStats.gotr_rift_searches, - stats - }; -} diff --git a/src/lib/util/findGroupOfUser.ts b/src/lib/util/findGroupOfUser.ts new file mode 100644 index 0000000000..e9528d5b0c --- /dev/null +++ b/src/lib/util/findGroupOfUser.ts @@ -0,0 +1,15 @@ +export async function findGroupOfUser(userID: string) { + const user = await roboChimpClient.user.findUnique({ + where: { + id: BigInt(userID) + } + }); + if (!user || !user.user_group_id) return [userID]; + const group = await roboChimpClient.user.findMany({ + where: { + user_group_id: user.user_group_id + } + }); + if (!group) return [userID]; + return group.map(u => u.id.toString()); +} diff --git a/src/lib/util/globalInteractions.ts b/src/lib/util/globalInteractions.ts index 4d068056ef..86977500d6 100644 --- a/src/lib/util/globalInteractions.ts +++ b/src/lib/util/globalInteractions.ts @@ -4,19 +4,20 @@ import { ButtonBuilder, ButtonStyle } from 'discord.js'; import { Time, removeFromArr, uniqueArr } from 'e'; import { Bank } from 'oldschooljs'; -import { Cooldowns } from '../../mahoji/lib/Cooldowns'; import { cancelGEListingCommand } from '../../mahoji/lib/abstracted_commands/cancelGEListingCommand'; import { autoContract } from '../../mahoji/lib/abstracted_commands/farmingContractCommand'; import { shootingStarsCommand, starCache } from '../../mahoji/lib/abstracted_commands/shootingStarsCommand'; import type { ClueTier } from '../clues/clueTiers'; import { BitField, PerkTier } from '../constants'; +import { RateLimitManager } from '@sapphire/ratelimits'; +import { InteractionID } from '../InteractionID'; import { runCommand } from '../settings/settings'; import { toaHelpCommand } from '../simulation/toa'; import type { ItemBank } from '../types'; import { formatDuration, stringMatches } from '../util'; import { updateGiveawayMessage } from './giveaway'; -import { deferInteraction, interactionReply } from './interactionReply'; +import { interactionReply } from './interactionReply'; import { minionIsBusy } from './minionIsBusy'; import { fetchRepeatTrips, repeatTrip } from './repeatStoredTrip'; @@ -54,6 +55,8 @@ function isValidGlobalInteraction(str: string): str is GlobalInteractionAction { return globalInteractionActions.includes(str as GlobalInteractionAction); } +const buttonRatelimiter = new RateLimitManager(Time.Second * 2, 1); + export function makeOpenCasketButton(tier: ClueTier) { const name: Uppercase = tier.name.toUpperCase() as Uppercase; const id: GlobalInteractionAction = `OPEN_${name}_CASKET`; @@ -220,15 +223,19 @@ async function giveawayButtonHandler(user: MUser, customID: string, interaction: return interactionReply(interaction, { content: 'You left the giveaway.', ephemeral: true }); } -async function repeatTripHandler(user: MUser, interaction: ButtonInteraction) { - if (user.minionIsBusy) return interactionReply(interaction, { content: 'Your minion is busy.' }); +async function repeatTripHandler(interaction: ButtonInteraction) { + if (minionIsBusy(interaction.user.id)) + return interactionReply(interaction, { content: 'Your minion is busy.', ephemeral: true }); const trips = await fetchRepeatTrips(interaction.user.id); - if (trips.length === 0) + if (trips.length === 0) { return interactionReply(interaction, { content: "Couldn't find a trip to repeat.", ephemeral: true }); + } const id = interaction.customId; const split = id.split('_'); const matchingActivity = trips.find(i => i.type === split[2]); - if (!matchingActivity) return repeatTrip(interaction, trips[0]); + if (!matchingActivity) { + return repeatTrip(interaction, trips[0]); + } return repeatTrip(interaction, matchingActivity); } @@ -310,8 +317,15 @@ async function handleGEButton(user: MUser, id: string, interaction: ButtonIntera export async function interactionHook(interaction: Interaction) { if (!interaction.isButton()) return; - if (['CONFIRM', 'CANCEL'].includes(interaction.customId)) return; - if (interaction.customId.startsWith('LP_')) 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) { return interactionReply(interaction, { @@ -320,27 +334,22 @@ export async function interactionHook(interaction: Interaction) { }); } - debugLog(`Interaction hook for button [${interaction.customId}]`, { - user_id: interaction.user.id, - channel_id: interaction.channelId, - guild_id: interaction.guildId - }); const id = interaction.customId; const userID = interaction.user.id; - const cd = Cooldowns.get(userID, 'button', Time.Second * 3); - if (cd !== null) { + const ratelimit = buttonRatelimiter.acquire(userID); + if (ratelimit.limited) { return interactionReply(interaction, { - content: `You're on cooldown from clicking buttons, please wait: ${formatDuration(cd, true)}.`, + content: `You're on cooldown from clicking buttons, please wait: ${formatDuration(ratelimit.remainingTime, true)}.`, ephemeral: true }); } - await deferInteraction(interaction); + if (id.includes('REPEAT_TRIP')) return repeatTripHandler(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); if (id.startsWith('PTR_')) return handlePinnedTripRepeat(user, id, interaction); if (id === 'TOA_CHECK') { @@ -369,7 +378,7 @@ export async function interactionHook(interaction: Interaction) { const timeSinceMessage = Date.now() - new Date(interaction.message.createdTimestamp).getTime(); const timeLimit = reactionTimeLimit(user.perkTier()); if (timeSinceMessage > Time.Day) { - console.log( + debugLog( `${user.id} clicked Diff[${formatDuration(timeSinceMessage)}] Button[${id}] Message[${ interaction.message.id }]` @@ -385,7 +394,7 @@ export async function interactionHook(interaction: Interaction) { } async function doClue(tier: ClueTier['name']) { - runCommand({ + return runCommand({ commandName: 'clue', args: { tier }, bypassInhibitors: true, @@ -394,7 +403,7 @@ export async function interactionHook(interaction: Interaction) { } async function openCasket(tier: ClueTier['name']) { - runCommand({ + return runCommand({ commandName: 'open', args: { name: tier, @@ -509,7 +518,9 @@ export async function interactionHook(interaction: Interaction) { } case 'AUTO_FARMING_CONTRACT': { const response = await autoContract(await mUserFetch(user.id), options.channelID, user.id); - if (response) interactionReply(interaction, response); + if (response) { + return interactionReply(interaction, response); + } return; } case 'FARMING_CONTRACT_EASIER': { diff --git a/src/lib/util/handleTripFinish.ts b/src/lib/util/handleTripFinish.ts index 4e639856aa..090bfc1ab5 100644 --- a/src/lib/util/handleTripFinish.ts +++ b/src/lib/util/handleTripFinish.ts @@ -1,8 +1,8 @@ +import { Stopwatch, channelIsSendable, makeComponents } from '@oldschoolgg/toolkit'; import type { activity_type_enum } from '@prisma/client'; import type { AttachmentBuilder, ButtonBuilder, MessageCollector, MessageCreateOptions } from 'discord.js'; -import type { Bank } from 'oldschooljs'; +import { Bank } from 'oldschooljs'; -import { channelIsSendable, makeComponents } from '@oldschoolgg/toolkit'; import { calculateBirdhouseDetails } from '../../mahoji/lib/abstracted_commands/birdhousesCommand'; import { canRunAutoContract } from '../../mahoji/lib/abstracted_commands/farmingContractCommand'; import { handleTriggerShootingStar } from '../../mahoji/lib/abstracted_commands/shootingStarsCommand'; @@ -42,21 +42,29 @@ interface TripFinishEffectOptions { loot: Bank | null; messages: string[]; } + +type TripEffectReturn = { + itemsToAddWithCL?: Bank; + itemsToRemove?: Bank; +}; + export interface TripFinishEffect { name: string; - fn: (options: TripFinishEffectOptions) => unknown; + // biome-ignore lint/suspicious/noConfusingVoidType: + fn: (options: TripFinishEffectOptions) => Promise; } const tripFinishEffects: TripFinishEffect[] = [ { name: 'Track GP Analytics', - fn: ({ data, loot }) => { + fn: async ({ data, loot }) => { if (loot && activitiesToTrackAsPVMGPSource.includes(data.type)) { const GP = loot.amount(COINS_ID); if (typeof GP === 'number') { - updateClientGPTrackSetting('gp_pvm', GP); + await updateClientGPTrackSetting('gp_pvm', GP); } } + return {}; } }, { @@ -66,9 +74,12 @@ const tripFinishEffects: TripFinishEffect[] = [ if (imp && imp.bank.length > 0) { const many = imp.bank.length > 1; messages.push(`Caught ${many ? 'some' : 'an'} impling${many ? 's' : ''}, you received: ${imp.bank}`); - userStatsBankUpdate(user.id, 'passive_implings_bank', imp.bank); - await transactItems({ userID: user.id, itemsToAdd: imp.bank, collectionLog: true }); + await userStatsBankUpdate(user, 'passive_implings_bank', imp.bank); + return { + itemsToAddWithCL: imp.bank + }; } + return {}; } }, { @@ -80,12 +91,14 @@ const tripFinishEffects: TripFinishEffect[] = [ { name: 'Random Events', fn: async ({ data, messages, user }) => { - await triggerRandomEvent(user, data.type, data.duration, messages); + return triggerRandomEvent(user, data.type, data.duration, messages); } }, { name: 'Combat Achievements', - fn: combatAchievementTripEffect + fn: async options => { + return combatAchievementTripEffect(options); + } } ]; @@ -111,7 +124,22 @@ export async function handleTripFinish( } const perkTier = user.perkTier(); const messages: string[] = []; - for (const effect of tripFinishEffects) await effect.fn({ data, user, loot, messages }); + + const itemsToAddWithCL = new Bank(); + const itemsToRemove = new Bank(); + for (const effect of tripFinishEffects) { + const stopwatch = new Stopwatch().start(); + const res = await effect.fn({ data, user, loot, messages }); + if (res?.itemsToAddWithCL) itemsToAddWithCL.add(res.itemsToAddWithCL); + if (res?.itemsToRemove) itemsToRemove.add(res.itemsToRemove); + stopwatch.stop(); + if (stopwatch.duration > 500) { + debugLog(`Finished ${effect.name} trip effect for ${user.id} in ${stopwatch}`); + } + } + if (itemsToAddWithCL.length > 0 || itemsToRemove.length > 0) { + await user.transactItems({ itemsToAdd: itemsToAddWithCL, collectionLog: true, itemsToRemove }); + } const clueReceived = loot ? ClueTiers.filter(tier => loot.amount(tier.scrollID) > 0) : []; @@ -142,7 +170,7 @@ export async function handleTripFinish( if (casketReceived) components.push(makeOpenCasketButton(casketReceived)); if (perkTier > PerkTier.One) { components.push(...buildClueButtons(loot, perkTier, user)); - const birdHousedetails = await calculateBirdhouseDetails(user.id); + const birdHousedetails = await calculateBirdhouseDetails(user); if (birdHousedetails.isReady && !user.bitfield.includes(BitField.DisableBirdhouseRunButton)) components.push(makeBirdHouseTripButton()); 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 2fe14d6626..4e001c1db8 100644 --- a/src/lib/util/interactionReply.ts +++ b/src/lib/util/interactionReply.ts @@ -1,4 +1,4 @@ -import { UserError } from '@oldschoolgg/toolkit/dist/lib/UserError'; +import { UserError } from '@oldschoolgg/toolkit'; import type { ButtonInteraction, ChatInputCommandInteraction, @@ -6,7 +6,8 @@ import type { InteractionReplyOptions, InteractionResponse, Message, - RepliableInteraction + RepliableInteraction, + StringSelectMenuInteraction } from 'discord.js'; import { DiscordAPIError } from 'discord.js'; @@ -15,20 +16,25 @@ import { logErrorForInteraction } from './logError'; export async function interactionReply(interaction: RepliableInteraction, response: string | InteractionReplyOptions) { let i: Promise | Promise | undefined = undefined; + let method = ''; + if (interaction.replied) { + method = 'followUp'; i = interaction.followUp(response); } else if (interaction.deferred) { + method = 'editReply'; i = interaction.editReply(response); } else { + method = 'reply'; 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. - logErrorForInteraction(e, interaction); + logErrorForInteraction(e, interaction, { method, response: JSON.stringify(response).slice(0, 50) }); } return undefined; } @@ -37,16 +43,17 @@ export async function interactionReply(interaction: RepliableInteraction, respon const wasDeferred = new Set(); export async function deferInteraction( - interaction: ButtonInteraction | ChatInputCommandInteraction, + interaction: ButtonInteraction | ChatInputCommandInteraction | StringSelectMenuInteraction, ephemeral = false ) { if (wasDeferred.size > 1000) wasDeferred.clear(); if (!interaction.deferred && !wasDeferred.has(interaction.id)) { wasDeferred.add(interaction.id); - const promise = await interaction.deferReply({ ephemeral }); - interaction.deferred = true; - wasDeferred.add(interaction.id); - return promise; + try { + await interaction.deferReply({ ephemeral }); + } catch (err) { + logErrorForInteraction(err, interaction); + } } } diff --git a/src/lib/util/linkedAccountsUtil.ts b/src/lib/util/linkedAccountsUtil.ts deleted file mode 100644 index 7542c65a36..0000000000 --- a/src/lib/util/linkedAccountsUtil.ts +++ /dev/null @@ -1,47 +0,0 @@ -import type { User } from '@prisma/client'; - -import { mahojiUsersSettingsFetch } from '../../mahoji/mahojiSettings'; -import { MUserClass } from '../MUser'; - -async function syncLinkedAccountPerks(user: MUser) { - const main = user.user.main_account; - const allAccounts: string[] = [...user.user.ironman_alts]; - if (main) { - allAccounts.push(main); - } - const allUsers = await Promise.all( - allAccounts.map(a => - mahojiUsersSettingsFetch(a, { - id: true, - premium_balance_tier: true, - premium_balance_expiry_date: true, - bitfield: true, - ironman_alts: true, - main_account: true, - minion_ironman: true - }) - ) - ); - allUsers.map(u => new MUserClass(u as User)); -} - -export async function syncLinkedAccounts() { - const users = await prisma.user.findMany({ - where: { - ironman_alts: { - isEmpty: false - } - }, - select: { - id: true, - ironman_alts: true, - premium_balance_tier: true, - premium_balance_expiry_date: true, - bitfield: true - } - }); - for (const u of users) { - const mUser = new MUserClass(u as User); - await syncLinkedAccountPerks(mUser); - } -} diff --git a/src/lib/util/logError.ts b/src/lib/util/logError.ts index 48df4c3113..5917b1c75e 100644 --- a/src/lib/util/logError.ts +++ b/src/lib/util/logError.ts @@ -1,8 +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) { @@ -15,26 +17,39 @@ 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); } } -export function logErrorForInteraction(err: Error | unknown, interaction: Interaction) { +export function logErrorForInteraction( + err: Error | unknown, + interaction: Interaction, + extraContext?: Record +) { const context: Record = { user_id: interaction.user.id, channel_id: interaction.channelId, guild_id: interaction.guildId, interaction_id: interaction.id, - interaction_type: interaction.type + interaction_type: interaction.type, + ...extraContext, + interaction_created_at: interaction.createdTimestamp, + current_timestamp: Date.now(), + difference_ms: Date.now() - interaction.createdTimestamp, + was_deferred: interaction.isRepliable() ? interaction.deferred : 'N/A' }; if (interaction.isChatInputCommand()) { context.options = JSON.stringify( @@ -45,5 +60,12 @@ export function logErrorForInteraction(err: Error | unknown, interaction: Intera context.button_id = interaction.customId; } + if ('rawError' in interaction) { + const _err = err as any; + if ('requestBody' in _err && isObject(_err.requestBody)) { + context.request_body = JSON.stringify(_err.requestBody); + } + } + logError(err, context); } diff --git a/src/lib/util/logger.ts b/src/lib/util/logger.ts index 40715f7fa6..edc78d05f4 100644 --- a/src/lib/util/logger.ts +++ b/src/lib/util/logger.ts @@ -1,29 +1,23 @@ import SonicBoom from 'sonic-boom'; +import { BOT_TYPE_LOWERCASE, globalConfig } from '../constants'; + const today = new Date(); const year = today.getFullYear(); const month = (today.getMonth() + 1).toString().padStart(2, '0'); const day = today.getDate().toString().padStart(2, '0'); const formattedDate = `${year}-${month}-${day}`; -export const LOG_FILE_NAME = `./logs/${formattedDate}-${today.getHours()}-${today.getMinutes()}-debug-logs.log`; +const LOG_FILE_NAME = globalConfig.isProduction + ? `../logs/${BOT_TYPE_LOWERCASE}.debug.log` + : `./logs/${formattedDate}-${today.getHours()}-${today.getMinutes()}-debug-logs.log`; export const sonicBoom = new SonicBoom({ fd: LOG_FILE_NAME, mkdir: true, - minLength: 4096, sync: false }); -const sqlLogger = new SonicBoom({ - fd: './logs/queries.sql', - mkdir: true, - minLength: 0, - sync: true -}); - -export const sqlLog = (str: string) => sqlLogger.write(`${new Date().toLocaleTimeString()} ${str}\n`); - interface LogContext { type?: string; [key: string]: unknown; diff --git a/src/lib/util/makeBadgeString.ts b/src/lib/util/makeBadgeString.ts new file mode 100644 index 0000000000..5a6598d358 --- /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(' ').trim(); +} diff --git a/src/lib/util/migrateUser.ts b/src/lib/util/migrateUser.ts index eb7a5c3d41..d759cc7c95 100644 --- a/src/lib/util/migrateUser.ts +++ b/src/lib/util/migrateUser.ts @@ -1,4 +1,4 @@ -import { UserError } from '@oldschoolgg/toolkit/dist/lib/UserError'; +import { UserError } from '@oldschoolgg/toolkit'; import { cancelUsersListings } from '../../mahoji/lib/abstracted_commands/cancelGEListingCommand'; diff --git a/src/lib/util/minionStatus.ts b/src/lib/util/minionStatus.ts index 0782428c44..269fd632f4 100644 --- a/src/lib/util/minionStatus.ts +++ b/src/lib/util/minionStatus.ts @@ -2,8 +2,8 @@ import { toTitleCase } from '@oldschoolgg/toolkit'; import { increaseNumByPercent, reduceNumByPercent } from 'e'; import { SkillsEnum } from 'oldschooljs/dist/constants'; -import { collectables } from '../../mahoji/lib/abstracted_commands/collectCommand'; import { shades, shadesLogs } from '../../mahoji/lib/abstracted_commands/shadesOfMortonCommand'; +import { collectables } from '../../mahoji/lib/collectables'; import { ClueTiers } from '../clues/clueTiers'; import { Emoji } from '../constants'; import killableMonsters from '../minions/data/killableMonsters'; @@ -94,25 +94,25 @@ export function minionStatus(user: MUser) { switch (currentTask.type) { case 'MonsterKilling': { const data = currentTask as MonsterActivityTaskOptions; - const monster = killableMonsters.find(mon => mon.id === data.monsterID); + const monster = killableMonsters.find(mon => mon.id === data.mi); - return `${name} is currently killing ${data.quantity}x ${monster?.name}. ${formattedDuration}`; + return `${name} is currently killing ${data.q}x ${monster?.name}. ${formattedDuration}`; } case 'GroupMonsterKilling': { const data = currentTask as GroupMonsterActivityTaskOptions; - const monster = killableMonsters.find(mon => mon.id === data.monsterID); + const monster = killableMonsters.find(mon => mon.id === data.mi); - return `${name} is currently killing ${data.quantity}x ${monster?.name} with a party of ${data.users.length + return `${name} is currently killing ${data.q}x ${monster?.name} with a party of ${data.users.length }. ${formattedDuration}`; } case 'ClueCompletion': { const data = currentTask as ClueActivityTaskOptions; - const clueTier = ClueTiers.find(tier => tier.id === data.clueID); + const clueTier = ClueTiers.find(tier => tier.id === data.ci); - return `${name} is currently completing ${data.quantity}x ${clueTier?.name} clues. ${formattedDuration}`; + return `${name} is currently completing ${data.q}x ${clueTier?.name} clues. ${formattedDuration}`; } case 'Crafting': { diff --git a/src/lib/util/parseStringBank.ts b/src/lib/util/parseStringBank.ts index 4ba8b87b1a..fe9933e470 100644 --- a/src/lib/util/parseStringBank.ts +++ b/src/lib/util/parseStringBank.ts @@ -1,4 +1,4 @@ -import { evalMathExpression } from '@oldschoolgg/toolkit/dist/util/expressionParser'; +import { evalMathExpression } from '@oldschoolgg/toolkit'; import { notEmpty } from 'e'; import { Bank, Items } from 'oldschooljs'; import type { Item } from 'oldschooljs/dist/meta/types'; diff --git a/src/lib/util/repeatStoredTrip.ts b/src/lib/util/repeatStoredTrip.ts index 329a168b0a..f2d8f15da7 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 { interactionReply } from './interactionReply'; const taskCanBeRepeated = (activity: Activity) => { if (activity.type === activity_type_enum.ClueCompletion) { @@ -88,7 +88,7 @@ const taskCanBeRepeated = (activity: Activity) => { const tripHandlers = { [activity_type_enum.ClueCompletion]: { commandName: 'clue', - args: (data: ClueActivityTaskOptions) => ({ tier: data.clueID, implings: getOSItem(data.implingID!).name }) + args: (data: ClueActivityTaskOptions) => ({ tier: data.ci, implings: getOSItem(data.implingID!).name }) }, [activity_type_enum.SpecificQuest]: { commandName: 'm', @@ -337,7 +337,7 @@ const tripHandlers = { [activity_type_enum.GroupMonsterKilling]: { commandName: 'mass', args: (data: GroupMonsterActivityTaskOptions) => ({ - monster: autocompleteMonsters.find(i => i.id === data.monsterID)?.name ?? data.monsterID.toString() + monster: autocompleteMonsters.find(i => i.id === data.mi)?.name ?? data.mi.toString() }) }, [activity_type_enum.Herblore]: { @@ -409,10 +409,10 @@ const tripHandlers = { let method: PvMMethod = 'none'; if (data.usingCannon) method = 'cannon'; if (data.chinning) method = 'chinning'; - else if (data.burstOrBarrage === SlayerActivityConstants.IceBarrage) method = 'barrage'; - else if (data.burstOrBarrage === SlayerActivityConstants.IceBurst) method = 'burst'; + else if (data.bob === SlayerActivityConstants.IceBarrage) method = 'barrage'; + else if (data.bob === SlayerActivityConstants.IceBurst) method = 'burst'; return { - name: autocompleteMonsters.find(i => i.id === data.monsterID)?.name ?? data.monsterID.toString(), + name: autocompleteMonsters.find(i => i.id === data.mi)?.name ?? data.mi.toString(), quantity: data.iQty, method, wilderness: data.isInWilderness @@ -699,7 +699,9 @@ export async function repeatTrip( interaction: ButtonInteraction, data: { data: Prisma.JsonValue; type: activity_type_enum } ) { - await deferInteraction(interaction); + if (!data || !data.data || !data.type) { + return interactionReply(interaction, { content: "Couldn't find any trip to repeat.", ephemeral: true }); + } const handler = tripHandlers[data.type]; return runCommand({ commandName: handler.commandName, diff --git a/src/lib/util/syncDisabledCommands.ts b/src/lib/util/syncDisabledCommands.ts new file mode 100644 index 0000000000..bb92a347b1 --- /dev/null +++ b/src/lib/util/syncDisabledCommands.ts @@ -0,0 +1,20 @@ +import { DISABLED_COMMANDS, globalConfig } from '../constants'; + +export async function syncDisabledCommands() { + const disabledCommands = await prisma.clientStorage.upsert({ + where: { + id: globalConfig.clientID + }, + select: { disabled_commands: true }, + create: { + id: globalConfig.clientID + }, + update: {} + }); + + if (disabledCommands.disabled_commands) { + for (const command of disabledCommands.disabled_commands) { + DISABLED_COMMANDS.add(command); + } + } +} diff --git a/src/lib/util/userEvents.ts b/src/lib/util/userEvents.ts index 3ace8beb6f..a80e986844 100644 --- a/src/lib/util/userEvents.ts +++ b/src/lib/util/userEvents.ts @@ -1,4 +1,4 @@ -import { dateFm } from '@oldschoolgg/toolkit/dist/util/misc'; +import { dateFm } from '@oldschoolgg/toolkit'; import type { Prisma, UserEvent, xp_gains_skill_enum } from '@prisma/client'; import { UserEventType } from '@prisma/client'; diff --git a/src/lib/util/userQueues.ts b/src/lib/util/userQueues.ts index 19bc1d041f..1d0cc83f2a 100644 --- a/src/lib/util/userQueues.ts +++ b/src/lib/util/userQueues.ts @@ -11,17 +11,16 @@ function getUserUpdateQueue(userID: string) { return currentQueue; } -export async function userQueueFn(userID: string, fn: () => Promise) { +export async function userQueueFn(userID: string, fn: () => Promise): Promise { const queue = getUserUpdateQueue(userID); return new Promise((resolve, reject) => { - queue.add(async () => { - try { - const result = await fn(); - resolve(result); - } catch (e) { - console.error(e); - reject(e); - } + queue.add(() => { + return fn() + .then(resolve) + .catch(e => { + console.error(e); + reject(e); + }); }); }); } diff --git a/src/lib/util/webhook.ts b/src/lib/util/webhook.ts index 1582226f02..165d4d7488 100644 --- a/src/lib/util/webhook.ts +++ b/src/lib/util/webhook.ts @@ -1,7 +1,6 @@ import { channelIsSendable, splitMessage } from '@oldschoolgg/toolkit'; import type { AttachmentBuilder, BaseMessageOptions, EmbedBuilder, Message } from 'discord.js'; import { PartialGroupDMChannel, PermissionsBitField, WebhookClient } from 'discord.js'; -import PQueue from 'p-queue'; import { production } from '../../config'; import { logError } from './logError'; @@ -43,8 +42,6 @@ async function deleteWebhook(channelID: string) { await prisma.webhook.delete({ where: { channel_id: channelID } }); } -const queue = new PQueue({ concurrency: 10 }); - export async function sendToChannelID( channelID: string, data: { @@ -59,37 +56,14 @@ export async function sendToChannelID( const allowedMentions = data.allowedMentions ?? { parse: ['users'] }; - async function queuedFn() { - const channel = await resolveChannel(channelID); - if (!channel) return; + const channel = await resolveChannel(channelID); + if (!channel) return; - const files = data.image ? [data.image] : data.files; - const embeds = []; - if (data.embed) embeds.push(data.embed); - if (channel instanceof WebhookClient) { - try { - await sendToChannelOrWebhook(channel, { - content: data.content, - files, - embeds, - components: data.components, - allowedMentions - }); - } catch (err: any) { - const error = err as Error; - if (error.message === 'Unknown Webhook') { - await deleteWebhook(channelID); - await sendToChannelID(channelID, data); - } else { - logError(error, { - content: data.content ?? 'None', - channelID - }); - } - } finally { - channel.destroy(); - } - } else { + const files = data.image ? [data.image] : data.files; + const embeds = []; + if (data.embed) embeds.push(data.embed); + if (channel instanceof WebhookClient) { + try { await sendToChannelOrWebhook(channel, { content: data.content, files, @@ -97,9 +71,29 @@ export async function sendToChannelID( components: data.components, allowedMentions }); + } catch (err: any) { + const error = err as Error; + if (error.message === 'Unknown Webhook') { + await deleteWebhook(channelID); + await sendToChannelID(channelID, data); + } else { + logError(error, { + content: data.content ?? 'None', + channelID + }); + } + } finally { + channel.destroy(); } + } else { + await sendToChannelOrWebhook(channel, { + content: data.content, + files, + embeds, + components: data.components, + allowedMentions + }); } - return queue.add(queuedFn); } async function sendToChannelOrWebhook(channel: WebhookClient | Message['channel'], input: BaseMessageOptions) { diff --git a/src/mahoji/commands/activities.ts b/src/mahoji/commands/activities.ts index 66cb1b39a6..f922033e57 100644 --- a/src/mahoji/commands/activities.ts +++ b/src/mahoji/commands/activities.ts @@ -22,7 +22,7 @@ import { championsChallengeCommand } from '../lib/abstracted_commands/championsC import { chargeGloriesCommand } from '../lib/abstracted_commands/chargeGloriesCommand'; import { chargeWealthCommand } from '../lib/abstracted_commands/chargeWealthCommand'; import { chompyHuntClaimCommand, chompyHuntCommand } from '../lib/abstracted_commands/chompyHuntCommand'; -import { collectCommand, collectables } from '../lib/abstracted_commands/collectCommand'; +import { collectCommand } from '../lib/abstracted_commands/collectCommand'; import { decantCommand } from '../lib/abstracted_commands/decantCommand'; import { driftNetCommand } from '../lib/abstracted_commands/driftNetCommand'; import { enchantCommand } from '../lib/abstracted_commands/enchantCommand'; @@ -35,6 +35,7 @@ import { sawmillCommand } from '../lib/abstracted_commands/sawmillCommand'; import { scatterCommand } from '../lib/abstracted_commands/scatterCommand'; import { underwaterAgilityThievingCommand } from '../lib/abstracted_commands/underwaterCommand'; import { warriorsGuildCommand } from '../lib/abstracted_commands/warriorsGuildCommand'; +import { collectables } from '../lib/collectables'; import { ownedItemOption } from '../lib/mahojiCommandOptions'; import type { OSBMahojiCommand } from '../lib/util'; @@ -545,7 +546,7 @@ export const activitiesCommand: OSBMahojiCommand = { return decantCommand(user, options.decant.potion_name, options.decant.dose); } if (options.inferno?.action === 'stats') return infernoStatsCommand(user); - if (options.birdhouses?.action === 'check') return birdhouseCheckCommand(user.user); + if (options.birdhouses?.action === 'check') return birdhouseCheckCommand(user); // Minion must be free const isBusy = user.minionIsBusy; diff --git a/src/mahoji/commands/admin.ts b/src/mahoji/commands/admin.ts index e62d8710d0..27bb5bdfef 100644 --- a/src/mahoji/commands/admin.ts +++ b/src/mahoji/commands/admin.ts @@ -1,14 +1,9 @@ -import { execSync } from 'node:child_process'; -import { inspect } from 'node:util'; - -import { type CommandRunOptions, Stopwatch, bulkUpdateCommands } from '@oldschoolgg/toolkit'; -import type { CommandResponse } from '@oldschoolgg/toolkit'; +import { type CommandRunOptions, bulkUpdateCommands } from '@oldschoolgg/toolkit'; import type { MahojiUserOption } from '@oldschoolgg/toolkit'; import type { ClientStorage } from '@prisma/client'; import { economy_transaction_type } from '@prisma/client'; -import { isThenable } from '@sentry/utils'; import type { InteractionReplyOptions } from 'discord.js'; -import { AttachmentBuilder, codeBlock, escapeCodeBlock } from 'discord.js'; +import { AttachmentBuilder } from 'discord.js'; import { ApplicationCommandOptionType } from 'discord.js'; import { Time, calcWhatPercent, noOp, notEmpty, randArrItem, sleep, uniqueArr } from 'e'; import { Bank } from 'oldschooljs'; @@ -18,7 +13,6 @@ import { ADMIN_IDS, OWNER_IDS, SupportServer, production } from '../../config'; import { mahojiUserSettingsUpdate } from '../../lib/MUser'; import { BLACKLISTED_GUILDS, BLACKLISTED_USERS, syncBlacklists } from '../../lib/blacklists'; import { - BOT_TYPE, BadgesEnum, BitField, BitFieldData, @@ -29,13 +23,11 @@ import { globalConfig } from '../../lib/constants'; import { economyLog } from '../../lib/economyLogs'; -import { generateGearImage } from '../../lib/gear/functions/generateGearImage'; import type { GearSetup } from '../../lib/gear/types'; import { GrandExchange } from '../../lib/grandExchange'; import { countUsersWithItemInCl } from '../../lib/settings/prisma'; import { cancelTask, minionActivityCacheDelete } from '../../lib/settings/settings'; import { sorts } from '../../lib/sorts'; -import { Gear } from '../../lib/structures/Gear'; import { calcPerHour, cleanString, @@ -51,7 +43,6 @@ import { mahojiClientSettingsFetch, mahojiClientSettingsUpdate } from '../../lib import getOSItem, { getItem } from '../../lib/util/getOSItem'; import { handleMahojiConfirmation } from '../../lib/util/handleMahojiConfirmation'; import { deferInteraction, interactionReply } from '../../lib/util/interactionReply'; -import { logError } from '../../lib/util/logError'; import { makeBankImage } from '../../lib/util/makeBankImage'; import { parseBank } from '../../lib/util/parseStringBank'; import { sendToChannelID } from '../../lib/util/webhook'; @@ -68,61 +59,6 @@ export const gifs = [ 'https://tenor.com/view/monkey-monito-mask-gif-23036908' ]; -async function unsafeEval({ userID, code }: { userID: string; code: string }) { - if (!OWNER_IDS.includes(userID)) return { content: 'Unauthorized' }; - code = code.replace(/[“”]/g, '"').replace(/[‘’]/g, "'"); - const stopwatch = new Stopwatch(); - let syncTime = '?'; - let asyncTime = '?'; - let result = null; - let thenable = false; - try { - // biome-ignore lint/security/noGlobalEval: - result = eval(code); - syncTime = stopwatch.toString(); - if (isThenable(result)) { - thenable = true; - stopwatch.restart(); - result = await result; - asyncTime = stopwatch.toString(); - } - } catch (error: any) { - if (!syncTime) syncTime = stopwatch.toString(); - if (thenable && !asyncTime) asyncTime = stopwatch.toString(); - if (error?.stack) logError(error); - result = error; - } - - stopwatch.stop(); - if (result instanceof Bank) { - return { files: [(await makeBankImage({ bank: result })).file] }; - } - if (result instanceof Gear) { - const image = await generateGearImage(await mUserFetch(userID), result, null, null); - return { files: [image] }; - } - - if (Buffer.isBuffer(result)) { - return { - content: 'The result was a buffer.', - files: [result] - }; - } - - if (typeof result !== 'string') { - result = inspect(result, { - depth: 1, - showHidden: false - }); - } - - return { - content: `${codeBlock(escapeCodeBlock(result))} -**Time:** ${asyncTime ? `⏱ ${asyncTime}<${syncTime}>` : `⏱ ${syncTime}`} -` - }; -} - async function allEquippedPets() { const pets = await prisma.$queryRawUnsafe<{ pet: number; qty: number }[]>(`SELECT "minion.equippedPet" AS pet, COUNT("minion.equippedPet")::int AS qty FROM users @@ -136,25 +72,6 @@ ORDER BY qty DESC;`); return bank; } -async function evalCommand(userID: string, code: string): CommandResponse { - try { - if (!OWNER_IDS.includes(userID)) { - return "You don't have permission to use this command."; - } - const res = await unsafeEval({ code, userID }); - - if (res.content && res.content.length > 2000) { - return { - files: [{ attachment: Buffer.from(res.content), name: 'output.txt' }] - }; - } - - return res; - } catch (err: any) { - return err.message ?? err; - } -} - async function getAllTradedItems(giveUniques = false) { const economyTrans = await prisma.economyTransaction.findMany({ where: { @@ -502,24 +419,6 @@ export const adminCommand: OSBMahojiCommand = { name: 'reboot', description: 'Reboot the bot.' }, - { - type: ApplicationCommandOptionType.Subcommand, - name: 'debug_patreon', - description: 'Debug patreon.' - }, - { - type: ApplicationCommandOptionType.Subcommand, - name: 'eval', - description: 'Eval.', - options: [ - { - type: ApplicationCommandOptionType.String, - name: 'code', - description: 'Code', - required: true - } - ] - }, { type: ApplicationCommandOptionType.Subcommand, name: 'sync_commands', @@ -583,16 +482,6 @@ export const adminCommand: OSBMahojiCommand = { } ] }, - { - type: ApplicationCommandOptionType.Subcommand, - name: 'sync_roles', - description: 'Sync roles' - }, - { - type: ApplicationCommandOptionType.Subcommand, - name: 'sync_patreon', - description: 'Sync patreon' - }, { type: ApplicationCommandOptionType.Subcommand, name: 'badges', @@ -733,11 +622,6 @@ export const adminCommand: OSBMahojiCommand = { } ] }, - // { - // type: ApplicationCommandOptionType.Subcommand, - // name: 'wipe_bingo_temp_cls', - // description: 'Wipe all temp cls of bingo users' - // }, { type: ApplicationCommandOptionType.Subcommand, name: 'give_items', @@ -771,15 +655,11 @@ export const adminCommand: OSBMahojiCommand = { }: CommandRunOptions<{ reboot?: {}; shut_down?: {}; - debug_patreon?: {}; - eval?: { code: string }; sync_commands?: {}; item_stats?: { item: string }; sync_blacklist?: {}; loot_track?: { name: string }; cancel_task?: { user: MahojiUserOption }; - sync_roles?: {}; - sync_patreon?: {}; badges?: { user: MahojiUserOption; add?: string; remove?: string }; bypass_age?: { user: MahojiUserOption }; command?: { enable?: string; disable?: string }; @@ -787,7 +667,6 @@ export const adminCommand: OSBMahojiCommand = { bitfield?: { user: MahojiUserOption; add?: string; remove?: string }; ltc?: { item?: string }; view?: { thing: string }; - wipe_bingo_temp_cls?: {}; give_items?: { user: MahojiUserOption; items: string; reason?: string }; }>) => { await deferInteraction(interaction); @@ -812,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)) { @@ -998,22 +863,25 @@ export const adminCommand: OSBMahojiCommand = { ${META_CONSTANTS.RENDERED_STR}` }).catch(noOp); - process.exit(); + import('exit-hook').then(({ gracefulExit }) => gracefulExit(1)); + return 'Turning off...'; } if (options.shut_down) { + debugLog('SHUTTING DOWN'); globalClient.isShuttingDown = true; const timer = production ? Time.Second * 30 : Time.Second * 5; await interactionReply(interaction, { 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); - execSync(`pm2 stop ${BOT_TYPE === 'OSB' ? 'osb' : 'bso'}`); + import('exit-hook').then(({ gracefulExit }) => gracefulExit(0)); + return 'Turning off...'; } if (options.sync_blacklist) { @@ -1110,9 +978,6 @@ ${guildCommands.length} Guild commands`; return randArrItem(gifs); } - if (options.eval) { - return evalCommand(userID.toString(), options.eval.code); - } if (options.item_stats) { const item = getItem(options.item_stats.item); if (!item) return 'Invalid item.'; @@ -1121,7 +986,7 @@ ${guildCommands.length} Guild commands`; FROM users WHERE bank->>'${item.id}' IS NOT NULL;`); return `There are ${ownedResult[0].qty.toLocaleString()} ${item.name} owned by everyone. -There are ${await countUsersWithItemInCl(item.id, isIron)} ${isIron ? 'ironmen' : 'people'} with atleast 1 ${ +There are ${await countUsersWithItemInCl(item.id, isIron)} ${isIron ? 'ironmen' : 'people'} with at least 1 ${ item.name } in their collection log.`; } diff --git a/src/mahoji/commands/bingo.ts b/src/mahoji/commands/bingo.ts index faa95659ce..0f73adc593 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') @@ -666,7 +675,7 @@ export const bingoCommand: OSBMahojiCommand = { const fee = BOT_TYPE === 'OSB' ? 20_000_000 : 50_000_000; const creationCost = new Bank().add('Coins', fee); if (user.GP < creationCost.amount('Coins')) { - return `You need atleast ${creationCost} to create a bingo.`; + return `You need at least ${creationCost} to create a bingo.`; } const channel = globalClient.channels.cache.get(options.create_bingo.notifications_channel_id); @@ -704,9 +713,9 @@ export const bingoCommand: OSBMahojiCommand = { return 'Team size must be between 1 and 5.'; } - // Start date must be atleast 3 hours into the future + // Start date must be at least 3 hours into the future if (createOptions.start_date.getTime() < Date.now() + Time.Minute * 3) { - return 'Start date must be atleast 3 minutes into the future.'; + return 'Start date must be at least 3 minutes into the future.'; } // Start date cannot be more than 31 days into the future @@ -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') @@ -907,7 +914,7 @@ Example: \`add_tile:Coal|Trout|Egg\` is a tile where you have to receive a coal const cost = new Bank().add('Coins', amount); if (user.GP < cost.amount('Coins')) { - return `You need atleast ${cost} to add that much GP to the prize pool.`; + return `You need at least ${cost} to add that much GP to the prize pool.`; } await handleMahojiConfirmation( @@ -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/buy.ts b/src/mahoji/commands/buy.ts index bfb55140be..a0eb017be8 100644 --- a/src/mahoji/commands/buy.ts +++ b/src/mahoji/commands/buy.ts @@ -16,7 +16,7 @@ import { updateBankSetting } from '../../lib/util/updateBankSetting'; import { buyFossilIslandNotes } from '../lib/abstracted_commands/buyFossilIslandNotes'; import { buyKitten } from '../lib/abstracted_commands/buyKitten'; import type { OSBMahojiCommand } from '../lib/util'; -import { mahojiParseNumber, multipleUserStatsBankUpdate } from '../mahojiSettings'; +import { mahojiParseNumber, userStatsUpdate } from '../mahojiSettings'; const allBuyablesAutocomplete = [...Buyables, { name: 'Kitten' }, { name: 'Fossil Island Notes' }]; @@ -151,12 +151,13 @@ export const buyCommand: OSBMahojiCommand = { .remove('Coins', totalCost.amount('Coins')).bank; if (Object.keys(costBankExcludingGP).length === 0) costBankExcludingGP = undefined; + const currentStats = await user.fetchStats({ buy_cost_bank: true, buy_loot_bank: true }); await Promise.all([ updateBankSetting('buy_cost_bank', totalCost), updateBankSetting('buy_loot_bank', outItems), - multipleUserStatsBankUpdate(user.id, { - buy_cost_bank: totalCost, - buy_loot_bank: outItems + userStatsUpdate(user.id, { + buy_cost_bank: totalCost.clone().add(currentStats.buy_cost_bank as ItemBank).bank, + buy_loot_bank: outItems.clone().add(currentStats.buy_loot_bank as ItemBank).bank }), prisma.buyCommandTransaction.create({ data: { diff --git a/src/mahoji/commands/ca.ts b/src/mahoji/commands/ca.ts index 108d35a35e..508bd74c1e 100644 --- a/src/mahoji/commands/ca.ts +++ b/src/mahoji/commands/ca.ts @@ -13,6 +13,7 @@ import { caToPlayerString, nextCATier } from '../../lib/combat_achievements/combatAchievements'; +import { Requirements } from '../../lib/structures/Requirements'; import { deferInteraction } from '../../lib/util/interactionReply'; import type { OSBMahojiCommand } from '../lib/util'; @@ -94,9 +95,10 @@ export const caCommand: OSBMahojiCommand = { .filter(i => !('rng' in i)); const completedTasks: CombatAchievement[] = []; + const reqData = await Requirements.fetchRequiredData(user); for (const task of tasksToCheck) { if ('requirements' in task) { - const { hasAll } = await task.requirements.check(user); + const { hasAll } = task.requirements.check(reqData); if (hasAll) { completedTasks.push(task); } diff --git a/src/mahoji/commands/cl.ts b/src/mahoji/commands/cl.ts index faa51d3f65..9664e61bb3 100644 --- a/src/mahoji/commands/cl.ts +++ b/src/mahoji/commands/cl.ts @@ -5,7 +5,7 @@ import { ApplicationCommandOptionType } from 'discord.js'; import type { CollectionLogType } from '../../lib/collectionLogTask'; import { CollectionLogFlags, clImageGenerator, collectionLogTypes } from '../../lib/collectionLogTask'; import { allCollectionLogs } from '../../lib/data/Collections'; -import { fetchStatsForCL } from '../../lib/util/fetchStatsForCL'; +import { MUserStats } from '../../lib/structures/MUserStats'; import type { OSBMahojiCommand } from '../lib/util'; export const collectionLogCommand: OSBMahojiCommand = { @@ -96,7 +96,7 @@ export const collectionLogCommand: OSBMahojiCommand = { type: options.type ?? 'collection', flags, collection: options.name, - stats: await fetchStatsForCL(user) + stats: await MUserStats.fromID(user.id) }); return result; } diff --git a/src/mahoji/commands/clue.ts b/src/mahoji/commands/clue.ts index 31a0f0518d..18a44557df 100644 --- a/src/mahoji/commands/clue.ts +++ b/src/mahoji/commands/clue.ts @@ -367,12 +367,12 @@ export const clueCommand: OSBMahojiCommand = { duration = timeToFinish * quantity; await addSubTaskToActivityTask({ - clueID: clueTier.id, + ci: clueTier.id, implingID: clueImpling ? clueImpling.id : undefined, implingClues: clueImpling ? implingClues : undefined, userID: user.id, channelID: channelID.toString(), - quantity, + q: quantity, duration, type: 'ClueCompletion' }); diff --git a/src/mahoji/commands/config.ts b/src/mahoji/commands/config.ts index 91dcd090a1..9a79f21fd9 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.' }; @@ -204,8 +212,9 @@ async function favItemConfig( const currentFavorites = user.user.favoriteItems; const item = getItem(itemToAdd ?? itemToRemove); const currentItems = `Your current favorite items are: ${ - currentFavorites.length === 0 ? 'None' : currentFavorites.map(itemNameFromID).join(', ') + currentFavorites.length === 0 ? 'None' : currentFavorites.map(itemNameFromID).join(', ').slice(0, 1500) }.`; + if (!item) return currentItems; if (itemToAdd) { const limit = (user.perkTier() + 1) * 100; @@ -629,6 +638,7 @@ export async function pinTripCommand( ) { if (!tripId) return 'Invalid trip.'; const id = Number(tripId); + if (!id || Number.isNaN(id)) return 'Invalid trip.'; const trip = await prisma.activity.findFirst({ where: { id, user_id: BigInt(user.id) } }); if (!trip) return 'Invalid trip.'; @@ -1063,7 +1073,7 @@ export const configCommand: OSBMahojiCommand = { >(` SELECT DISTINCT ON (activity.type) activity.type, activity.data, activity.id, activity.finish_date FROM activity -WHERE finish_date::date > now() - INTERVAL '31 days' +WHERE finish_date > now() - INTERVAL '14 days' AND user_id = '${user.id}'::bigint ORDER BY activity.type, finish_date DESC LIMIT 20; diff --git a/src/mahoji/commands/create.ts b/src/mahoji/commands/create.ts index f78bcd21b1..5fb0db5c3d 100644 --- a/src/mahoji/commands/create.ts +++ b/src/mahoji/commands/create.ts @@ -203,8 +203,8 @@ export const createCommand: OSBMahojiCommand = { await updateBankSetting('create_cost', inItems); await updateBankSetting('create_loot', outItems); - await userStatsBankUpdate(user.id, 'create_cost_bank', inItems); - await userStatsBankUpdate(user.id, 'create_loot_bank', outItems); + await userStatsBankUpdate(user, 'create_cost_bank', inItems); + await userStatsBankUpdate(user, 'create_loot_bank', outItems); if (action === 'revert') { return `You reverted ${inItems} into ${outItems}.${extraMessage}`; 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/drycalc.ts b/src/mahoji/commands/drycalc.ts index 1975749cf5..dbc12697d6 100644 --- a/src/mahoji/commands/drycalc.ts +++ b/src/mahoji/commands/drycalc.ts @@ -39,7 +39,7 @@ export const dryCalcCommand: OSBMahojiCommand = { )}%** chance of not receiving any drop, and a **${round( dropChance, 2 - )}%** chance of receiving atleast one drop.`; + )}%** chance of receiving at least one drop.`; return output; } diff --git a/src/mahoji/commands/farming.ts b/src/mahoji/commands/farming.ts index 68f802cd57..d2eecfdf93 100644 --- a/src/mahoji/commands/farming.ts +++ b/src/mahoji/commands/farming.ts @@ -9,7 +9,7 @@ import { superCompostables } from '../../lib/data/filterables'; import type { ContractOption } from '../../lib/minions/farming/types'; import { ContractOptions } from '../../lib/minions/farming/types'; import { autoFarm } from '../../lib/minions/functions/autoFarm'; -import { getFarmingInfo } from '../../lib/skilling/functions/getFarmingInfo'; +import { getFarmingInfoFromUser } from '../../lib/skilling/functions/getFarmingInfo'; import Farming, { CompostTiers } from '../../lib/skilling/skills/farming'; import { stringMatches } from '../../lib/util'; import { farmingPatchNames, userGrowingProgressStr } from '../../lib/util/farmingHelpers'; @@ -204,7 +204,7 @@ export const farmingCommand: OSBMahojiCommand = { }>) => { await deferInteraction(interaction); const klasaUser = await mUserFetch(userID); - const { patchesDetailed } = await getFarmingInfo(userID); + const { patchesDetailed } = getFarmingInfoFromUser(klasaUser.user); if (options.auto_farm) { return autoFarm(klasaUser, patchesDetailed, channelID); diff --git a/src/mahoji/commands/ge.ts b/src/mahoji/commands/ge.ts index b116e852d3..55666c1a35 100644 --- a/src/mahoji/commands/ge.ts +++ b/src/mahoji/commands/ge.ts @@ -1,6 +1,6 @@ import type { CommandRunOptions } from '@oldschoolgg/toolkit'; import type { CommandOption } from '@oldschoolgg/toolkit'; -import { evalMathExpression } from '@oldschoolgg/toolkit/dist/util/expressionParser'; +import { evalMathExpression } from '@oldschoolgg/toolkit'; import type { GEListing, GETransaction } from '@prisma/client'; import { ApplicationCommandOptionType } from 'discord.js'; import { sumArr, uniqueArr } from 'e'; @@ -10,6 +10,8 @@ import { PerkTier } from '../../lib/constants'; import { GrandExchange, createGECancelButton } from '../../lib/grandExchange'; import { marketPricemap } from '../../lib/marketPrices'; +import { Bank } from 'oldschooljs'; +import type { ItemBank } from 'oldschooljs/dist/meta/types'; import { formatDuration, itemNameFromID, makeComponents, returnStringOrFile, toKMB } from '../../lib/util'; import { createChart } from '../../lib/util/chart'; import getOSItem from '../../lib/util/getOSItem'; @@ -19,6 +21,7 @@ import itemIsTradeable from '../../lib/util/itemIsTradeable'; import { cancelGEListingCommand } from '../lib/abstracted_commands/cancelGEListingCommand'; import { itemOption, tradeableItemArr } from '../lib/mahojiCommandOptions'; import type { OSBMahojiCommand } from '../lib/util'; +import { mahojiUsersSettingsFetch } from '../mahojiSettings'; export type GEListingWithTransactions = GEListing & { buyTransactions: GETransaction[]; @@ -135,9 +138,10 @@ export const geCommand: OSBMahojiCommand = { description: 'The item you want to sell.', required: true, autocomplete: async (value, { id }) => { - const user = await mUserFetch(id); + const raw = await mahojiUsersSettingsFetch(id, { bank: true }); + const bank = new Bank(raw.bank as ItemBank); - return user.bank + return bank .items() .filter(i => i[0].tradeable_on_ge) .filter(i => (!value ? true : i[0].name.toLowerCase().includes(value.toLowerCase()))) diff --git a/src/mahoji/commands/hunt.ts b/src/mahoji/commands/hunt.ts index 64d6d7e1ff..883a735cd4 100644 --- a/src/mahoji/commands/hunt.ts +++ b/src/mahoji/commands/hunt.ts @@ -280,8 +280,8 @@ export const huntCommand: OSBMahojiCommand = { type: 'Hunter' }); - let response = `${user.minionName} is now ${crystalImpling ? 'hunting' : `${creature.huntTechnique}`} ${ - crystalImpling ? '' : ` ${quantity}x ` + let response = `${user.minionName} is now ${crystalImpling ? 'hunting' : `${creature.huntTechnique}`}${ + crystalImpling ? ' ' : ` ${quantity}x ` }${creature.name}, it'll take around ${formatDuration(duration)} to finish.`; if (boosts.length > 0) { diff --git a/src/mahoji/commands/k.ts b/src/mahoji/commands/k.ts index 34998c7a02..1842c9b894 100644 --- a/src/mahoji/commands/k.ts +++ b/src/mahoji/commands/k.ts @@ -47,7 +47,7 @@ export const autocompleteMonsters = [ async function fetchUsersRecentlyKilledMonsters(userID: string) { const res = await prisma.$queryRawUnsafe<{ mon_id: string; last_killed: Date }[]>( - `SELECT DISTINCT((data->>'monsterID')) AS mon_id, MAX(start_date) as last_killed + `SELECT DISTINCT((data->>'mi')) AS mon_id, MAX(start_date) as last_killed FROM activity WHERE user_id = $1 AND type = 'MonsterKilling' diff --git a/src/mahoji/commands/laps.ts b/src/mahoji/commands/laps.ts index 2e7961b462..45585d18b8 100644 --- a/src/mahoji/commands/laps.ts +++ b/src/mahoji/commands/laps.ts @@ -127,7 +127,7 @@ export const lapsCommand: OSBMahojiCommand = { } if (course.qpRequired && user.QP < course.qpRequired) { - return `You need atleast ${course.qpRequired} Quest Points to do this course.`; + return `You need at least ${course.qpRequired} Quest Points to do this course.`; } const maxTripLength = calcMaxTripLength(user, 'Agility'); diff --git a/src/mahoji/commands/leaderboard.ts b/src/mahoji/commands/leaderboard.ts index 6edf5b07fd..f98a813caf 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,29 +134,24 @@ 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 ${ironmanOnly ? ' AND "users"."minion.ironman" = true ' : ''} - ORDER BY kc DESC + ORDER BY score DESC 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( @@ -273,39 +324,21 @@ 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()); - 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 +365,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 +390,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 +436,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 +454,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 +601,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 +622,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 +661,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 +672,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 +702,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 +730,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 +753,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 +833,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 +864,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 +875,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/mass.ts b/src/mahoji/commands/mass.ts index abf8c088f9..8439cf7d20 100644 --- a/src/mahoji/commands/mass.ts +++ b/src/mahoji/commands/mass.ts @@ -71,7 +71,7 @@ export const massCommand: OSBMahojiCommand = { } ], run: async ({ interaction, options, userID, channelID }: CommandRunOptions<{ monster: string }>) => { - deferInteraction(interaction); + await deferInteraction(interaction); const user = await mUserFetch(userID); if (user.user.minion_ironman) return 'Ironmen cannot do masses.'; const channel = globalClient.channels.cache.get(channelID.toString()); @@ -156,10 +156,10 @@ export const massCommand: OSBMahojiCommand = { } await addSubTaskToActivityTask({ - monsterID: monster.id, + mi: monster.id, userID: user.id, channelID: channelID.toString(), - quantity, + q: quantity, duration, type: 'GroupMonsterKilling', leader: user.id, diff --git a/src/mahoji/commands/mine.ts b/src/mahoji/commands/mine.ts index 8de09264f4..7f8ab8a624 100644 --- a/src/mahoji/commands/mine.ts +++ b/src/mahoji/commands/mine.ts @@ -91,7 +91,7 @@ export const mineCommand: OSBMahojiCommand = { return `To mine ${ore.name}, you need ${formatSkillRequirements(sinsOfTheFatherSkillRequirements)}.`; } if (user.QP < 125) { - return `To mine ${ore.name}, you need atleast 125 Quest Points.`; + return `To mine ${ore.name}, you need at least 125 Quest Points.`; } } diff --git a/src/mahoji/commands/minion.ts b/src/mahoji/commands/minion.ts index c136cc9b88..4c2f348e8f 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'; @@ -23,12 +22,13 @@ import type { AttackStyles } from '../../lib/minions/functions'; import { blowpipeCommand, blowpipeDarts } from '../../lib/minions/functions/blowpipeCommand'; import { degradeableItemsCommand } from '../../lib/minions/functions/degradeableItemsCommand'; import { allPossibleStyles, trainCommand } from '../../lib/minions/functions/trainCommand'; +import { roboChimpCache } from '../../lib/perkTier'; import { roboChimpUserFetch } from '../../lib/roboChimp'; import { Minigames } from '../../lib/settings/minigames'; import Skills from '../../lib/skilling/skills'; import creatures from '../../lib/skilling/skills/hunter/creatures'; import { MUserStats } from '../../lib/structures/MUserStats'; -import { convertLVLtoXP, getUsername, isValidNickname } from '../../lib/util'; +import { convertLVLtoXP, isValidNickname } from '../../lib/util'; import { getKCByName } from '../../lib/util/getKCByName'; import getOSItem, { getItem } from '../../lib/util/getOSItem'; import { handleMahojiConfirmation } from '../../lib/util/handleMahojiConfirmation'; @@ -82,23 +82,10 @@ 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, - mainAccount: - user.user.main_account !== null - ? `${getUsername(user.user.main_account)}[${user.user.main_account}]` - : 'None', - ironmanAlts: user.user.ironman_alts.map(id => `${getUsername(id)}[${id}]`), - premiumBalance: `${premiumDate ? new Date(premiumDate).toLocaleString() : ''} ${ - premiumTier ? `Tier ${premiumTier}` : '' - }`, + badges: user.badgesString, isIronman: user.isIronman, bitfields, currentTask: taskText, @@ -110,16 +97,14 @@ export async function getUserInfo(user: MUser) { 2 ); + const roboCache = roboChimpCache.get(user.id); return { ...result, everythingString: `${user.badgedUsername}[${user.id}] **Current Trip:** ${taskText} -**Perk Tier:** ${result.perkTier} +**Perk Tier:** ${roboCache?.perk_tier ?? 'None'} **Blacklisted:** ${result.isBlacklisted} -**Badges:** ${result.badges.join(' ')} -**Main Account:** ${result.mainAccount} -**Ironman Alts:** ${result.ironmanAlts} -**Patron Balance:** ${result.premiumBalance} +**Badges:** ${result.badges} **Ironman:** ${result.isIronman} **Bitfields:** ${result.bitfields} **Patreon Connected:** ${result.patreon} diff --git a/src/mahoji/commands/mix.ts b/src/mahoji/commands/mix.ts index b16a74d103..85db808a86 100644 --- a/src/mahoji/commands/mix.ts +++ b/src/mahoji/commands/mix.ts @@ -72,7 +72,7 @@ export const mixCommand: OSBMahojiCommand = { } if (mixableItem.qpRequired && user.QP < mixableItem.qpRequired) { - return `You need atleast **${mixableItem.qpRequired}** QP to make ${mixableItem.item.name}.`; + return `You need at least **${mixableItem.qpRequired}** QP to make ${mixableItem.item.name}.`; } const requiredItems = new Bank(mixableItem.inputItems); diff --git a/src/mahoji/commands/offer.ts b/src/mahoji/commands/offer.ts index d4432485cd..08401b2474 100644 --- a/src/mahoji/commands/offer.ts +++ b/src/mahoji/commands/offer.ts @@ -187,7 +187,7 @@ export const offerCommand: OSBMahojiCommand = { itemsToAdd: loot, itemsToRemove: cost }); - await userStatsBankUpdate(user.id, 'bird_eggs_offered_bank', cost); + await userStatsBankUpdate(user, 'bird_eggs_offered_bank', cost); notifyUniques(user, egg.name, evilChickenOutfit, loot, quantity); @@ -208,10 +208,10 @@ export const offerCommand: OSBMahojiCommand = { const specialBone = specialBones.find(bone => stringMatches(bone.item.name, options.name)); if (specialBone) { if (user.QP < 8) { - return 'You need atleast 8 QP to offer long/curved bones for XP.'; + return 'You need at least 8 QP to offer long/curved bones for XP.'; } if (user.skillLevel(SkillsEnum.Construction) < 30) { - return 'You need atleast level 30 Construction to offer long/curved bones for XP.'; + return 'You need at least level 30 Construction to offer long/curved bones for XP.'; } const amountHas = userBank.amount(specialBone.item.id); if (!quantity) quantity = Math.max(amountHas, 1); diff --git a/src/mahoji/commands/rp.ts b/src/mahoji/commands/rp.ts index 32a11433f3..ee0f40f6ce 100644 --- a/src/mahoji/commands/rp.ts +++ b/src/mahoji/commands/rp.ts @@ -1,36 +1,35 @@ 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'; import { ADMIN_IDS, OWNER_IDS, SupportServer, production } from '../../config'; -import { mahojiUserSettingsUpdate } from '../../lib/MUser'; -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'; import { marketPricemap } from '../../lib/marketPrices'; import { unEquipAllCommand } from '../../lib/minions/functions/unequipAllCommand'; import { unequipPet } from '../../lib/minions/functions/unequipPet'; -import { allPerkBitfields } from '../../lib/perkTiers'; 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'; import itemIsTradeable from '../../lib/util/itemIsTradeable'; -import { syncLinkedAccounts } from '../../lib/util/linkedAccountsUtil'; +import { logError } from '../../lib/util/logError'; import { makeBankImage } from '../../lib/util/makeBankImage'; import { migrateUser } from '../../lib/util/migrateUser'; import { parseBank } from '../../lib/util/parseStringBank'; @@ -39,7 +38,6 @@ import { sendToChannelID } from '../../lib/util/webhook'; import { cancelUsersListings } from '../lib/abstracted_commands/cancelGEListingCommand'; import { gearSetupOption } from '../lib/mahojiCommandOptions'; import type { OSBMahojiCommand } from '../lib/util'; -import { mahojiUsersSettingsFetch } from '../mahojiSettings'; import { gifs } from './admin'; import { getUserInfo } from './minion'; import { sellPriceOfItem } from './sell'; @@ -47,17 +45,140 @@ 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!'; + } } ]; +async function usernameSync() { + 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 + } + }); + + response.push(`Cached ${allNewUsers.length} usernames.`); + return response.join(', '); +} + function isProtectedAccount(user: MUser) { const botAccounts = ['303730326692429825', '729244028989603850', '969542224058654790']; if ([...ADMIN_IDS, ...OWNER_IDS, ...botAccounts].includes(user.id)) return true; - if ([BitField.isModerator, BitField.isContributor].some(bf => user.bitfield.includes(bf))) return true; + if ([BitField.isModerator].some(bf => user.bitfield.includes(bf))) return true; 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', @@ -66,39 +187,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: [] - } - ] + description: 'Actions', + options: actions.map(a => ({ + type: ApplicationCommandOptionType.Subcommand, + name: a.name, + description: a.name, + options: [] + })) }, { type: ApplicationCommandOptionType.SubcommandGroup, @@ -234,25 +329,6 @@ export const rpCommand: OSBMahojiCommand = { } ] }, - { - type: ApplicationCommandOptionType.Subcommand, - name: 'add_ironman_alt', - description: 'Add an ironman alt account for a user', - options: [ - { - type: ApplicationCommandOptionType.User, - name: 'main', - description: 'The main', - required: true - }, - { - type: ApplicationCommandOptionType.User, - name: 'ironman_alt', - description: 'The ironman alt', - required: true - } - ] - }, { type: ApplicationCommandOptionType.Subcommand, name: 'view_user', @@ -449,13 +525,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?: {}; - }; + action?: any; player?: { viewbank?: { user: MahojiUserOption; json?: boolean }; add_patron_time?: { user: MahojiUserOption; tier: number; time: string }; @@ -476,7 +546,6 @@ export const rpCommand: OSBMahojiCommand = { user: MahojiUserOption; message_id: string; }; - add_ironman_alt?: { main: MahojiUserOption; ironman_alt: MahojiUserOption }; view_user?: { user: MahojiUserOption }; migrate_user?: { source: MahojiUserOption; dest: MahojiUserOption; reason?: string }; list_trades?: { @@ -493,11 +562,8 @@ export const rpCommand: OSBMahojiCommand = { const isOwner = OWNER_IDS.includes(userID.toString()); const isAdmin = ADMIN_IDS.includes(userID); const isMod = isOwner || isAdmin || adminUser.bitfield.includes(BitField.isModerator); - const isTrusted = [BitField.IsWikiContributor, BitField.isContributor].some(bit => - adminUser.bitfield.includes(bit) - ); if (!guildID || (production && guildID.toString() !== SupportServer)) return randArrItem(gifs); - if (!isAdmin && !isMod && !isTrusted) return randArrItem(gifs); + if (!isAdmin && !isMod) return randArrItem(gifs); if (options.user_event) { const messageId = @@ -552,62 +618,19 @@ 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?.networth_sync) { - const users = await prisma.user.findMany({ - where: { - GP: { - gt: 10_000_000_000 + if (options.action) { + for (const action of actions) { + if (options.action[action.name]) { + if (!action.allowed(adminUser)) return randArrItem(gifs); + try { + const result = await action.run(); + return result; + } catch (err) { + logError(err); + return 'An error occurred.'; } - }, - take: 20, - orderBy: { - GP: 'desc' - }, - select: { - id: true } - }); - 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.action?.patreon_reset) { - const bitfieldsToRemove = [ - BitField.IsPatronTier1, - BitField.IsPatronTier2, - BitField.IsPatronTier3, - BitField.IsPatronTier4, - BitField.IsPatronTier5, - BitField.IsPatronTier6 - ]; - await prisma.$queryRaw`UPDATE users SET bitfield = bitfield - '{${bitfieldsToRemove.join(',')}'::int[];`; - await syncLinkedAccounts(); - return 'Finished.'; } if (options.player?.set_buy_date) { @@ -741,62 +764,6 @@ ORDER BY item_id ASC;`); if (!toDelete) await adminUser.addItemsToBank({ items, collectionLog: false }); return `${toTitleCase(actionMsgPast)} ${items.toString().slice(0, 500)} from ${userToStealFrom.mention}`; } - if (options.player?.add_ironman_alt) { - const mainAccount = await mahojiUsersSettingsFetch(options.player.add_ironman_alt.main.user.id, { - minion_ironman: true, - id: true, - ironman_alts: true, - main_account: true - }); - const altAccount = await mahojiUsersSettingsFetch(options.player.add_ironman_alt.ironman_alt.user.id, { - minion_ironman: true, - bitfield: true, - id: true, - ironman_alts: true, - main_account: true - }); - const mainUser = await mUserFetch(mainAccount.id); - const altUser = await mUserFetch(altAccount.id); - if (mainAccount === altAccount) return "They're they same account."; - if (mainAccount.minion_ironman) return `${mainUser.usernameOrMention} is an ironman.`; - if (!altAccount.minion_ironman) return `${altUser.usernameOrMention} is not an ironman.`; - if (!altAccount.bitfield.includes(BitField.PermanentIronman)) { - return `${altUser.usernameOrMention} is not a *permanent* ironman.`; - } - - const peopleWithThisAltAlready = ( - await prisma.$queryRawUnsafe( - `SELECT id FROM users WHERE '${altAccount.id}' = ANY(ironman_alts);` - ) - ).length; - if (peopleWithThisAltAlready > 0) { - return `Someone already has ${altUser.usernameOrMention} as an ironman alt.`; - } - if (mainAccount.main_account) { - return `${mainUser.usernameOrMention} has a main account connected already.`; - } - if (altAccount.main_account) { - return `${altUser.usernameOrMention} has a main account connected already.`; - } - const mainAccountsAlts = mainAccount.ironman_alts; - if (mainAccountsAlts.includes(altAccount.id)) { - return `${mainUser.usernameOrMention} already has ${altUser.usernameOrMention} as an alt.`; - } - - await handleMahojiConfirmation( - interaction, - `Are you sure that \`${altUser.usernameOrMention}\` is the alt account of \`${mainUser.usernameOrMention}\`?` - ); - await mahojiUserSettingsUpdate(mainAccount.id, { - ironman_alts: { - push: altAccount.id - } - }); - await mahojiUserSettingsUpdate(altAccount.id, { - main_account: mainAccount.id - }); - return `You set \`${altUser.usernameOrMention}\` as the alt account of \`${mainUser.usernameOrMention}\`.`; - } if (options.player?.view_user) { const userToView = await mUserFetch(options.player.view_user.user.user.id); @@ -817,12 +784,6 @@ ORDER BY item_id ASC;`); const destUser = await mUserFetch(dest.user.id); if (isProtectedAccount(destUser)) return 'You cannot clobber that account.'; - if (allPerkBitfields.some(pt => destUser.bitfield.includes(pt))) { - await handleMahojiConfirmation( - interaction, - `The target user, ${destUser.logName}, has a Patreon Tier; are you really sure you want to DELETE all data from that account?` - ); - } const sourceXp = sumArr(Object.values(sourceUser.skillsAsXP)); const destXp = sumArr(Object.values(destUser.skillsAsXP)); if (destXp > sourceXp) { diff --git a/src/mahoji/commands/sacrifice.ts b/src/mahoji/commands/sacrifice.ts index fcddfcf922..1b5019e57e 100644 --- a/src/mahoji/commands/sacrifice.ts +++ b/src/mahoji/commands/sacrifice.ts @@ -8,7 +8,7 @@ import { Emoji, Events } from '../../lib/constants'; import { cats } from '../../lib/growablePets'; import minionIcons from '../../lib/minions/data/minionIcons'; import type { ItemBank } from '../../lib/types'; -import { toKMB } from '../../lib/util'; +import { toKMB, truncateString } from '../../lib/util'; import { handleMahojiConfirmation } from '../../lib/util/handleMahojiConfirmation'; import { deferInteraction } from '../../lib/util/interactionReply'; import { parseBank } from '../../lib/util/parseStringBank'; @@ -21,7 +21,7 @@ import { sellPriceOfItem } from './sell'; async function trackSacBank(user: MUser, bank: Bank) { await Promise.all([ updateBankSetting('economyStats_sacrificedBank', bank), - userStatsBankUpdate(user.id, 'sacrificed_bank', bank) + userStatsBankUpdate(user, 'sacrificed_bank', bank) ]); const stats = await user.fetchStats({ sacrificed_bank: true }); return new Bank(stats.sacrificed_bank as ItemBank); @@ -99,7 +99,7 @@ export const sacrificeCommand: OSBMahojiCommand = { ); } - deferInteraction(interaction); + await deferInteraction(interaction); const bankToSac = parseBank({ inputStr: options.items, @@ -156,7 +156,7 @@ export const sacrificeCommand: OSBMahojiCommand = { await handleMahojiConfirmation( interaction, - `${user}, are you sure you want to sacrifice ${bankToSac}? This will add ${totalPrice.toLocaleString()} (${toKMB( + `${user}, are you sure you want to sacrifice ${truncateString(bankToSac.toString(), 15000)}? This will add ${totalPrice.toLocaleString()} (${toKMB( totalPrice )}) to your sacrificed amount.` ); diff --git a/src/mahoji/commands/sell.ts b/src/mahoji/commands/sell.ts index 432012da65..7253fbf2a9 100644 --- a/src/mahoji/commands/sell.ts +++ b/src/mahoji/commands/sell.ts @@ -3,7 +3,7 @@ import type { Prisma } from '@prisma/client'; import { ApplicationCommandOptionType } from 'discord.js'; import { clamp, reduceNumByPercent } from 'e'; import { Bank } from 'oldschooljs'; -import type { Item, ItemBank } from 'oldschooljs/dist/meta/types'; +import type { Item } from 'oldschooljs/dist/meta/types'; import { MAX_INT_JAVA } from '../../lib/constants'; @@ -14,7 +14,7 @@ import { parseBank } from '../../lib/util/parseStringBank'; import { updateBankSetting } from '../../lib/util/updateBankSetting'; import { filterOption } from '../lib/mahojiCommandOptions'; import type { OSBMahojiCommand } from '../lib/util'; -import { updateClientGPTrackSetting, userStatsUpdate } from '../mahojiSettings'; +import { updateClientGPTrackSetting, userStatsBankUpdate, userStatsUpdate } from '../mahojiSettings'; /** * - Hardcoded prices @@ -265,14 +265,14 @@ export const sellCommand: OSBMahojiCommand = { await Promise.all([ updateClientGPTrackSetting('gp_sell', totalPrice), updateBankSetting('sold_items_bank', bankToSell), + userStatsBankUpdate(user, 'items_sold_bank', bankToSell), userStatsUpdate( user.id, - userStats => ({ - items_sold_bank: new Bank(userStats.items_sold_bank as ItemBank).add(bankToSell).bank, + { sell_gp: { increment: totalPrice } - }), + }, {} ), prisma.botItemSell.createMany({ data: botItemSellData }) diff --git a/src/mahoji/commands/simulate.ts b/src/mahoji/commands/simulate.ts index a55a6f43c3..7ab6de1696 100644 --- a/src/mahoji/commands/simulate.ts +++ b/src/mahoji/commands/simulate.ts @@ -57,7 +57,10 @@ function simulateColosseumRuns(samples = 100) { hasBF: false, hasClaws: true, hasSGS: true, - hasTorture: true + hasTorture: true, + scytheCharges: 300, + venatorBowCharges: 50, + bloodFuryCharges: 0 }); totalDuration += result.realDuration; kcBank.add(result.addedWaveKCBank); diff --git a/src/mahoji/commands/smelt.ts b/src/mahoji/commands/smelt.ts index ca8bded912..d099c2d281 100644 --- a/src/mahoji/commands/smelt.ts +++ b/src/mahoji/commands/smelt.ts @@ -145,7 +145,7 @@ export const smeltingCommand: OSBMahojiCommand = { coinsToRemove = Math.floor(gpPerHour * (duration / Time.Hour)); const gp = user.GP; if (gp < coinsToRemove) { - return `You need atleast ${coinsToRemove} GP to work at the Blast Furnace.`; + return `You need at least ${coinsToRemove} GP to work at the Blast Furnace.`; } cost.add('Coins', coinsToRemove); diff --git a/src/mahoji/commands/steal.ts b/src/mahoji/commands/steal.ts index 5ba6d227d1..c1248ed3df 100644 --- a/src/mahoji/commands/steal.ts +++ b/src/mahoji/commands/steal.ts @@ -70,7 +70,7 @@ export const stealCommand: OSBMahojiCommand = { } if (stealable.qpRequired && user.QP < stealable.qpRequired) { - return `You need atleast **${stealable.qpRequired}** QP to ${ + return `You need at least **${stealable.qpRequired}** QP to ${ stealable.type === 'pickpockable' ? 'pickpocket' : 'steal from' } a ${stealable.name}.`; } diff --git a/src/mahoji/commands/testpotato.ts b/src/mahoji/commands/testpotato.ts index 4cc5505d3c..d429bc1bdb 100644 --- a/src/mahoji/commands/testpotato.ts +++ b/src/mahoji/commands/testpotato.ts @@ -879,14 +879,15 @@ ${droprates.join('\n')}`), stringMatches(m.name, options.setmonsterkc?.monster ?? '') ); if (!monster) return 'Invalid monster'; + const stats = await user.fetchStats({ monster_scores: true }); await userStatsUpdate( user.id, - ({ monster_scores }) => ({ + { monster_scores: { - ...(monster_scores as Record), + ...(stats.monster_scores as Record), [monster.id]: options.setmonsterkc?.kc ?? 1 } - }), + }, {} ); return `Set your ${monster.name} KC to ${options.setmonsterkc.kc ?? 1}.`; diff --git a/src/mahoji/commands/tools.ts b/src/mahoji/commands/tools.ts index 886739a859..d86763f7c4 100644 --- a/src/mahoji/commands/tools.ts +++ b/src/mahoji/commands/tools.ts @@ -147,13 +147,13 @@ async function clueGains(interval: string, tier?: string, ironmanOnly?: boolean) const clueTier = ClueTiers.find(t => t.name.toLowerCase() === tier.toLowerCase()); if (!clueTier) return 'Invalid clue scroll tier.'; const tierId = clueTier.id; - tierFilter = `AND (a."data"->>'clueID')::int = ${tierId}`; + tierFilter = `AND (a."data"->>'ci')::int = ${tierId}`; title = `Highest ${clueTier.name} clue scroll completions in the past ${interval}`; } else { title = `Highest All clue scroll completions in the past ${interval}`; } - const query = `SELECT a.user_id::text, SUM((a."data"->>'quantity')::int) AS qty, MAX(a.finish_date) AS lastDate + const query = `SELECT a.user_id::text, SUM((a."data"->>'q')::int) AS qty, MAX(a.finish_date) AS lastDate FROM activity a JOIN users u ON a.user_id::text = u.id WHERE a.type = 'ClueCompletion' @@ -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] }; @@ -285,10 +292,10 @@ async function kcGains(interval: string, monsterName: string, ironmanOnly?: bool } const query = ` - SELECT a.user_id::text, SUM((a."data"->>'quantity')::int) AS qty, MAX(a.finish_date) AS lastDate + SELECT a.user_id::text, SUM((a."data"->>'q')::int) AS qty, MAX(a.finish_date) AS lastDate FROM activity a JOIN users u ON a.user_id::text = u.id - WHERE a.type = 'MonsterKilling' AND (a."data"->>'monsterID')::int = ${monster.id} + WHERE a.type = 'MonsterKilling' AND (a."data"->>'mi')::int = ${monster.id} AND a.finish_date >= now() - interval '1 ${intervalValue}' -- Corrected interval usage AND a.completed = true ${ironmanOnly ? ' AND u."minion.ironman" = true' : ''} @@ -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/achievementDiaryCommand.ts b/src/mahoji/lib/abstracted_commands/achievementDiaryCommand.ts index 96063494a5..8ed056367e 100644 --- a/src/mahoji/lib/abstracted_commands/achievementDiaryCommand.ts +++ b/src/mahoji/lib/abstracted_commands/achievementDiaryCommand.ts @@ -3,9 +3,9 @@ import { strikethrough } from 'discord.js'; import { calcWhatPercent } from 'e'; import { Bank, Monsters } from 'oldschooljs'; +import type { Minigame } from '@prisma/client'; import { diaries, userhasDiaryTier, userhasDiaryTierSync } from '../../../lib/diaries'; import type { DiaryTier } from '../../../lib/minions/types'; -import type { MinigameScore } from '../../../lib/settings/minigames'; import { Minigames } from '../../../lib/settings/minigames'; import { MUserStats } from '../../../lib/structures/MUserStats'; import { formatSkillRequirements, itemNameFromID, stringMatches } from '../../../lib/util'; @@ -153,19 +153,15 @@ export async function claimAchievementDiaryCommand(user: MUser, diaryName: strin return `You have already completed the entire ${diary.name} diary!`; } -export async function calculateAchievementDiaryProgress( - user: MUser, - stats: MUserStats, - minigameScores: MinigameScore[] -) { +export function calculateAchievementDiaryProgress(user: MUser, stats: MUserStats, minigameScores: Minigame) { let totalDiaries = 0; let totalCompleted = 0; for (const diaryLocation of diaries) { for (const diaryTier of [diaryLocation.easy, diaryLocation.medium, diaryLocation.hard, diaryLocation.elite]) { - const has = userhasDiaryTierSync(user, diaryTier, { stats, minigameScores })[0]; + const { hasDiary } = userhasDiaryTierSync(user, diaryTier, { stats, minigameScores }); totalDiaries++; - if (has) { + if (hasDiary) { totalCompleted++; } } diff --git a/src/mahoji/lib/abstracted_commands/aerialFishingCommand.ts b/src/mahoji/lib/abstracted_commands/aerialFishingCommand.ts index d8e69518b2..b21649e697 100644 --- a/src/mahoji/lib/abstracted_commands/aerialFishingCommand.ts +++ b/src/mahoji/lib/abstracted_commands/aerialFishingCommand.ts @@ -8,7 +8,7 @@ import { calcMaxTripLength } from '../../../lib/util/calcMaxTripLength'; export async function aerialFishingCommand(user: MUser, channelID: string) { if (user.skillLevel(SkillsEnum.Fishing) < 43 || user.skillLevel(SkillsEnum.Hunter) < 35) { - return 'You need atleast level 35 Hunter and 43 Fishing to do Aerial fishing.'; + return 'You need at least level 35 Hunter and 43 Fishing to do Aerial fishing.'; } const timePerFish = randomVariation(2, 7.5) * Time.Second; diff --git a/src/mahoji/lib/abstracted_commands/autoSlayCommand.ts b/src/mahoji/lib/abstracted_commands/autoSlayCommand.ts index f8722f6091..f6e99e8ad3 100644 --- a/src/mahoji/lib/abstracted_commands/autoSlayCommand.ts +++ b/src/mahoji/lib/abstracted_commands/autoSlayCommand.ts @@ -17,7 +17,7 @@ interface AutoslayLink { // Name and Monster must be specified if either is. efficientName?: string; efficientMonster?: number; - efficientMethod?: PvMMethod; + efficientMethod?: PvMMethod | PvMMethod[]; slayerMasters?: SlayerMasterEnum[]; } @@ -147,7 +147,7 @@ const AutoSlayMaxEfficiencyTable: AutoslayLink[] = [ monsterID: Monsters.SmokeDevil.id, efficientName: Monsters.SmokeDevil.name, efficientMonster: Monsters.SmokeDevil.id, - efficientMethod: 'barrage' + efficientMethod: ['barrage', 'cannon'] }, { monsterID: Monsters.DarkBeast.id, @@ -216,13 +216,13 @@ const WildyAutoSlayMaxEfficiencyTable: AutoslayLink[] = [ monsterID: Monsters.AbyssalDemon.id, efficientName: Monsters.AbyssalDemon.name, efficientMonster: Monsters.AbyssalDemon.id, - efficientMethod: 'barrage' + efficientMethod: ['barrage', 'cannon'] }, { monsterID: Monsters.Ankou.id, efficientName: Monsters.Ankou.name, efficientMonster: Monsters.Ankou.id, - efficientMethod: 'barrage' + efficientMethod: ['barrage', 'cannon'] }, { monsterID: Monsters.BlackDemon.id, @@ -240,7 +240,7 @@ const WildyAutoSlayMaxEfficiencyTable: AutoslayLink[] = [ monsterID: Monsters.Bloodveld.id, efficientName: Monsters.Bloodveld.name, efficientMonster: Monsters.Bloodveld.id, - efficientMethod: 'barrage' + efficientMethod: 'none' }, { monsterID: Monsters.ChaosDruid.id, @@ -264,7 +264,7 @@ const WildyAutoSlayMaxEfficiencyTable: AutoslayLink[] = [ monsterID: Monsters.DustDevil.id, efficientName: Monsters.DustDevil.name, efficientMonster: Monsters.DustDevil.id, - efficientMethod: 'barrage' + efficientMethod: ['barrage', 'cannon'] }, { monsterID: Monsters.ElderChaosDruid.id, @@ -318,7 +318,7 @@ const WildyAutoSlayMaxEfficiencyTable: AutoslayLink[] = [ monsterID: Monsters.Jelly.id, efficientName: Monsters.Jelly.name, efficientMonster: Monsters.Jelly.id, - efficientMethod: 'barrage' + efficientMethod: ['barrage', 'cannon'] }, { monsterID: Monsters.LesserDemon.id, @@ -345,10 +345,10 @@ const WildyAutoSlayMaxEfficiencyTable: AutoslayLink[] = [ efficientMethod: 'cannon' }, { - monsterID: Monsters.Nechryael.id, - efficientName: Monsters.Nechryael.name, - efficientMonster: Monsters.Nechryael.id, - efficientMethod: 'barrage' + monsterID: Monsters.GreaterNechryael.id, + efficientName: Monsters.GreaterNechryael.name, + efficientMonster: Monsters.GreaterNechryael.id, + efficientMethod: ['barrage', 'cannon'] }, { monsterID: Monsters.RevenantImp.id, @@ -485,7 +485,7 @@ export async function autoSlayCommand({ name: ehpMonster.efficientName }; if (ehpMonster.efficientMethod) { - args.method = ehpMonster.efficientMethod; + args.method = ehpMonster.efficientMethod as unknown as CommandOptions; } runCommand({ commandName: 'k', diff --git a/src/mahoji/lib/abstracted_commands/bankBgCommand.ts b/src/mahoji/lib/abstracted_commands/bankBgCommand.ts index 4cae340826..9af5fa5346 100644 --- a/src/mahoji/lib/abstracted_commands/bankBgCommand.ts +++ b/src/mahoji/lib/abstracted_commands/bankBgCommand.ts @@ -36,7 +36,7 @@ export async function bankBgCommand(interaction: ChatInputCommandInteraction, us if (selectedImage.sacValueRequired) { const sac = Number(user.user.sacrificedValue); if (sac < selectedImage.sacValueRequired) { - return `You have to have sacrificed atleast ${toKMB( + return `You have to have sacrificed at least ${toKMB( selectedImage.sacValueRequired )} GP worth of items to use this background.`; } diff --git a/src/mahoji/lib/abstracted_commands/birdhousesCommand.ts b/src/mahoji/lib/abstracted_commands/birdhousesCommand.ts index fb1dd671ed..c8d5e85a6c 100644 --- a/src/mahoji/lib/abstracted_commands/birdhousesCommand.ts +++ b/src/mahoji/lib/abstracted_commands/birdhousesCommand.ts @@ -1,4 +1,3 @@ -import type { User } from '@prisma/client'; import { time } from 'discord.js'; import { Bank } from 'oldschooljs'; @@ -20,11 +19,9 @@ interface BirdhouseDetails { readyAt: Date | null; } -export async function calculateBirdhouseDetails(userID: string | bigint): Promise { - const bh = await mahojiUsersSettingsFetch(userID, { - minion_birdhouseTraps: true - }); - if (!bh.minion_birdhouseTraps) { +export function calculateBirdhouseDetails(user: MUser): BirdhouseDetails { + const birdHouseTraps = user.user.minion_birdhouseTraps; + if (!birdHouseTraps) { return { raw: defaultBirdhouseTrap, isReady: false, @@ -34,7 +31,7 @@ export async function calculateBirdhouseDetails(userID: string | bigint): Promis }; } - const details = bh.minion_birdhouseTraps as unknown as BirdhouseData; + const details = birdHouseTraps as unknown as BirdhouseData; const birdHouse = details.lastPlaced ? birdhouses.find(_birdhouse => _birdhouse.name === details.lastPlaced) : null; if (!birdHouse) throw new Error(`Missing ${details.lastPlaced} birdhouse`); @@ -52,8 +49,8 @@ export async function calculateBirdhouseDetails(userID: string | bigint): Promis }; } -export async function birdhouseCheckCommand(user: User) { - const details = await calculateBirdhouseDetails(user.id); +export async function birdhouseCheckCommand(user: MUser) { + const details = calculateBirdhouseDetails(user); if (!details.birdHouse) { return 'You have no birdhouses planted.'; } @@ -67,8 +64,8 @@ export async function birdhouseHarvestCommand(user: MUser, channelID: string, in const infoStr: string[] = []; const boostStr: string[] = []; - const existingBirdhouse = await calculateBirdhouseDetails(user.id); - if (!existingBirdhouse.isReady && existingBirdhouse.raw.lastPlaced) return birdhouseCheckCommand(user.user); + const existingBirdhouse = await calculateBirdhouseDetails(user); + if (!existingBirdhouse.isReady && existingBirdhouse.raw.lastPlaced) return birdhouseCheckCommand(user); let birdhouseToPlant = inputBirdhouseName ? birdhouses.find(_birdhouse => diff --git a/src/mahoji/lib/abstracted_commands/cancelGEListingCommand.ts b/src/mahoji/lib/abstracted_commands/cancelGEListingCommand.ts index 9b62654073..e8a1dd14ba 100644 --- a/src/mahoji/lib/abstracted_commands/cancelGEListingCommand.ts +++ b/src/mahoji/lib/abstracted_commands/cancelGEListingCommand.ts @@ -1,4 +1,4 @@ -import { UserError } from '@oldschoolgg/toolkit/dist/lib/UserError'; +import { UserError } from '@oldschoolgg/toolkit'; import { Bank } from 'oldschooljs'; import { GrandExchange } from '../../../lib/grandExchange'; diff --git a/src/mahoji/lib/abstracted_commands/chompyHuntCommand.ts b/src/mahoji/lib/abstracted_commands/chompyHuntCommand.ts index 0e2a1853a1..49341819e6 100644 --- a/src/mahoji/lib/abstracted_commands/chompyHuntCommand.ts +++ b/src/mahoji/lib/abstracted_commands/chompyHuntCommand.ts @@ -40,7 +40,7 @@ export async function chompyHuntClaimCommand(user: MUser) { export async function chompyHuntCommand(user: MUser, channelID: string) { if (user.QP < 10) { - return 'You need atleast 10 QP to hunt Chompy birds.'; + return 'You need at least 10 QP to hunt Chompy birds.'; } const rangeGear = user.gear.range; diff --git a/src/mahoji/lib/abstracted_commands/collectCommand.ts b/src/mahoji/lib/abstracted_commands/collectCommand.ts index 7d976683c4..d12bf8d581 100644 --- a/src/mahoji/lib/abstracted_commands/collectCommand.ts +++ b/src/mahoji/lib/abstracted_commands/collectCommand.ts @@ -1,159 +1,16 @@ import { Time } from 'e'; import { Bank } from 'oldschooljs'; -import type { Item } from 'oldschooljs/dist/meta/types'; import { WildernessDiary, userhasDiaryTier } from '../../../lib/diaries'; import type { SkillsEnum } from '../../../lib/skilling/types'; -import type { Skills } from '../../../lib/types'; import type { CollectingOptions } from '../../../lib/types/minions'; import { formatDuration, stringMatches } from '../../../lib/util'; import addSubTaskToActivityTask from '../../../lib/util/addSubTaskToActivityTask'; import { calcMaxTripLength } from '../../../lib/util/calcMaxTripLength'; -import getOSItem from '../../../lib/util/getOSItem'; import { updateBankSetting } from '../../../lib/util/updateBankSetting'; +import { collectables } from '../collectables'; import { getPOH } from './pohCommand'; -interface Collectable { - item: Item; - skillReqs?: Skills; - itemCost?: Bank; - quantity: number; - duration: number; - qpRequired?: number; -} - -export const collectables: Collectable[] = [ - { - item: getOSItem('Blue dragon scale'), - quantity: 26, - itemCost: new Bank({ - 'Water rune': 1, - 'Air rune': 3, - 'Law rune': 1 - }), - skillReqs: { - agility: 70, - magic: 37 - }, - duration: Time.Minute * 2 - }, - { - item: getOSItem('Mort myre fungus'), - quantity: 100, - itemCost: new Bank({ - 'Prayer potion(4)': 1, - 'Ring of dueling(8)': 1 - }), - skillReqs: { - prayer: 50 - }, - duration: Time.Minute * 8.3, - qpRequired: 32 - }, - { - item: getOSItem('Flax'), - quantity: 28, - duration: Time.Minute * 1.68 - }, - { - item: getOSItem('Swamp toad'), - quantity: 28, - duration: Time.Minute * 1.68 - }, - { - item: getOSItem("Red spiders' eggs"), - quantity: 80, - itemCost: new Bank({ - 'Stamina potion(4)': 1 - }), - duration: Time.Minute * 8.5 - }, - { - item: getOSItem('Wine of zamorak'), - quantity: 27, - itemCost: new Bank({ - 'Law rune': 27, - 'Air rune': 27 - }), - skillReqs: { - magic: 33 - }, - duration: Time.Minute * 3.12 - }, - { - item: getOSItem('White berries'), - quantity: 27, - qpRequired: 22, - skillReqs: { - ranged: 60, - thieving: 50, - agility: 56, - crafting: 10, - fletching: 5, - cooking: 30 - }, - duration: Time.Minute * 4.05 - }, - { - item: getOSItem('Snape grass'), - quantity: 120, - itemCost: new Bank({ - 'Law rune': 12, - 'Astral rune': 12 - }), - duration: Time.Minute * 6.5, - qpRequired: 72 - }, - { - item: getOSItem('Snake weed'), - quantity: 150, - itemCost: new Bank({ - 'Ring of dueling(8)': 1 - }), - duration: Time.Minute * 30, - qpRequired: 3 - }, - { - item: getOSItem('Bucket of sand'), - quantity: 30, - itemCost: new Bank({ - 'Law rune': 1, - Coins: 30 * 25 - }), - duration: Time.Minute, - qpRequired: 30 - }, - { - item: getOSItem('Jangerberries'), - quantity: 224, - itemCost: new Bank({ - 'Ring of dueling(8)': 1 - }), - skillReqs: { - agility: 10 - }, - duration: Time.Minute * 24 - }, - // Miniquest to get Tarn's diary for Salve amulet (e)/(ei) - { - item: getOSItem("Tarn's diary"), - quantity: 1, - itemCost: new Bank({ - 'Prayer potion(4)': 2 - }), - skillReqs: { - slayer: 40, - attack: 60, - strength: 60, - ranged: 60, - defence: 60, - magic: 60 - }, - duration: 10 * Time.Minute, - qpRequired: 100 - } -]; - export async function collectCommand( user: MUser, channelID: string, diff --git a/src/mahoji/lib/abstracted_commands/coxCommand.ts b/src/mahoji/lib/abstracted_commands/coxCommand.ts index 366ed284e0..6eeaca3135 100644 --- a/src/mahoji/lib/abstracted_commands/coxCommand.ts +++ b/src/mahoji/lib/abstracted_commands/coxCommand.ts @@ -95,7 +95,7 @@ export async function coxCommand( if (isChallengeMode) { const normalKC = await getMinigameScore(user.id, 'raids'); if (normalKC < 200) { - return 'You need atleast 200 completions of the Chambers of Xeric before you can attempt Challenge Mode.'; + return 'You need at least 200 completions of the Chambers of Xeric before you can attempt Challenge Mode.'; } } if (user.minionIsBusy) { diff --git a/src/mahoji/lib/abstracted_commands/diceCommand.ts b/src/mahoji/lib/abstracted_commands/diceCommand.ts index b53ebbbe98..f8a4117615 100644 --- a/src/mahoji/lib/abstracted_commands/diceCommand.ts +++ b/src/mahoji/lib/abstracted_commands/diceCommand.ts @@ -29,7 +29,7 @@ export async function diceCommand(user: MUser, interaction: ChatInputCommandInte } if (amount < 1_000_000) { - return 'You have to dice atleast 1,000,000.'; + return 'You have to dice at least 1,000,000.'; } const gp = user.GP; diff --git a/src/mahoji/lib/abstracted_commands/driftNetCommand.ts b/src/mahoji/lib/abstracted_commands/driftNetCommand.ts index 9c24f5cde9..de2b715d50 100644 --- a/src/mahoji/lib/abstracted_commands/driftNetCommand.ts +++ b/src/mahoji/lib/abstracted_commands/driftNetCommand.ts @@ -21,7 +21,7 @@ export async function driftNetCommand( } if (user.skillLevel(SkillsEnum.Fishing) < 47 || user.skillLevel(SkillsEnum.Hunter) < 44) { - return 'You need atleast level 44 Hunter and 47 Fishing to do Drift net fishing.'; + return 'You need at least level 44 Hunter and 47 Fishing to do Drift net fishing.'; } if (!user.hasEquipped(['Graceful gloves', 'Graceful top', 'Graceful legs'])) { diff --git a/src/mahoji/lib/abstracted_commands/farmingCommand.ts b/src/mahoji/lib/abstracted_commands/farmingCommand.ts index c0b24fa88b..9d094766df 100644 --- a/src/mahoji/lib/abstracted_commands/farmingCommand.ts +++ b/src/mahoji/lib/abstracted_commands/farmingCommand.ts @@ -299,7 +299,7 @@ export async function farmingPlantCommand({ } }); - await userStatsBankUpdate(user.id, 'farming_plant_cost_bank', cost); + await userStatsBankUpdate(user, 'farming_plant_cost_bank', cost); await addSubTaskToActivityTask({ plantsName: plant.name, diff --git a/src/mahoji/lib/abstracted_commands/farmingContractCommand.ts b/src/mahoji/lib/abstracted_commands/farmingContractCommand.ts index 56b644e515..03620fce8a 100644 --- a/src/mahoji/lib/abstracted_commands/farmingContractCommand.ts +++ b/src/mahoji/lib/abstracted_commands/farmingContractCommand.ts @@ -7,7 +7,7 @@ import type { FarmingContractDifficultyLevel } from '../../../lib/minions/farming/types'; import { getPlantToGrow } from '../../../lib/skilling/functions/calcFarmingContracts'; -import { getFarmingInfo } from '../../../lib/skilling/functions/getFarmingInfo'; +import { getFarmingInfoFromUser } from '../../../lib/skilling/functions/getFarmingInfo'; import { plants } from '../../../lib/skilling/skills/farming'; import { makeComponents, makeEasierFarmingContractButton, roughMergeMahojiResponse } from '../../../lib/util'; import { newChatHeadImage } from '../../../lib/util/chatHeadImage'; @@ -149,7 +149,7 @@ export async function farmingContractCommand(userID: string, input?: ContractOpt }; } -export async function canRunAutoContract(user: MUser) { +export function canRunAutoContract(user: MUser) { // Must be above 45 farming if (user.skillLevel('farming') < 45) return false; @@ -157,7 +157,7 @@ export async function canRunAutoContract(user: MUser) { const contract = user.user.minion_farmingContract as FarmingContract | null; if (!contract || !contract.hasContract) return true; - const farmingDetails = await getFarmingInfo(user.id); + const farmingDetails = getFarmingInfoFromUser(user.user); // If the patch we're contracted to is ready, we can auto contract const contractedPatch = farmingDetails.patchesDetailed.find( @@ -173,12 +173,9 @@ function bestFarmingContractUserCanDo(user: MUser) { } export async function autoContract(user: MUser, channelID: string, userID: string): CommandResponse { - const [farmingDetails, mahojiUser] = await Promise.all([ - getFarmingInfo(userID), - mahojiUsersSettingsFetch(userID, { minion_farmingContract: true }) - ]); - const contract = mahojiUser.minion_farmingContract as FarmingContract | null; - const plant = contract?.hasContract ? findPlant(contract?.plantToGrow) : null; + const farmingDetails = getFarmingInfoFromUser(user.user); + const contract = user.farmingContract(); + const plant = contract?.contract ? findPlant(contract?.contract.plantToGrow) : null; const patch = farmingDetails.patchesDetailed.find(p => p.plant === plant); const bestContractTierCanDo = bestFarmingContractUserCanDo(user); @@ -189,7 +186,7 @@ export async function autoContract(user: MUser, channelID: string, userID: strin } // If they have no contract, get them a contract, recurse. - if (!contract || !contract.hasContract) { + if (!contract || !contract.contract) { const contractResult = await farmingContractCommand(userID, bestContractTierCanDo); const newUser = await mahojiUsersSettingsFetch(userID, { minion_farmingContract: true }); const newContract = (newUser.minion_farmingContract ?? defaultFarmingContract) as FarmingContract; diff --git a/src/mahoji/lib/abstracted_commands/fightCavesCommand.ts b/src/mahoji/lib/abstracted_commands/fightCavesCommand.ts index b30248334e..543a275fa5 100644 --- a/src/mahoji/lib/abstracted_commands/fightCavesCommand.ts +++ b/src/mahoji/lib/abstracted_commands/fightCavesCommand.ts @@ -86,11 +86,11 @@ function checkGear(user: MUser): string | undefined { } if (!user.owns(fightCavesCost)) { - return `JalYt, you need supplies to have a chance in the caves...come back with ${fightCavesCost}.`; + return `JalYt, you need supplies to have a chance in the caves... Come back with ${fightCavesCost}.`; } if (user.skillLevel('prayer') < 43) { - return 'JalYt, come back when you have atleast 43 Prayer, TzTok-Jad annihilate you without protection from gods.'; + return 'JalYt, come back when you have at least 43 Prayer, TzTok-Jad annihilate you without protection from gods.'; } } diff --git a/src/mahoji/lib/abstracted_commands/fishingTrawler.ts b/src/mahoji/lib/abstracted_commands/fishingTrawler.ts index 36e4ccb8fd..21657215d8 100644 --- a/src/mahoji/lib/abstracted_commands/fishingTrawler.ts +++ b/src/mahoji/lib/abstracted_commands/fishingTrawler.ts @@ -8,7 +8,7 @@ import { calcMaxTripLength } from '../../../lib/util/calcMaxTripLength'; export async function fishingTrawlerCommand(user: MUser, channelID: string) { if (user.skillLevel('fishing') < 15) { - return 'You need atleast level 15 Fishing to do the Fishing Trawler.'; + return 'You need at least level 15 Fishing to do the Fishing Trawler.'; } const tripsDone = await getMinigameScore(user.id, 'fishing_trawler'); diff --git a/src/mahoji/lib/abstracted_commands/gauntletCommand.ts b/src/mahoji/lib/abstracted_commands/gauntletCommand.ts index 086ba8d13f..362c9a1d18 100644 --- a/src/mahoji/lib/abstracted_commands/gauntletCommand.ts +++ b/src/mahoji/lib/abstracted_commands/gauntletCommand.ts @@ -44,7 +44,7 @@ const corruptedRequirements = { export async function gauntletCommand(user: MUser, channelID: string, type: 'corrupted' | 'normal' = 'normal') { if (user.minionIsBusy) return `${user.minionName} is busy.`; if (user.QP < 200) { - return 'You need atleast 200 QP to do the Gauntlet.'; + return 'You need at least 200 QP to do the Gauntlet.'; } const readableName = `${toTitleCase(type)} Gauntlet`; const requiredSkills = type === 'corrupted' ? corruptedRequirements : standardRequirements; diff --git a/src/mahoji/lib/abstracted_commands/gearCommands.ts b/src/mahoji/lib/abstracted_commands/gearCommands.ts index 3cb1fb6c54..b3a98117ce 100644 --- a/src/mahoji/lib/abstracted_commands/gearCommands.ts +++ b/src/mahoji/lib/abstracted_commands/gearCommands.ts @@ -406,7 +406,7 @@ export async function gearViewCommand(user: MUser, input: string, text: boolean) }) .join('\n\n'); - const updatedContent = `${content}\n\nThese assume you have atleast 25 prayer for the protect item prayer.`; + const updatedContent = `${content}\n\nThese assume you have at least 25 prayer for the protect item prayer.`; return { content: updatedContent }; } diff --git a/src/mahoji/lib/abstracted_commands/giantsFoundryCommand.ts b/src/mahoji/lib/abstracted_commands/giantsFoundryCommand.ts index 4706d86de3..2576fa30ff 100644 --- a/src/mahoji/lib/abstracted_commands/giantsFoundryCommand.ts +++ b/src/mahoji/lib/abstracted_commands/giantsFoundryCommand.ts @@ -172,7 +172,7 @@ export async function giantsFoundryStartCommand( } if (userSmithingLevel < alloy.level) { - return `${user.minionName} needs atleast level ${alloy.level} Smithing to user ${alloy.name} alloy in the Giants' Foundry.`; + return `${user.minionName} needs at least level ${alloy.level} Smithing to user ${alloy.name} alloy in the Giants' Foundry.`; } // If they have the entire Smiths' Uniform, give an extra 15% speed bonus @@ -228,7 +228,7 @@ export async function giantsFoundryStartCommand( } ] }); - await userStatsBankUpdate(user.id, 'gf_cost', totalCost); + await userStatsBankUpdate(user, 'gf_cost', totalCost); await addSubTaskToActivityTask({ quantity, diff --git a/src/mahoji/lib/abstracted_commands/gnomeRestaurantCommand.ts b/src/mahoji/lib/abstracted_commands/gnomeRestaurantCommand.ts index 369f9f0a56..f78cc30f8e 100644 --- a/src/mahoji/lib/abstracted_commands/gnomeRestaurantCommand.ts +++ b/src/mahoji/lib/abstracted_commands/gnomeRestaurantCommand.ts @@ -18,7 +18,7 @@ export async function gnomeRestaurantCommand(user: MUser, channelID: string) { const itemsToRemove = new Bank(); const gp = user.GP; if (gp < 5000) { - return 'You need atleast 5k GP to work at the Gnome Restaurant.'; + return 'You need at least 5k GP to work at the Gnome Restaurant.'; } itemsToRemove.add('Coins', 5000); diff --git a/src/mahoji/lib/abstracted_commands/infernoCommand.ts b/src/mahoji/lib/abstracted_commands/infernoCommand.ts index 56f2e1f54f..080b9ff36d 100644 --- a/src/mahoji/lib/abstracted_commands/infernoCommand.ts +++ b/src/mahoji/lib/abstracted_commands/infernoCommand.ts @@ -249,7 +249,7 @@ async function infernoRun({ const dartIndex = blowpipeDarts.indexOf(dartItem); const percent = dartIndex >= 3 ? dartIndex * 0.9 : -(4 * (4 - dartIndex)); if (dartIndex < 5) { - return 'Your darts are simply too weak, to work in the Inferno!'; + return 'Your darts are simply too weak to work in the Inferno!'; } duration.add(true, -percent, `${dartItem.name} in blowpipe`); @@ -516,7 +516,7 @@ export async function infernoStartCommand(user: MUser, channelID: string): Comma { name: 'image.jpg', attachment: await newChatHeadImage({ - content: "You're on your own now JalYt, you face certain death... prepare to fight for your life.", + content: "You're on your own now JalYt, you face certain death... Prepare to fight for your life.", head: 'ketKeh' }) } diff --git a/src/mahoji/lib/abstracted_commands/ironmanCommand.ts b/src/mahoji/lib/abstracted_commands/ironmanCommand.ts index 49d92228b2..a1013f73b0 100644 --- a/src/mahoji/lib/abstracted_commands/ironmanCommand.ts +++ b/src/mahoji/lib/abstracted_commands/ironmanCommand.ts @@ -4,7 +4,6 @@ import type { ChatInputCommandInteraction } from 'discord.js'; import type { ItemBank } from 'oldschooljs/dist/meta/types'; import { BitField } from '../../../lib/constants'; -import { GrandExchange } from '../../../lib/grandExchange'; import { roboChimpUserFetch } from '../../../lib/roboChimp'; import { assert } from '../../../lib/util'; @@ -67,8 +66,25 @@ export async function ironmanCommand( return "You can't become an ironman because you have active bingos."; } - const activeGEListings = await GrandExchange.fetchActiveListings(); - if ([...activeGEListings.buyListings, ...activeGEListings.sellListings].some(i => i.user_id === user.id)) { + const activeListings = await prisma.gEListing.findMany({ + where: { + user_id: user.id, + quantity_remaining: { + gt: 0 + }, + fulfilled_at: null, + cancelled_at: null + }, + include: { + buyTransactions: true, + sellTransactions: true + }, + orderBy: { + created_at: 'desc' + } + }); + // Return early if no active listings. + if (activeListings.length !== 0) { return `You can't become an ironman because you have active Grand Exchange listings. Cancel them and try again: ${mentionCommand( globalClient, 'ge', @@ -95,13 +111,9 @@ After becoming an ironman: const mUser = (await mUserFetch(user.id)).user; type KeysThatArentReset = - | 'ironman_alts' - | 'main_account' | 'bank_bg_hex' | 'bank_sort_weightings' | 'bank_sort_method' - | 'premium_balance_expiry_date' - | 'premium_balance_tier' | 'minion_bought_date' | 'id' | 'pets' @@ -115,27 +127,21 @@ After becoming an ironman: BitField.IsPatronTier4, BitField.IsPatronTier5, BitField.isModerator, - BitField.isContributor, BitField.BypassAgeRestriction, BitField.HasPermanentEventBackgrounds, BitField.HasPermanentTierOne, BitField.DisabledRandomEvents, BitField.AlwaysSmallBank, - BitField.IsWikiContributor, BitField.IsPatronTier6 ]; const createOptions: Required> = { id: user.id, - main_account: mUser.main_account, - ironman_alts: mUser.ironman_alts, bank_bg_hex: mUser.bank_bg_hex, bank_sort_method: mUser.bank_sort_method, bank_sort_weightings: mUser.bank_sort_weightings as ItemBank, minion_bought_date: mUser.minion_bought_date, RSN: mUser.RSN, - premium_balance_expiry_date: mUser.premium_balance_expiry_date, - premium_balance_tier: mUser.premium_balance_tier, pets: mUser.pets as ItemBank, bitfield: bitFieldsToKeep.filter(i => user.bitfield.includes(i)) }; diff --git a/src/mahoji/lib/abstracted_commands/lampCommand.ts b/src/mahoji/lib/abstracted_commands/lampCommand.ts index 2a5d9bb0b3..db678e76fb 100644 --- a/src/mahoji/lib/abstracted_commands/lampCommand.ts +++ b/src/mahoji/lib/abstracted_commands/lampCommand.ts @@ -84,13 +84,12 @@ export const XPLamps: IXPLamp[] = [ minimumLevel: 1, allowedSkills: [SkillsEnum.Magic] }, - /* Needs OSJS Update { itemID: 28_820, amount: 5000, name: 'Antique lamp (defender of varrock)', minimumLevel: 1 - },*/ + }, { itemID: itemID('Antique lamp (easy ca)'), amount: 5000, @@ -149,6 +148,13 @@ export const Lampables: IXPObject[] = [ skills[skill] = data.user.skillLevel(skill) * ([ + SkillsEnum.Attack, + SkillsEnum.Strength, + SkillsEnum.Defence, + SkillsEnum.Magic, + SkillsEnum.Ranged, + SkillsEnum.Hitpoints, + SkillsEnum.Prayer, SkillsEnum.Mining, SkillsEnum.Woodcutting, SkillsEnum.Herblore, diff --git a/src/mahoji/lib/abstracted_commands/lmsCommand.ts b/src/mahoji/lib/abstracted_commands/lmsCommand.ts index e7460c0fb4..056bea5212 100644 --- a/src/mahoji/lib/abstracted_commands/lmsCommand.ts +++ b/src/mahoji/lib/abstracted_commands/lmsCommand.ts @@ -53,7 +53,7 @@ export async function lmsCommand( return `You don't have enough points. ${quantity}x ${itemToBuy.item.name} costs ${cost}, but you have ${stats.points}.`; } if (itemToBuy.wins && stats.gamesWon < itemToBuy.wins) { - return `You are not worthy! You need to have won atleast ${itemToBuy.wins} games to buy the ${itemToBuy.item.name}.`; + return `You are not worthy! You need to have won at least ${itemToBuy.wins} games to buy the ${itemToBuy.item.name}.`; } const loot = new Bank().add(itemToBuy.item.id, quantity * (itemToBuy.quantity ?? 1)); await handleMahojiConfirmation(interaction, `Are you sure you want to spend ${cost} points on buying ${loot}?`); 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/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/mahoji/lib/abstracted_commands/minionBuyCommand.ts b/src/mahoji/lib/abstracted_commands/minionBuyCommand.ts index 666fdb1f57..d1a0c78daf 100644 --- a/src/mahoji/lib/abstracted_commands/minionBuyCommand.ts +++ b/src/mahoji/lib/abstracted_commands/minionBuyCommand.ts @@ -2,7 +2,6 @@ import type { CommandResponse } from '@oldschoolgg/toolkit'; import { ComponentType } from 'discord.js'; import { mahojiInformationalButtons } from '../../../lib/constants'; -import { clArrayUpdate } from '../../../lib/handleNewCLItems'; export async function minionBuyCommand(user: MUser, ironman: boolean): CommandResponse { if (user.user.minion_hasBought) return 'You already have a minion!'; @@ -14,7 +13,15 @@ export async function minionBuyCommand(user: MUser, ironman: boolean): CommandRe }); // Ensure user has a userStats row - await clArrayUpdate(user, user.cl); + await prisma.userStats.upsert({ + where: { + user_id: BigInt(user.id) + }, + create: { + user_id: BigInt(user.id) + }, + update: {} + }); return { content: `You have successfully got yourself a minion, and you're ready to use the bot now! Please check out the links below for information you should read. diff --git a/src/mahoji/lib/abstracted_commands/minionKill.ts b/src/mahoji/lib/abstracted_commands/minionKill.ts index c7a231f728..f729fee9a7 100644 --- a/src/mahoji/lib/abstracted_commands/minionKill.ts +++ b/src/mahoji/lib/abstracted_commands/minionKill.ts @@ -22,7 +22,7 @@ import { BitField, PeakTier } from '../../../lib/constants'; import { Eatables } from '../../../lib/data/eatables'; import { getSimilarItems } from '../../../lib/data/similarItems'; import { checkUserCanUseDegradeableItem, degradeItem, degradeablePvmBoostItems } from '../../../lib/degradeableItems'; -import { userhasDiaryIDTier } from '../../../lib/diaries'; +import { userhasDiaryTier } from '../../../lib/diaries'; import type { GearSetupType } from '../../../lib/gear/types'; import { trackLoot } from '../../../lib/lootTrack'; import type { CombatOptionsEnum } from '../../../lib/minions/data/combatConstants'; @@ -48,7 +48,7 @@ import type { Consumable } from '../../../lib/minions/types'; import { calcPOHBoosts } from '../../../lib/poh'; import { SkillsEnum } from '../../../lib/skilling/types'; import { SlayerTaskUnlocksEnum } from '../../../lib/slayer/slayerUnlocks'; -import { determineBoostChoice, getUsersCurrentSlayerInfo } from '../../../lib/slayer/slayerUtil'; +import { determineCombatBoosts, getUsersCurrentSlayerInfo } from '../../../lib/slayer/slayerUtil'; import { maxOffenceStats } from '../../../lib/structures/Gear'; import type { Peak } from '../../../lib/tickers'; import type { MonsterActivityTaskOptions } from '../../../lib/types/minions'; @@ -134,10 +134,10 @@ export async function minionKillCommand( channelID: string, name: string, quantity: number | undefined, - method: PvMMethod | undefined, + method: PvMMethod | PvMMethod[] | undefined, wilderness: boolean | undefined, solo: boolean | undefined -) { +): Promise { if (user.minionIsBusy) { return 'Your minion is busy.'; } @@ -206,32 +206,31 @@ export async function minionKillCommand( } } - // Add jelly check as can barrage in wilderness + // Add check for burstable monsters in wilderness const jelly = monster.id === Monsters.Jelly.id; - const bloodveld = monster.id === Monsters.Bloodveld.id; + const wildyBurst = jelly && isInWilderness; - const wildyBurst = (jelly || bloodveld) && isInWilderness; - - // Set chosen boost based on priority: + // determines what pvm methods the user can use const myCBOpts = user.combatOptions; - const boostChoice = determineBoostChoice({ + const methods = [method] as PvMMethod[]; + const combatMethods = determineCombatBoosts({ cbOpts: myCBOpts as CombatOptionsEnum[], user, monster, - method, + methods, isOnTask, wildyBurst }); - // Check requirements const [hasReqs, reason] = hasMonsterRequirements(user, monster); - if (!hasReqs) return reason ?? "You don't have the requirements to fight this monster"; + if (!hasReqs) { + return typeof reason === 'string' ? reason : "You don't have the requirements to fight this monster"; + } if (monster.diaryRequirement) { - const [diaryID, tier] = monster.diaryRequirement; - const { hasDiary, diaryGroup } = await userhasDiaryIDTier(user, diaryID, tier); + const [hasDiary, _, diaryGroup] = await userhasDiaryTier(user, monster.diaryRequirement); if (!hasDiary) { - return `${user.minionName} is missing the ${diaryGroup.name} ${tier} diary to kill ${monster.name}.`; + return `${user.minionName} is missing the ${diaryGroup.name} ${monster.diaryRequirement[1]} diary to kill ${monster.name}.`; } } @@ -239,7 +238,7 @@ export async function minionKillCommand( const [, osjsMon, attackStyles] = resolveAttackStyles(user, { monsterID: monster.id, - boostMethod: boostChoice + boostMethod: combatMethods }); const [newTime, skillBoostMsg] = applySkillBoost(user, timeToFinish, attackStyles); @@ -457,21 +456,25 @@ export async function minionKillCommand( } // Check for stats - if (boostChoice === 'barrage' && user.skillLevel(SkillsEnum.Magic) < 94) { + if (combatMethods.includes('barrage') && user.skillLevel(SkillsEnum.Magic) < 94) { return `You need 94 Magic to use Ice Barrage. You have ${user.skillLevel(SkillsEnum.Magic)}`; } - if (boostChoice === 'burst' && user.skillLevel(SkillsEnum.Magic) < 70) { + if (combatMethods.includes('burst') && user.skillLevel(SkillsEnum.Magic) < 70) { return `You need 70 Magic to use Ice Burst. You have ${user.skillLevel(SkillsEnum.Magic)}`; } - if (boostChoice === 'chinning' && user.skillLevel(SkillsEnum.Ranged) < 65) { + if (combatMethods.includes('chinning') && user.skillLevel(SkillsEnum.Ranged) < 65) { return `You need 65 Ranged to use Chinning method. You have ${user.skillLevel(SkillsEnum.Ranged)}`; } - // Wildy Monster checks - if (isInWilderness === true && boostChoice === 'cannon') { + // Wildy monster cannon checks + if (isInWilderness === true && combatMethods.includes('cannon')) { if (monster.id === Monsters.HillGiant.id || monster.id === Monsters.MossGiant.id) { usingCannon = isInWilderness; } + if (monster.id === Monsters.Spider.id || Monsters.Scorpion.id) { + usingCannon = isInWilderness; + cannonMulti = isInWilderness; + } if (monster.wildySlayerCave) { usingCannon = isInWilderness; cannonMulti = isInWilderness; @@ -482,8 +485,9 @@ export async function minionKillCommand( } } + // Burst/barrage check with wilderness conditions if ((method === 'burst' || method === 'barrage') && !monster?.canBarrage) { - if (jelly || bloodveld) { + if (jelly) { if (!isInWilderness) { return `${monster.name} can only be barraged or burst in the wilderness.`; } @@ -496,14 +500,18 @@ export async function minionKillCommand( } } - if (boostChoice === 'barrage' && attackStyles.includes(SkillsEnum.Magic) && (monster?.canBarrage || wildyBurst)) { + if ( + combatMethods.includes('barrage') && + attackStyles.includes(SkillsEnum.Magic) && + (monster?.canBarrage || wildyBurst) + ) { consumableCosts.push(iceBarrageConsumables); calculateVirtusBoost(); timeToFinish = reduceNumByPercent(timeToFinish, boostIceBarrage + virtusBoost); boosts.push(`${boostIceBarrage + virtusBoost}% for Ice Barrage${virtusBoostMsg}`); burstOrBarrage = SlayerActivityConstants.IceBarrage; } else if ( - boostChoice === 'burst' && + combatMethods.includes('burst') && attackStyles.includes(SkillsEnum.Magic) && (monster?.canBarrage || wildyBurst) ) { @@ -512,13 +520,14 @@ export async function minionKillCommand( timeToFinish = reduceNumByPercent(timeToFinish, boostIceBurst + virtusBoost); boosts.push(`${boostIceBurst + virtusBoost}% for Ice Burst${virtusBoostMsg}`); burstOrBarrage = SlayerActivityConstants.IceBurst; - } else if ((boostChoice === 'cannon' && hasCannon && monster?.cannonMulti) || cannonMulti) { + } + if ((combatMethods.includes('cannon') && hasCannon && monster?.cannonMulti) || cannonMulti) { usingCannon = true; cannonMulti = true; consumableCosts.push(cannonMultiConsumables); timeToFinish = reduceNumByPercent(timeToFinish, boostCannonMulti); boosts.push(`${boostCannonMulti}% for Cannon in multi`); - } else if ((boostChoice === 'cannon' && hasCannon && monster?.canCannon) || usingCannon) { + } else if ((combatMethods.includes('cannon') && hasCannon && monster?.canCannon) || usingCannon) { usingCannon = true; consumableCosts.push(cannonSingleConsumables); timeToFinish = reduceNumByPercent(timeToFinish, boostCannon); @@ -823,7 +832,7 @@ export async function minionKillCommand( } else { antiPKSupplies.add('Super restore(4)', antiPkRestoresNeeded); } - if (user.bank.amount('Blighted karambwan') >= antiPkKarambwanNeeded) { + if (user.bank.amount('Blighted karambwan') >= antiPkKarambwanNeeded + 20) { antiPKSupplies.add('Blighted karambwan', antiPkKarambwanNeeded); } else { antiPKSupplies.add('Cooked karambwan', antiPkKarambwanNeeded); @@ -927,7 +936,7 @@ export async function minionKillCommand( // Remove items after food calc to prevent losing items if the user doesn't have the right amount of food. Example: Mossy key if (lootToRemove.length > 0) { - updateBankSetting('economyStats_PVMCost', lootToRemove); + await updateBankSetting('economyStats_PVMCost', lootToRemove); await user.specialRemoveItems(lootToRemove, { wildy: !!isInWilderness }); totalCost.add(lootToRemove); } @@ -948,17 +957,17 @@ export async function minionKillCommand( } await addSubTaskToActivityTask({ - monsterID: monster.id, + mi: monster.id, userID: user.id, channelID: channelID.toString(), - quantity, + q: quantity, iQty: inputQuantity, duration, type: 'MonsterKilling', usingCannon: !usingCannon ? undefined : usingCannon, cannonMulti: !cannonMulti ? undefined : cannonMulti, chinning: !chinning ? undefined : chinning, - burstOrBarrage: !burstOrBarrage ? undefined : burstOrBarrage, + bob: !burstOrBarrage ? undefined : burstOrBarrage, died: hasDied, pkEncounters: thePkCount, hasWildySupplies, diff --git a/src/mahoji/lib/abstracted_commands/minionStatusCommand.ts b/src/mahoji/lib/abstracted_commands/minionStatusCommand.ts index 5038c0de6a..689adb9652 100644 --- a/src/mahoji/lib/abstracted_commands/minionStatusCommand.ts +++ b/src/mahoji/lib/abstracted_commands/minionStatusCommand.ts @@ -5,7 +5,6 @@ import { roll, stripNonAlphanumeric } from 'e'; import { ClueTiers } from '../../../lib/clues/clueTiers'; import { BitField, Emoji, minionBuyButton } from '../../../lib/constants'; -import { clArrayUpdate } from '../../../lib/handleNewCLItems'; import { roboChimpSyncData, roboChimpUserFetch } from '../../../lib/roboChimp'; import { makeComponents } from '../../../lib/util'; @@ -57,16 +56,15 @@ async function fetchPinnedTrips(userID: string) { export async function minionStatusCommand(user: MUser): Promise { const { minionIsBusy } = user; - const [roboChimpUser, birdhouseDetails, gearPresetButtons, pinnedTripButtons, dailyIsReady] = await Promise.all([ + const birdhouseDetails = minionIsBusy ? { isReady: false } : calculateBirdhouseDetails(user); + const [roboChimpUser, gearPresetButtons, pinnedTripButtons, dailyIsReady] = await Promise.all([ roboChimpUserFetch(user.id), - minionIsBusy ? { isReady: false } : calculateBirdhouseDetails(user.id), minionIsBusy ? [] : fetchFavoriteGearPresets(user.id), minionIsBusy ? [] : fetchPinnedTrips(user.id), isUsersDailyReady(user) ]); - roboChimpSyncData(user); - await clArrayUpdate(user, user.cl); + await roboChimpSyncData(user); if (user.user.cached_networth_value === null || roll(100)) { await user.update({ cached_networth_value: (await user.calculateNetWorth()).value diff --git a/src/mahoji/lib/abstracted_commands/nightmareCommand.ts b/src/mahoji/lib/abstracted_commands/nightmareCommand.ts index 759dd7800c..170602cfdb 100644 --- a/src/mahoji/lib/abstracted_commands/nightmareCommand.ts +++ b/src/mahoji/lib/abstracted_commands/nightmareCommand.ts @@ -89,7 +89,7 @@ async function checkReqs(user: MUser, monster: KillableMonster, isPhosani: boole return `${user.usernameOrMention} doesn't meet the requirements: ${requirements[1]}.`; } if ((await user.getKC(NightmareMonster.id)) < 50) { - return "You need to have killed The Nightmare atleast 50 times before you can face the Phosani's Nightmare."; + return "You need to have killed The Nightmare at least 50 times before you can face the Phosani's Nightmare."; } } } @@ -100,13 +100,13 @@ function perUserCost(user: MUser, quantity: number, isPhosani: boolean, hasShado const sangCharges = sangChargesPerKc * quantity; if (isPhosani) { if (hasShadow && user.user.tum_shadow_charges < tumCharges) { - return `You need atleast ${tumCharges} Tumeken's shadow charges to use it, otherwise it has to be unequipped: ${mentionCommand( + return `You need at least ${tumCharges} Tumeken's shadow charges to use it, otherwise it has to be unequipped: ${mentionCommand( globalClient, 'minion', 'charge' )}`; } else if (hasSang && user.user.sang_charges < sangCharges) { - return `You need atleast ${sangCharges} Sanguinesti staff charges to use it, otherwise it has to be unequipped: ${mentionCommand( + return `You need at least ${sangCharges} Sanguinesti staff charges to use it, otherwise it has to be unequipped: ${mentionCommand( globalClient, 'minion', 'charge' diff --git a/src/mahoji/lib/abstracted_commands/puroPuroCommand.ts b/src/mahoji/lib/abstracted_commands/puroPuroCommand.ts index 41a816b15d..c4142eb55d 100644 --- a/src/mahoji/lib/abstracted_commands/puroPuroCommand.ts +++ b/src/mahoji/lib/abstracted_commands/puroPuroCommand.ts @@ -68,7 +68,7 @@ export async function puroPuroStartCommand( } if (!impToHunt) return 'Error selecting impling, please try again.'; if (hunterLevel < impToHunt.hunterLevel) - return `${user.minionName} needs atleast level ${impToHunt.hunterLevel} hunter to hunt ${impToHunt.name} in Puro-Puro.`; + return `${user.minionName} needs at least level ${impToHunt.hunterLevel} hunter to hunt ${impToHunt.name} in Puro-Puro.`; if (!darkLure || (darkLure && !impToHunt.spell)) darkLure = false; if (darkLure) { if (user.QP < 9) return 'To use Dark Lure, you need 9 QP.'; diff --git a/src/mahoji/lib/abstracted_commands/pyramidPlunderCommand.ts b/src/mahoji/lib/abstracted_commands/pyramidPlunderCommand.ts index 8825d580a5..9c51f0e8ab 100644 --- a/src/mahoji/lib/abstracted_commands/pyramidPlunderCommand.ts +++ b/src/mahoji/lib/abstracted_commands/pyramidPlunderCommand.ts @@ -15,7 +15,7 @@ export async function pyramidPlunderCommand(user: MUser, channelID: string) { const thievingLevel = skills.thieving; const minLevel = plunderRooms[0].thievingLevel; if (thievingLevel < minLevel) { - return `You need atleast level ${minLevel} Thieving to do the Pyramid Plunder.`; + return `You need at least level ${minLevel} Thieving to do the Pyramid Plunder.`; } const completableRooms = plunderRooms.filter(room => thievingLevel >= room.thievingLevel); diff --git a/src/mahoji/lib/abstracted_commands/sepulchreCommand.ts b/src/mahoji/lib/abstracted_commands/sepulchreCommand.ts index e1bc9defe6..46b2c4fc29 100644 --- a/src/mahoji/lib/abstracted_commands/sepulchreCommand.ts +++ b/src/mahoji/lib/abstracted_commands/sepulchreCommand.ts @@ -14,11 +14,11 @@ export async function sepulchreCommand(user: MUser, channelID: string) { const thievingLevel = skills.thieving; const minLevel = sepulchreFloors[0].agilityLevel; if (agilityLevel < minLevel) { - return `You need atleast level ${minLevel} Agility to do the Hallowed Sepulchre.`; + return `You need at least level ${minLevel} Agility to do the Hallowed Sepulchre.`; } if (thievingLevel < 66) { - return 'You need atleast level 66 Thieving to do the Hallowed Sepulchre.'; + return 'You need at least level 66 Thieving to do the Hallowed Sepulchre.'; } if (!userHasGracefulEquipped(user)) { diff --git a/src/mahoji/lib/abstracted_commands/shadesOfMortonCommand.ts b/src/mahoji/lib/abstracted_commands/shadesOfMortonCommand.ts index c03f36826f..610571d084 100644 --- a/src/mahoji/lib/abstracted_commands/shadesOfMortonCommand.ts +++ b/src/mahoji/lib/abstracted_commands/shadesOfMortonCommand.ts @@ -298,7 +298,7 @@ export async function shadesOfMortonStartCommand(user: MUser, channelID: string, if (!user.owns(cost)) return `You don't own: ${cost}.`; await user.removeItemsFromBank(cost); - await userStatsBankUpdate(user.id, 'shades_of_morton_cost_bank', cost); + await userStatsBankUpdate(user, 'shades_of_morton_cost_bank', cost); await addSubTaskToActivityTask({ userID: user.id, 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 17ce1582ff..43c81d0ce0 100644 --- a/src/mahoji/lib/abstracted_commands/statCommand.ts +++ b/src/mahoji/lib/abstracted_commands/statCommand.ts @@ -30,7 +30,7 @@ import { createChart } from '../../../lib/util/chart'; import { getItem } from '../../../lib/util/getOSItem'; import { makeBankImage } from '../../../lib/util/makeBankImage'; import { Cooldowns } from '../Cooldowns'; -import { collectables } from './collectCommand'; +import { collectables } from '../collectables'; interface DataPiece { name: string; @@ -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 @@ -359,14 +362,14 @@ GROUP BY type;`); name: 'Personal Monster KC', perkTierNeeded: PerkTier.Four, run: async (user: MUser) => { - const result: { id: number; kc: number }[] = await prisma.$queryRawUnsafe(`SELECT (data->>'monsterID')::int as id, SUM((data->>'quantity')::int)::int AS kc + const result: { id: number; kc: number }[] = await prisma.$queryRawUnsafe(`SELECT (data->>'mi')::int as id, SUM((data->>'q')::int)::int AS kc FROM activity WHERE completed = true AND user_id = ${BigInt(user.id)} AND type = 'MonsterKilling' AND data IS NOT NULL AND data::text != '{}' -GROUP BY data->>'monsterID';`); +GROUP BY data->>'mi';`); const dataPoints: [string, number][] = result .sort((a, b) => b.kc - a.kc) .slice(0, 30) @@ -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/abstracted_commands/tobCommand.ts b/src/mahoji/lib/abstracted_commands/tobCommand.ts index 3b1897abad..5b6f1b9e06 100644 --- a/src/mahoji/lib/abstracted_commands/tobCommand.ts +++ b/src/mahoji/lib/abstracted_commands/tobCommand.ts @@ -180,11 +180,11 @@ async function checkTOBUser( } const dartsNeeded = 150 * quantity; if (blowpipeData.dartQuantity < dartsNeeded) { - return [true, `${user.usernameOrMention}, you need atleast ${dartsNeeded} darts in your blowpipe.`]; + return [true, `${user.usernameOrMention}, you need at least ${dartsNeeded} darts in your blowpipe.`]; } const scalesNeeded = 1000 * quantity; if (blowpipeData.scales < scalesNeeded) { - return [true, `${user.usernameOrMention}, you need atleast ${scalesNeeded} scales in your blowpipe.`]; + return [true, `${user.usernameOrMention}, you need at least ${scalesNeeded} scales in your blowpipe.`]; } const dartIndex = blowpipeDarts.indexOf(getOSItem(blowpipeData.dartID)); if (dartIndex < 5) { @@ -209,7 +209,7 @@ async function checkTOBUser( if (rangeGear.ammo!.quantity < arrowsRequired) { return [ true, - `${user.usernameOrMention}, you need atleast ${arrowsRequired} arrows equipped in your range setup.` + `${user.usernameOrMention}, you need at least ${arrowsRequired} arrows equipped in your range setup.` ]; } @@ -217,7 +217,7 @@ async function checkTOBUser( const kc = await getMinigameScore(user.id, 'tob'); if (kc < 250) { - return [true, `${user.usernameOrMention} needs atleast 250 Theatre of Blood KC before doing Hard mode.`]; + return [true, `${user.usernameOrMention} needs at least 250 Theatre of Blood KC before doing Hard mode.`]; } if (!meleeGear.hasEquipped('Infernal cape')) { return [true, `${user.usernameOrMention} needs an Infernal cape to do Hard mode.`]; @@ -227,7 +227,7 @@ async function checkTOBUser( if (teamSize === 2) { const kc = await getMinigameScore(user.id, isHardMode ? 'tob_hard' : 'tob'); if (kc < 150) { - return [true, `${user.usernameOrMention} needs atleast 150 KC before doing duo's.`]; + return [true, `${user.usernameOrMention} needs at least 150 KC before doing duo's.`]; } } @@ -306,7 +306,7 @@ export async function tobStartCommand( if (isHardMode) { const normalKC = await getMinigameScore(user.id, 'tob'); if (normalKC < 250) { - return 'You need atleast 250 completions of the Theatre of Blood before you can attempt Hard Mode.'; + return 'You need at least 250 completions of the Theatre of Blood before you can attempt Hard Mode.'; } } if (user.minionIsBusy) { @@ -427,7 +427,7 @@ export async function tobStartCommand( .add(u.gear.range.ammo?.item, 100) .multiply(qty) ); - await userStatsBankUpdate(u.id, 'tob_cost', realCost); + await userStatsBankUpdate(u, 'tob_cost', realCost); const effectiveCost = realCost.clone().remove('Coins', realCost.amount('Coins')); totalCost.add(effectiveCost); if (u.gear.melee.hasEquipped('Abyssal tentacle')) { diff --git a/src/mahoji/lib/abstracted_commands/trekCommand.ts b/src/mahoji/lib/abstracted_commands/trekCommand.ts index 12f05f54dd..25b62cfcdc 100644 --- a/src/mahoji/lib/abstracted_commands/trekCommand.ts +++ b/src/mahoji/lib/abstracted_commands/trekCommand.ts @@ -53,7 +53,7 @@ export async function trekCommand(user: MUser, channelID: string, difficulty: st if (!meetsRequirements) { return `You don't have the requirements to do ${tier.difficulty} treks! Your ${readableStatName( unmetKey! - )} stat in your ${setup} setup is ${has}, but you need atleast ${ + )} stat in your ${setup} setup is ${has}, but you need at least ${ tier.minimumGearRequirements[setup]?.[unmetKey!] }.`; } @@ -62,7 +62,7 @@ export async function trekCommand(user: MUser, channelID: string, difficulty: st } if (qp < 30) { - return 'You need atleast level 30 QP to do Temple Trekking.'; + return 'You need at least level 30 QP to do Temple Trekking.'; } if (minLevel !== undefined && user.combatLevel < minLevel) { diff --git a/src/mahoji/lib/abstracted_commands/volcanicMineCommand.ts b/src/mahoji/lib/abstracted_commands/volcanicMineCommand.ts index 8ddb48a360..6abdcf379d 100644 --- a/src/mahoji/lib/abstracted_commands/volcanicMineCommand.ts +++ b/src/mahoji/lib/abstracted_commands/volcanicMineCommand.ts @@ -234,6 +234,6 @@ export async function volcanicMineStatsCommand(user: MUser) { const currentUserPoints = user.user.volcanic_mine_points; const kc = await getMinigameScore(user.id, 'volcanic_mine'); - return `You have ${currentUserPoints.toLocaleString()} Volanic Mine points points. + return `You have ${currentUserPoints.toLocaleString()} Volcanic Mine points. You have completed ${kc} games of Volcanic Mine.`; } diff --git a/src/mahoji/lib/abstracted_commands/warriorsGuildCommand.ts b/src/mahoji/lib/abstracted_commands/warriorsGuildCommand.ts index 46a0472f69..fe12eddfff 100644 --- a/src/mahoji/lib/abstracted_commands/warriorsGuildCommand.ts +++ b/src/mahoji/lib/abstracted_commands/warriorsGuildCommand.ts @@ -82,7 +82,7 @@ async function cyclopsCommand(user: MUser, channelID: string, quantity: number | // Check if either 100 warrior guild tokens or attack cape (similar items in future) const amountTokens = userBank.amount('Warrior guild token'); if (!hasAttackCape && amountTokens < 100) { - return 'You need atleast 100 Warriors guild tokens to kill Cyclops.'; + return 'You need at least 100 Warriors guild tokens to kill Cyclops.'; } // If no quantity provided, set it to the max. if (!quantity) { @@ -107,7 +107,7 @@ async function cyclopsCommand(user: MUser, channelID: string, quantity: number | if (!hasAttackCape && amountTokens < tokensToSpend) { return `You don't have enough Warrior guild tokens to kill cyclopes for ${formatDuration( duration - )}, try a lower quantity. You need atleast ${Math.floor( + )}, try a lower quantity. You need at least ${Math.floor( (duration / Time.Minute) * 10 + 10 )}x Warrior guild tokens to kill ${quantity}x cyclopes.`; } @@ -144,7 +144,7 @@ export async function warriorsGuildCommand( const atkLvl = user.skillLevel('attack'); const strLvl = user.skillLevel('strength'); if (atkLvl + strLvl < 130 && atkLvl !== 99 && strLvl !== 99) { - return "To enter the Warrior's Guild, your Attack and Strength levels must add up to atleast 130, or you must have level 99 in either."; + return "To enter the Warrior's Guild, your Attack and Strength levels must add up to at least 130, or you must have level 99 in either."; } if (choice === 'cyclops') { diff --git a/src/mahoji/lib/collectables.ts b/src/mahoji/lib/collectables.ts new file mode 100644 index 0000000000..28f0775291 --- /dev/null +++ b/src/mahoji/lib/collectables.ts @@ -0,0 +1,146 @@ +import { Time } from 'e'; +import { Bank } from 'oldschooljs'; +import type { Item } from 'oldschooljs/dist/meta/types'; +import type { Skills } from '../../lib/types'; +import getOSItem from '../../lib/util/getOSItem'; + +interface Collectable { + item: Item; + skillReqs?: Skills; + itemCost?: Bank; + quantity: number; + duration: number; + qpRequired?: number; +} + +export const collectables: Collectable[] = [ + { + item: getOSItem('Blue dragon scale'), + quantity: 26, + itemCost: new Bank({ + 'Water rune': 1, + 'Air rune': 3, + 'Law rune': 1 + }), + skillReqs: { + agility: 70, + magic: 37 + }, + duration: Time.Minute * 2 + }, + { + item: getOSItem('Mort myre fungus'), + quantity: 100, + itemCost: new Bank({ + 'Prayer potion(4)': 1, + 'Ring of dueling(8)': 1 + }), + skillReqs: { + prayer: 50 + }, + duration: Time.Minute * 8.3, + qpRequired: 32 + }, + { + item: getOSItem('Flax'), + quantity: 28, + duration: Time.Minute * 1.68 + }, + { + item: getOSItem('Swamp toad'), + quantity: 28, + duration: Time.Minute * 1.68 + }, + { + item: getOSItem("Red spiders' eggs"), + quantity: 80, + itemCost: new Bank({ + 'Stamina potion(4)': 1 + }), + duration: Time.Minute * 8.5 + }, + { + item: getOSItem('Wine of zamorak'), + quantity: 27, + itemCost: new Bank({ + 'Law rune': 27, + 'Air rune': 27 + }), + skillReqs: { + magic: 33 + }, + duration: Time.Minute * 3.12 + }, + { + item: getOSItem('White berries'), + quantity: 27, + qpRequired: 22, + skillReqs: { + ranged: 60, + thieving: 50, + agility: 56, + crafting: 10, + fletching: 5, + cooking: 30 + }, + duration: Time.Minute * 4.05 + }, + { + item: getOSItem('Snape grass'), + quantity: 120, + itemCost: new Bank({ + 'Law rune': 12, + 'Astral rune': 12 + }), + duration: Time.Minute * 6.5, + qpRequired: 72 + }, + { + item: getOSItem('Snake weed'), + quantity: 150, + itemCost: new Bank({ + 'Ring of dueling(8)': 1 + }), + duration: Time.Minute * 30, + qpRequired: 3 + }, + { + item: getOSItem('Bucket of sand'), + quantity: 30, + itemCost: new Bank({ + 'Law rune': 1, + Coins: 30 * 25 + }), + duration: Time.Minute, + qpRequired: 30 + }, + { + item: getOSItem('Jangerberries'), + quantity: 224, + itemCost: new Bank({ + 'Ring of dueling(8)': 1 + }), + skillReqs: { + agility: 10 + }, + duration: Time.Minute * 24 + }, + // Miniquest to get Tarn's diary for Salve amulet (e)/(ei) + { + item: getOSItem("Tarn's diary"), + quantity: 1, + itemCost: new Bank({ + 'Prayer potion(4)': 2 + }), + skillReqs: { + slayer: 40, + attack: 60, + strength: 60, + ranged: 60, + defence: 60, + magic: 60 + }, + duration: 10 * Time.Minute, + qpRequired: 100 + } +]; diff --git a/src/mahoji/lib/events.ts b/src/mahoji/lib/events.ts index db954c8fd4..556b4aeffc 100644 --- a/src/mahoji/lib/events.ts +++ b/src/mahoji/lib/events.ts @@ -1,79 +1,100 @@ import type { ItemBank } from 'oldschooljs/dist/meta/types'; import { bulkUpdateCommands } from '@oldschoolgg/toolkit'; -import { DEV_SERVER_ID, production } from '../../config'; -import { cacheBadges } from '../../lib/badges'; -import { syncBlacklists } from '../../lib/blacklists'; -import { Channel, DISABLED_COMMANDS, META_CONSTANTS, globalConfig } from '../../lib/constants'; +import { ActivityType, bold, time } from 'discord.js'; +import { Channel, META_CONSTANTS, globalConfig } from '../../lib/constants'; import { initCrons } from '../../lib/crons'; -import { GrandExchange } from '../../lib/grandExchange'; - import { initTickers } from '../../lib/tickers'; -import { runTimedLoggedFn } from '../../lib/util'; -import { cacheCleanup } from '../../lib/util/cachedUserIDs'; +import { logWrapFn } from '../../lib/util'; 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() { - const clientData = await mahojiClientSettingsFetch({ custom_prices: true }); - for (const [key, value] of Object.entries(clientData.custom_prices as ItemBank)) { - CUSTOM_PRICE_CACHE.set(Number(key), Number(value)); - } -} +export async function updateTestBotStatus(online = true) { + try { + if (globalConfig.isProduction) return; + const idMap: Record = { + '829398443821891634': '1265571664142270464', + '577488230539067403': '1265582554644217977', + '353484579840983042': '1265582554644217977', + '897549995446779964': '1265582743970910259', + '1158785741028081696': '1265583194108067925' + }; + const catChannelID = idMap[globalConfig.clientID]; + if (!catChannelID) return; + const cat = await globalClient.channels.fetch(catChannelID); + if (!cat || !cat.isTextBased() || cat.isDMBased()) { + console.log('Could not find status channel'); + return; + } -export async function onStartup() { - globalClient.application.commands.fetch({ guildId: production ? undefined : DEV_SERVER_ID }); + const emoji = online ? '🟢' : '🔴'; + let text = ''; + if (online) { + text = `${emoji} ${globalClient.user.username} is ONLINE ${emoji} - // Sync disabled commands - const disabledCommands = await prisma.clientStorage.upsert({ - where: { - id: globalConfig.clientID - }, - select: { disabled_commands: true }, - create: { - id: globalConfig.clientID - }, - update: {} - }); +Turned on ${time(new Date(), 'R')}`; + text = bold(text); + } else { + text = `${emoji} ${globalClient.user.username} is offline ${emoji} - if (disabledCommands.disabled_commands) { - for (const command of disabledCommands.disabled_commands) { - DISABLED_COMMANDS.add(command); +Turned off ${time(new Date(), 'R')}`; + } + const message = await cat.messages + .fetch({ limit: 5 }) + .then(messages => messages.filter(m => m.author.id === globalClient.user!.id)) + .then(msg => msg.first()); + if (!message) { + await cat.send(text); + } else { + await message.edit(text); } + if (online) { + await globalClient.user.setPresence({ + status: 'online', + activities: [ + { + name: `${emoji} ONLINE`, + type: ActivityType.Custom + } + ] + }); + } + } catch (err) { + console.error(err); } - - // Sync blacklists - await syncBlacklists(); - - if (!production) { - console.log('Syncing commands locally...'); - await bulkUpdateCommands({ - client: globalClient.mahojiClient, - commands: Array.from(globalClient.mahojiClient.commands.values()), - guildID: DEV_SERVER_ID - }); +} +export async function syncCustomPrices() { + const clientData = await mahojiClientSettingsFetch({ custom_prices: true }); + for (const [key, value] of Object.entries(clientData.custom_prices as ItemBank)) { + CUSTOM_PRICE_CACHE.set(Number(key), Number(value)); } +} - runTimedLoggedFn('Syncing prices', syncCustomPrices); - - runTimedLoggedFn('Caching badges', cacheBadges); - runTimedLoggedFn('Cache Usernames', cacheUsernames); - cacheCleanup(); - - runTimedLoggedFn('Sync Linked Accounts', syncLinkedAccounts); - runTimedLoggedFn('Init Grand Exchange', GrandExchange.init.bind(GrandExchange)); +export const onStartup = logWrapFn('onStartup', async () => { + const syncTestBotCommands = globalConfig.isProduction + ? null + : bulkUpdateCommands({ + client: globalClient.mahojiClient, + commands: Array.from(globalClient.mahojiClient.commands.values()), + guildID: globalConfig.testingServerID + }); initCrons(); initTickers(); - if (production) { - sendToChannelID(Channel.GeneralChannel, { - content: `I have just turned on! + const sendStartupMessage = globalConfig.isProduction + ? sendToChannelID(Channel.GeneralChannel, { + content: `I have just turned on!\n\n${META_CONSTANTS.RENDERED_STR}` + }).catch(console.error) + : null; -${META_CONSTANTS.RENDERED_STR}` - }).catch(console.error); - } -} + await Promise.all([ + globalClient.application.commands.fetch({ + guildId: globalConfig.isProduction ? undefined : globalConfig.testingServerID + }), + updateTestBotStatus(), + sendStartupMessage, + syncTestBotCommands + ]); +}); diff --git a/src/mahoji/lib/exitHandler.ts b/src/mahoji/lib/exitHandler.ts new file mode 100644 index 0000000000..14324c629b --- /dev/null +++ b/src/mahoji/lib/exitHandler.ts @@ -0,0 +1,21 @@ +import { TimerManager } from '@sapphire/timer-manager'; + +import { updateTestBotStatus } from './events'; + +export async function exitCleanup() { + try { + globalClient.isShuttingDown = true; + console.log('Cleaning up and exiting...'); + TimerManager.destroy(); + await updateTestBotStatus(false); + await Promise.all([ + globalClient.destroy(), + prisma.$disconnect(), + redis.disconnect(), + roboChimpClient.$disconnect() + ]); + console.log('\nCleaned up and exited.'); + } catch (err) { + console.error(err); + } +} diff --git a/src/mahoji/lib/inhibitors.ts b/src/mahoji/lib/inhibitors.ts index 1a6b67ff3f..2f3685e47f 100644 --- a/src/mahoji/lib/inhibitors.ts +++ b/src/mahoji/lib/inhibitors.ts @@ -1,12 +1,10 @@ -import type { DMChannel, Guild, GuildMember, InteractionReplyOptions, TextChannel, User } from 'discord.js'; +import { PerkTier, formatDuration } from '@oldschoolgg/toolkit'; +import type { DMChannel, Guild, GuildMember, InteractionReplyOptions, TextChannel } from 'discord.js'; import { ComponentType, PermissionsBitField } from 'discord.js'; -import { PerkTier } from '@oldschoolgg/toolkit'; -import { formatDuration } from '@oldschoolgg/toolkit'; import { OWNER_IDS, SupportServer } from '../../config'; import { BLACKLISTED_GUILDS, BLACKLISTED_USERS } from '../../lib/blacklists'; import { BadgesEnum, BitField, Channel, DISABLED_COMMANDS, minionBuyButton } from '../../lib/constants'; -import { perkTierCache, syncPerkTierOfUser } from '../../lib/perkTiers'; import type { CategoryFlag } from '../../lib/types'; import { minionIsBusy } from '../../lib/util/minionIsBusy'; import { mahojiGuildSettingsFetch, untrustedGuildSettingsCache } from '../guildSettings'; @@ -31,7 +29,6 @@ export interface AbstractCommand { interface Inhibitor { name: string; run: (options: { - APIUser: User; user: MUser; command: AbstractCommand; guild: Guild | null; @@ -54,23 +51,6 @@ const inhibitors: Inhibitor[] = [ }, canBeDisabled: false }, - { - name: 'bots', - run: async ({ APIUser, user }) => { - if (!APIUser.bot) return false; - if ( - ![ - '798308589373489172', // BIRDIE#1963 - '902745429685469264' // Randy#0008 - ].includes(user.id) - ) { - return { content: 'Bots cannot use commands.' }; - } - return false; - }, - canBeDisabled: false, - silent: true - }, { name: 'hasMinion', run: async ({ user, command }) => { @@ -122,9 +102,9 @@ const inhibitors: Inhibitor[] = [ }, { name: 'disabled', - run: async ({ command, guild, APIUser }) => { + run: async ({ command, guild, user }) => { if ( - !OWNER_IDS.includes(APIUser.id) && + !OWNER_IDS.includes(user.id) && (command.attributes?.enabled === false || DISABLED_COMMANDS.has(command.name)) ) { return { content: 'This command is globally disabled.' }; @@ -143,11 +123,7 @@ const inhibitors: Inhibitor[] = [ run: async ({ member, guild, channel, user }) => { if (!guild || guild.id !== SupportServer) return false; if (channel.id !== Channel.General) return false; - - let perkTier = perkTierCache.get(user.id); - if (!perkTier) { - perkTier = syncPerkTierOfUser(user); - } + const perkTier = user.perkTier(); if (member && perkTier >= PerkTier.Two) { return false; } @@ -168,8 +144,7 @@ const inhibitors: Inhibitor[] = [ // Allow contributors + moderators to use disabled channels in SupportServer const userBitfield = user.bitfield; - const isStaff = - userBitfield.includes(BitField.isModerator) || userBitfield.includes(BitField.isContributor); + const isStaff = userBitfield.includes(BitField.isModerator); if (guild.id === SupportServer && isStaff) { return false; } @@ -245,11 +220,9 @@ export async function runInhibitors({ member, command, guild, - bypassInhibitors, - APIUser + bypassInhibitors }: { user: MUser; - APIUser: User; channel: TextChannel | DMChannel; member: GuildMember | null; command: AbstractCommand; @@ -258,7 +231,7 @@ export async function runInhibitors({ }): Promise { for (const { run, canBeDisabled, silent } of inhibitors) { if (bypassInhibitors && canBeDisabled) continue; - const result = await run({ user, channel, member, command, guild, APIUser }); + const result = await run({ user, channel, member, command, guild }); if (result !== false) { return { reason: result, silent: Boolean(silent) }; } diff --git a/src/mahoji/lib/postCommand.ts b/src/mahoji/lib/postCommand.ts index cfa61abad4..eceb159855 100644 --- a/src/mahoji/lib/postCommand.ts +++ b/src/mahoji/lib/postCommand.ts @@ -3,6 +3,7 @@ import type { CommandOptions } from '@oldschoolgg/toolkit'; import { modifyBusyCounter } from '../../lib/busyCounterCache'; import { busyImmuneCommands, shouldTrackCommand } from '../../lib/constants'; +import { TimerManager } from '@sapphire/timer-manager'; import { makeCommandUsage } from '../../lib/util/commandUsage'; import { logError } from '../../lib/util/logError'; import type { AbstractCommand } from './inhibitors'; @@ -28,15 +29,8 @@ export async function postCommand({ continueDeltaMillis: number | null; }): Promise { if (!busyImmuneCommands.includes(abstractCommand.name)) { - setTimeout(() => modifyBusyCounter(userID, -1), 1000); + TimerManager.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, @@ -45,14 +39,16 @@ export async function postCommand({ commandName: abstractCommand.name, args, isContinue, - flags: null, inhibited, continueDeltaMillis }); try { await prisma.$transaction([ prisma.commandUsage.create({ - data: commandUsage + data: commandUsage, + select: { + id: true + } }), prisma.user.update({ where: { @@ -60,6 +56,9 @@ export async function postCommand({ }, data: { last_command_date: new Date() + }, + select: { + id: true } }) ]); diff --git a/src/mahoji/lib/preCommand.ts b/src/mahoji/lib/preCommand.ts index ba8bafbdb1..1232d995fb 100644 --- a/src/mahoji/lib/preCommand.ts +++ b/src/mahoji/lib/preCommand.ts @@ -1,55 +1,14 @@ -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 { logWrapFn } from '../../lib/util'; 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 -}: { +interface PreCommandOptions { apiUser: User | null; abstractCommand: AbstractCommand; userID: string; @@ -57,46 +16,48 @@ export async function preCommand({ channelID: string | bigint; bypassInhibitors: boolean; options: CommandOptions; -}): Promise< +} + +type PrecommandReturn = Promise< | undefined | { reason: InteractionReplyOptions; - silent: boolean; 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); +>; +export const preCommand: (opts: PreCommandOptions) => PrecommandReturn = logWrapFn('PreCommand', rawPreCommand); +async function rawPreCommand({ + abstractCommand, + userID, + guildID, + channelID, + bypassInhibitors, + apiUser +}: PreCommandOptions): PrecommandReturn { if (globalClient.isShuttingDown) { return { - silent: true, reason: { content: 'The bot is currently restarting, please try again later.' }, dontRunPostCommand: true }; } - const user = await mUserFetch(userID); - user.checkBankBackground(); + + const username = apiUser?.username ? cleanUsername(apiUser?.username) : undefined; + const user = await mUserFetch(userID, { + username + }); + + // TODO: user.checkBankBackground(); if (userIsBusy(userID) && !bypassInhibitors && !busyImmuneCommands.includes(abstractCommand.name)) { - return { silent: true, reason: { content: 'You cannot use a command right now.' }, dontRunPostCommand: true }; + return { reason: { content: 'You cannot use a command right now.' }, dontRunPostCommand: true }; } if (!busyImmuneCommands.includes(abstractCommand.name)) modifyBusyCounter(userID, 1); 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), guild: guild ?? null, member: member ?? null, command: abstractCommand, diff --git a/src/mahoji/mahojiSettings.ts b/src/mahoji/mahojiSettings.ts index 1e6a3284d6..e4df529d54 100644 --- a/src/mahoji/mahojiSettings.ts +++ b/src/mahoji/mahojiSettings.ts @@ -1,6 +1,6 @@ -import { evalMathExpression } from '@oldschoolgg/toolkit/dist/util/expressionParser'; +import { evalMathExpression } from '@oldschoolgg/toolkit'; import type { Prisma, User, UserStats } from '@prisma/client'; -import { isFunction, objectEntries, round } from 'e'; +import { isObject, objectEntries, round } from 'e'; import { Bank } from 'oldschooljs'; import type { SelectedUserStats } from '../lib/MUser'; @@ -11,6 +11,7 @@ import type { Rune } from '../lib/skilling/skills/runecraft'; import { hasGracefulEquipped } from '../lib/structures/Gear'; import type { ItemBank } from '../lib/types'; import { + type JsonKeys, anglerBoosts, formatItemReqs, hasSkillReqs, @@ -69,38 +70,35 @@ export function getMahojiBank(user: { bank: Prisma.JsonValue }) { return new Bank(user.bank as ItemBank); } +export async function fetchUserStats( + userID: string, + selectKeys: T +): Promise> { + const keysToSelect = Object.keys(selectKeys).length === 0 ? { user_id: true } : selectKeys; + const result = await prisma.userStats.upsert({ + where: { + user_id: BigInt(userID) + }, + create: { + user_id: BigInt(userID) + }, + update: {}, + select: keysToSelect + }); + + return result as unknown as SelectedUserStats; +} + export async function userStatsUpdate( userID: string, - data: Omit | ((u: UserStats) => Prisma.UserStatsUpdateInput), + data: Omit, selectKeys?: T ): Promise> { const id = BigInt(userID); - let keys: object | undefined = selectKeys; if (!selectKeys || Object.keys(selectKeys).length === 0) { keys = { user_id: true }; } - - if (isFunction(data)) { - const userStats = await prisma.userStats.upsert({ - create: { - user_id: id - }, - update: {}, - where: { - user_id: id - } - }); - - return (await prisma.userStats.update({ - data: data(userStats), - where: { - user_id: id - }, - select: keys - })) as SelectedUserStats; - } - await prisma.userStats.upsert({ create: { user_id: id @@ -109,7 +107,7 @@ export async function userStatsUpdate; } -export async function userStatsBankUpdate(userID: string, key: keyof UserStats, bank: Bank) { - await userStatsUpdate( - userID, - u => ({ - [key]: bank.clone().add(u[key] as ItemBank).bank - }), - {} - ); -} - -export async function multipleUserStatsBankUpdate(userID: string, updates: Partial>) { +export async function userStatsBankUpdate(user: MUser | string, key: JsonKeys, bank: Bank) { + if (!key) throw new Error('No key provided to userStatsBankUpdate'); + const userID = typeof user === 'string' ? user : user.id; + const stats = + typeof user === 'string' + ? await fetchUserStats(userID, { [key]: true }) + : await user.fetchStats({ [key]: true }); + const currentItemBank = stats[key] as ItemBank; + if (!isObject(currentItemBank)) { + throw new Error(`Key ${key} is not an object.`); + } await userStatsUpdate( userID, - u => { - const updateObj: Prisma.UserStatsUpdateInput = {}; - for (const [key, bank] of objectEntries(updates)) { - updateObj[key] = bank?.clone().add(u[key] as ItemBank).bank; - } - return updateObj; + { + [key]: bank.clone().add(currentItemBank).bank }, - {} + { [key]: true } ); } @@ -263,7 +257,7 @@ export function hasMonsterRequirements(user: MUser, monster: KillableMonster) { false, `You don't have the requirements to kill ${monster.name}! Your ${readableStatName( unmetKey! - )} stat in your ${setup} setup is ${has}, but you need atleast ${ + )} stat in your ${setup} setup is ${has}, but you need at least ${ monster.minimumGearRequirements[setup]?.[unmetKey!] }.` ]; @@ -339,12 +333,13 @@ export async function addToGPTaxBalance(userID: string | string, amount: number) ]); } -export async function addToOpenablesScores(mahojiUser: MUser, kcBank: Bank) { +export async function addToOpenablesScores(user: MUser, kcBank: Bank) { + const stats = await user.fetchStats({ openable_scores: true }); const { openable_scores: newOpenableScores } = await userStatsUpdate( - mahojiUser.id, - ({ openable_scores }) => ({ - openable_scores: new Bank(openable_scores as ItemBank).add(kcBank).bank - }), + user.id, + { + openable_scores: new Bank(stats.openable_scores as ItemBank).add(kcBank).bank + }, { openable_scores: true } ); return new Bank(newOpenableScores as ItemBank); diff --git a/src/tasks/minions/HunterActivity/hunterActivity.ts b/src/tasks/minions/HunterActivity/hunterActivity.ts index 914d29dfdd..24994a87f5 100644 --- a/src/tasks/minions/HunterActivity/hunterActivity.ts +++ b/src/tasks/minions/HunterActivity/hunterActivity.ts @@ -178,8 +178,8 @@ 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/src/tasks/minions/PrayerActivity/scatteringActivity.ts b/src/tasks/minions/PrayerActivity/scatteringActivity.ts index 3a3ab92b9d..a94d07b378 100644 --- a/src/tasks/minions/PrayerActivity/scatteringActivity.ts +++ b/src/tasks/minions/PrayerActivity/scatteringActivity.ts @@ -24,7 +24,7 @@ export const scatteringTask: MinionTask = { const str = `${user}, ${user.minionName} finished scattering ${quantity}x ${ash.name}. ${xpRes}`; - await userStatsBankUpdate(user.id, 'scattered_ashes_bank', new Bank().add(ash.inputId, quantity)); + await userStatsBankUpdate(user, 'scattered_ashes_bank', new Bank().add(ash.inputId, quantity)); handleTripFinish(user, channelID, str, undefined, data, null); } diff --git a/src/tasks/minions/agilityActivity.ts b/src/tasks/minions/agilityActivity.ts index fbf394197d..1d09a9fbe6 100644 --- a/src/tasks/minions/agilityActivity.ts +++ b/src/tasks/minions/agilityActivity.ts @@ -70,11 +70,12 @@ export const agilityTask: MinionTask = { const xpReceived = (quantity - lapsFailed / 2) * (typeof course.xp === 'number' ? course.xp : course.xp(currentLevel)); + const stats = await user.fetchStats({ laps_scores: true }); const { laps_scores: newLapScores } = await userStatsUpdate( user.id, - ({ laps_scores }) => ({ - laps_scores: addItemToBank(laps_scores as ItemBank, course.id, quantity - lapsFailed) - }), + { + laps_scores: addItemToBank(stats.laps_scores as ItemBank, course.id, quantity - lapsFailed) + }, { laps_scores: true } ); diff --git a/src/tasks/minions/clueActivity.ts b/src/tasks/minions/clueActivity.ts index 1a24d9f269..58f8272e40 100644 --- a/src/tasks/minions/clueActivity.ts +++ b/src/tasks/minions/clueActivity.ts @@ -7,7 +7,7 @@ import { handleTripFinish } from '../../lib/util/handleTripFinish'; export const clueTask: MinionTask = { type: 'ClueCompletion', async run(data: ClueActivityTaskOptions) { - const { clueID, userID, channelID, quantity, implingClues } = data; + const { ci: clueID, userID, channelID, q: quantity, implingClues } = data; const clueTier = ClueTiers.find(mon => mon.id === clueID)!; const user = await mUserFetch(userID); diff --git a/src/tasks/minions/collectingActivity.ts b/src/tasks/minions/collectingActivity.ts index b4b7d65865..8f9f1557f1 100644 --- a/src/tasks/minions/collectingActivity.ts +++ b/src/tasks/minions/collectingActivity.ts @@ -5,7 +5,7 @@ import { MorytaniaDiary, userhasDiaryTier } from '../../lib/diaries'; import type { CollectingOptions } from '../../lib/types/minions'; import { handleTripFinish } from '../../lib/util/handleTripFinish'; import { updateBankSetting } from '../../lib/util/updateBankSetting'; -import { collectables } from '../../mahoji/lib/abstracted_commands/collectCommand'; +import { collectables } from '../../mahoji/lib/collectables'; export const collectingTask: MinionTask = { type: 'Collecting', diff --git a/src/tasks/minions/colosseumActivity.ts b/src/tasks/minions/colosseumActivity.ts index 305b05b6d5..bc0da01e0b 100644 --- a/src/tasks/minions/colosseumActivity.ts +++ b/src/tasks/minions/colosseumActivity.ts @@ -4,8 +4,10 @@ import type { ItemBank } from 'oldschooljs/dist/meta/types'; import { resolveItems } from 'oldschooljs/dist/util/util'; import { ColosseumWaveBank, colosseumWaves } from '../../lib/colosseum'; +import { refundChargeBank } from '../../lib/degradeableItems'; import { trackLoot } from '../../lib/lootTrack'; import { incrementMinigameScore } from '../../lib/settings/minigames'; +import { ChargeBank } from '../../lib/structures/Bank'; import type { ColoTaskOptions } from '../../lib/types/minions'; import { handleTripFinish } from '../../lib/util/handleTripFinish'; import { makeBankImage } from '../../lib/util/makeBankImage'; @@ -17,7 +19,16 @@ const sunfireItems = resolveItems(['Sunfire fanatic helm', 'Sunfire fanatic cuir export const colosseumTask: MinionTask = { type: 'Colosseum', async run(data: ColoTaskOptions) { - const { channelID, userID, loot: possibleLoot, diedAt, maxGlory } = data; + const { + channelID, + userID, + loot: possibleLoot, + diedAt, + maxGlory, + scytheCharges, + venatorBowCharges, + bloodFuryCharges + } = data; const user = await mUserFetch(userID); const newKCs = new ColosseumWaveBank(); @@ -33,20 +44,41 @@ export const colosseumTask: MinionTask = { .map(([kc, amount]) => `Wave ${kc}: ${amount} KC`) .join(', ')}`; + let scytheRefund = 0; + let venatorBowRefund = 0; + let bloodFuryRefund = 0; + const newWaveKcStr = !diedAt || diedAt > 1 ? `New wave KCs: ${newKCsStr}.` : 'No new KCs.'; if (diedAt) { const wave = colosseumWaves.find(i => i.waveNumber === diedAt)!; - return handleTripFinish( - user, - channelID, - `${user}, you died on wave ${diedAt} to ${randArrItem([ - ...(wave?.reinforcements ?? []), - ...wave.enemies - ])}, and received no loot. ${newWaveKcStr}`, - undefined, - data, - null - ); + + let str = `${user}, you died on wave ${diedAt} to ${randArrItem([ + ...(wave?.reinforcements ?? []), + ...wave.enemies + ])}, and received no loot. ${newWaveKcStr}`; + + // Calculate refund for unused charges + const completionPercentage = (diedAt - 1) / 12; + if (scytheCharges > 0) scytheRefund = Math.ceil(scytheCharges * (1 - completionPercentage)); + if (venatorBowCharges > 0) venatorBowRefund = Math.ceil(venatorBowCharges * (1 - completionPercentage)); + if (bloodFuryCharges > 0) bloodFuryRefund = Math.ceil(bloodFuryCharges * (1 - completionPercentage)); + + const chargeBank = new ChargeBank(); + if (scytheRefund > 0) chargeBank.add('scythe_of_vitur_charges', scytheRefund); + if (venatorBowRefund > 0) chargeBank.add('venator_bow_charges', venatorBowRefund); + if (bloodFuryRefund > 0) chargeBank.add('blood_fury_charges', bloodFuryRefund); + + if (chargeBank.length() > 0) { + const refundResults = await refundChargeBank(user, chargeBank); + + const refundMessages = refundResults + .map(result => `${result.userMessage} Total charges: ${result.totalCharges}.`) + .join('\n'); + + str += `\n${refundMessages}`; + } + + return handleTripFinish(user, channelID, str, undefined, data, null); } await incrementMinigameScore(user.id, 'colosseum'); @@ -67,7 +99,7 @@ export const colosseumTask: MinionTask = { const { previousCL } = await user.addItemsToBank({ items: loot, collectionLog: true }); await updateBankSetting('colo_loot', loot); - await userStatsBankUpdate(user.id, 'colo_loot', loot); + await userStatsBankUpdate(user, 'colo_loot', loot); await trackLoot({ totalLoot: loot, id: 'colo', diff --git a/src/tasks/minions/farmingActivity.ts b/src/tasks/minions/farmingActivity.ts index c333b47179..f3538c8bb5 100644 --- a/src/tasks/minions/farmingActivity.ts +++ b/src/tasks/minions/farmingActivity.ts @@ -228,7 +228,7 @@ export const farmingTask: MinionTask = { const uncleanedHerbLoot = new Bank().add(plantToHarvest.outputCrop, cropYield); await user.addItemsToCollectionLog(uncleanedHerbLoot); const cleanedHerbLoot = new Bank().add(plantToHarvest.cleanHerbCrop, cropYield); - await userStatsBankUpdate(user.id, 'herbs_cleaned_while_farming_bank', cleanedHerbLoot); + await userStatsBankUpdate(user, 'herbs_cleaned_while_farming_bank', cleanedHerbLoot); } if (plantToHarvest.name === 'Limpwurt') { @@ -340,8 +340,8 @@ export const farmingTask: MinionTask = { farmingLevel: currentFarmingLevel }); const fakeMonsterTaskOptions: MonsterActivityTaskOptions = { - monsterID: Monsters.Hespori.id, - quantity: patchType.lastQuantity, + mi: Monsters.Hespori.id, + q: patchType.lastQuantity, type: 'MonsterKilling', userID: user.id, duration: data.duration, @@ -437,13 +437,13 @@ export const farmingTask: MinionTask = { infoStr.push(`\n${user.minionName} tells you to come back after your plants have finished growing!`); } - updateBankSetting('farming_loot_bank', loot); + await updateBankSetting('farming_loot_bank', loot); await transactItems({ userID: user.id, collectionLog: true, itemsToAdd: loot }); - await userStatsBankUpdate(user.id, 'farming_harvest_loot_bank', loot); + await userStatsBankUpdate(user, 'farming_harvest_loot_bank', loot); if (pid) { await prisma.farmedCrop.update({ where: { diff --git a/src/tasks/minions/groupMonsterActivity.ts b/src/tasks/minions/groupMonsterActivity.ts index 9271e90808..a87a51dde9 100644 --- a/src/tasks/minions/groupMonsterActivity.ts +++ b/src/tasks/minions/groupMonsterActivity.ts @@ -12,7 +12,7 @@ import { handleTripFinish } from '../../lib/util/handleTripFinish'; export const groupoMonsterTask: MinionTask = { type: 'GroupMonsterKilling', async run(data: GroupMonsterActivityTaskOptions) { - const { monsterID, channelID, quantity, users, leader, duration } = data; + const { mi: monsterID, channelID, q: quantity, users, leader, duration } = data; const monster = killableMonsters.find(mon => mon.id === monsterID)!; const teamsLoot: { [key: string]: Bank } = {}; diff --git a/src/tasks/minions/mageArena2Activity.ts b/src/tasks/minions/mageArena2Activity.ts index 4a348b5e13..7cc092ecf6 100644 --- a/src/tasks/minions/mageArena2Activity.ts +++ b/src/tasks/minions/mageArena2Activity.ts @@ -14,12 +14,12 @@ export const mageArenaTwoTask: MinionTask = { let loot: Bank | undefined = undefined; if (percentChance(70)) { const deathReason = randArrItem([ - 'Died to Porazdir.', - 'Killed by Derwen.', - 'Killed by Justiciar Zachariah.', - "PK'd by a clan.", - 'Killed by Chaos Elemental.', - 'Killed by a PKer.' + 'Died to Porazdir', + 'Killed by Derwen', + 'Killed by Justiciar Zachariah', + "PK'd by a clan", + 'Killed by Chaos Elemental', + 'Killed by a PKer' ]); str = `${user}, ${user.minionName} failed to complete the Mage Arena II: ${deathReason}. Try again.`; } else { diff --git a/src/tasks/minions/minigames/fightCavesActivity.ts b/src/tasks/minions/minigames/fightCavesActivity.ts index f4ced983d9..a5cb2e71c2 100644 --- a/src/tasks/minions/minigames/fightCavesActivity.ts +++ b/src/tasks/minions/minigames/fightCavesActivity.ts @@ -35,7 +35,7 @@ export const fightCavesTask: MinionTask = { { fight_caves_attempts: true } ); - const attemptsStr = `You have tried Fight caves ${newFightCavesAttempts}x times.`; + const attemptsStr = `You have tried Fight caves ${newFightCavesAttempts}x times`; // Add slayer const usersTask = await getUsersCurrentSlayerInfo(user.id); @@ -81,7 +81,7 @@ export const fightCavesTask: MinionTask = { preJadDeathTime )} into your attempt.${slayerMsg} The following supplies were refunded back into your bank: ${itemLootBank}.`, await chatHeadImage({ - content: `You die before you even reach TzTok-Jad...atleast you tried, I give you ${tokkulReward}x Tokkul. ${attemptsStr}`, + content: `You die before you even reach TzTok-Jad... At least you tried, I give you ${tokkulReward}x Tokkul. ${attemptsStr}.`, head: 'mejJal' }), data, @@ -117,7 +117,7 @@ export const fightCavesTask: MinionTask = { channelID, `${user} ${msg}`, await chatHeadImage({ - content: `TzTok-Jad stomp you to death...nice try though JalYt, for your effort I give you ${tokkulReward}x Tokkul. ${attemptsStr}.`, + content: `TzTok-Jad stomp you to death... Nice try though JalYt, for your effort I give you ${tokkulReward}x Tokkul. ${attemptsStr}.`, head: 'mejJal' }), data, @@ -152,8 +152,8 @@ export const fightCavesTask: MinionTask = { itemsToAdd: loot }); - const rangeXP = await user.addXP({ skillName: SkillsEnum.Ranged, amount: 47_580, duration }); - const hpXP = await user.addXP({ skillName: SkillsEnum.Hitpoints, amount: 15_860, duration }); + const rangeXP = await user.addXP({ skillName: SkillsEnum.Ranged, amount: 47_580, duration, minimal: true }); + const hpXP = await user.addXP({ skillName: SkillsEnum.Hitpoints, amount: 15_860, duration, minimal: true }); let msg = `${rangeXP}. ${hpXP}.`; if (isOnTask) { @@ -186,7 +186,13 @@ export const fightCavesTask: MinionTask = { } }); - const slayXP = await user.addXP({ skillName: SkillsEnum.Slayer, amount: slayerXP, duration }); + const slayXP = await user.addXP({ + skillName: SkillsEnum.Slayer, + amount: slayerXP, + duration, + minimal: true + }); + const xpMessage = `${msg} ${slayXP}`; msg = `Jad task completed. ${xpMessage}. \n**You've completed ${currentStreak} tasks and received ${points} points; giving you a total of ${secondNewUser.newUser.slayer_points}; return to a Slayer master.**`; diff --git a/src/tasks/minions/minigames/giantsFoundryActivity.ts b/src/tasks/minions/minigames/giantsFoundryActivity.ts index 1b9facbf8f..059bcc0974 100644 --- a/src/tasks/minions/minigames/giantsFoundryActivity.ts +++ b/src/tasks/minions/minigames/giantsFoundryActivity.ts @@ -100,7 +100,7 @@ export const giantsFoundryTask: MinionTask = { } ] }); - await userStatsBankUpdate(user.id, 'gf_loot', loot); + await userStatsBankUpdate(user, 'gf_loot', loot); handleTripFinish(user, channelID, str, undefined, data, itemsAdded); } diff --git a/src/tasks/minions/minigames/infernoActivity.ts b/src/tasks/minions/minigames/infernoActivity.ts index c5dc66f388..02693b14b7 100644 --- a/src/tasks/minions/minigames/infernoActivity.ts +++ b/src/tasks/minions/minigames/infernoActivity.ts @@ -51,33 +51,43 @@ export const infernoTask: MinionTask = { const [hasDiary] = await userhasDiaryTier(user, diariesObject.KaramjaDiary.elite); if (hasDiary) tokkul *= 2; const baseBank = new Bank().add('Tokkul', tokkul); - - let xpStr = await user.addXP({ - skillName: SkillsEnum.Ranged, - amount: calcPercentOfNum(percentMadeItThrough, 80_000), - duration, - minimal: true - }); - xpStr += await user.addXP({ - skillName: SkillsEnum.Hitpoints, - amount: calcPercentOfNum(percentMadeItThrough, 35_000), - duration, - minimal: true - }); - xpStr += await user.addXP({ - skillName: SkillsEnum.Magic, - amount: calcPercentOfNum(percentMadeItThrough, 25_000), - duration, - minimal: true - }); + const xpBonuses = []; + + xpBonuses.push( + await user.addXP({ + skillName: SkillsEnum.Ranged, + amount: calcPercentOfNum(percentMadeItThrough, 80_000), + duration, + minimal: true + }) + ); + xpBonuses.push( + await user.addXP({ + skillName: SkillsEnum.Hitpoints, + amount: calcPercentOfNum(percentMadeItThrough, 35_000), + duration, + minimal: true + }) + ); + xpBonuses.push( + await user.addXP({ + skillName: SkillsEnum.Magic, + amount: calcPercentOfNum(percentMadeItThrough, 25_000), + duration, + minimal: true + }) + ); if (isOnTask) { - xpStr += await user.addXP({ - skillName: SkillsEnum.Slayer, - amount: deathTime === null ? 125_000 : calcPercentOfNum(percentMadeItThrough, 25_000), - duration - }); + xpBonuses.push( + await user.addXP({ + skillName: SkillsEnum.Slayer, + amount: deathTime === null ? 125_000 : calcPercentOfNum(percentMadeItThrough, 25_000), + duration + }) + ); } + const xpStr = xpBonuses.join(', '); if (!deathTime) { await incrementMinigameScore(userID, 'inferno', 1); } @@ -156,7 +166,7 @@ export const infernoTask: MinionTask = { if (diedPreZuk) { text += `You died ${formatDuration(deathTime!)} into your attempt, before you reached Zuk.`; - chatText = `You die before you even reach TzKal-Zuk...atleast you tried, I give you ${baseBank.amount( + chatText = `You die before you even reach TzKal-Zuk... At least you tried, I give you ${baseBank.amount( 'Tokkul' )}x Tokkul.`; } else if (diedZuk) { diff --git a/src/tasks/minions/minigames/puroPuroActivity.ts b/src/tasks/minions/minigames/puroPuroActivity.ts index 7ecf566691..7dd9701703 100644 --- a/src/tasks/minions/minigames/puroPuroActivity.ts +++ b/src/tasks/minions/minigames/puroPuroActivity.ts @@ -164,7 +164,7 @@ export const puroPuroTask: MinionTask = { itemsToRemove: itemCost }); - userStatsBankUpdate(user.id, 'puropuro_implings_bank', bank); + userStatsBankUpdate(user, 'puropuro_implings_bank', bank); handleTripFinish(user, channelID, str, undefined, data, bank); } diff --git a/src/tasks/minions/minigames/toaActivity.ts b/src/tasks/minions/minigames/toaActivity.ts index ba0fb0ab86..86a2b9880d 100644 --- a/src/tasks/minions/minigames/toaActivity.ts +++ b/src/tasks/minions/minigames/toaActivity.ts @@ -11,14 +11,9 @@ import { toaCL } from '../../../lib/data/CollectionsExport'; import { trackLoot } from '../../../lib/lootTrack'; import { getMinigameScore, incrementMinigameScore } from '../../../lib/settings/settings'; import { TeamLoot } from '../../../lib/simulation/TeamLoot'; -import { - calcTOALoot, - calculateXPFromRaid, - normalizeTOAUsers, - toaOrnamentKits, - toaPetTransmogItems -} from '../../../lib/simulation/toa'; +import { calcTOALoot, calculateXPFromRaid, toaOrnamentKits, toaPetTransmogItems } from '../../../lib/simulation/toa'; import type { TOAOptions } from '../../../lib/types/minions'; +import { normalizeTOAUsers } from '../../../lib/util'; import { handleTripFinish } from '../../../lib/util/handleTripFinish'; import { assert } from '../../../lib/util/logError'; import { updateBankSetting } from '../../../lib/util/updateBankSetting'; @@ -164,21 +159,16 @@ export const toaTask: MinionTask = { itemsAddedTeamLoot.add(userID, itemsAdded); - userStatsUpdate( - user.id, - u => { - return { - toa_raid_levels_bank: new Bank() - .add(u.toa_raid_levels_bank as ItemBank) - .add(raidLevel, quantity).bank, - total_toa_duration_minutes: { - increment: Math.floor(duration / Time.Minute) - }, - toa_loot: new Bank(u.toa_loot as ItemBank).add(totalLoot.get(userID)).bank - }; + const currentStats = await user.fetchStats({ toa_raid_levels_bank: true, toa_loot: true }); + await userStatsUpdate(user.id, { + toa_raid_levels_bank: new Bank() + .add(currentStats.toa_raid_levels_bank as ItemBank) + .add(raidLevel, quantity).bank, + total_toa_duration_minutes: { + increment: Math.floor(duration / Time.Minute) }, - {} - ); + toa_loot: new Bank(currentStats.toa_loot as ItemBank).add(totalLoot.get(userID)).bank + }); const items = itemsAdded.items(); diff --git a/src/tasks/minions/minigames/tobActivity.ts b/src/tasks/minions/minigames/tobActivity.ts index f5b997c8f2..a9755bb34a 100644 --- a/src/tasks/minions/minigames/tobActivity.ts +++ b/src/tasks/minions/minigames/tobActivity.ts @@ -115,7 +115,7 @@ export const tobTask: MinionTask = { // Track loot for T3+ patrons await Promise.all( allUsers.map(user => { - return userStatsBankUpdate(user.id, 'tob_loot', new Bank(result.loot[user.id])); + return userStatsBankUpdate(user, 'tob_loot', new Bank(result.loot[user.id])); }) ); diff --git a/src/tasks/minions/monsterActivity.ts b/src/tasks/minions/monsterActivity.ts index b4205b5e1b..7af6832f7a 100644 --- a/src/tasks/minions/monsterActivity.ts +++ b/src/tasks/minions/monsterActivity.ts @@ -25,14 +25,14 @@ export const monsterTask: MinionTask = { type: 'MonsterKilling', async run(data: MonsterActivityTaskOptions) { let { - monsterID, + mi: monsterID, userID, channelID, - quantity, + q: quantity, duration, usingCannon, cannonMulti, - burstOrBarrage, + bob: burstOrBarrage, died, pkEncounters, hasWildySupplies, diff --git a/src/tasks/minions/woodcuttingActivity.ts b/src/tasks/minions/woodcuttingActivity.ts index e31dbf1216..450923254f 100644 --- a/src/tasks/minions/woodcuttingActivity.ts +++ b/src/tasks/minions/woodcuttingActivity.ts @@ -127,7 +127,7 @@ async function handleForestry({ user, duration, loot }: { user: MUser; duration: for (const [event, count] of objectEntries(eventCounts)) { if (event && count && count > 0) { totalEvents += count; - await userStatsBankUpdate(user.id, 'forestry_event_completions_bank', new Bank().add(Number(event), count)); + await userStatsBankUpdate(user, 'forestry_event_completions_bank', new Bank().add(Number(event), count)); } } diff --git a/tests/integration/MUser.test.ts b/tests/integration/MUser.test.ts index 3f85e50690..476959cfbc 100644 --- a/tests/integration/MUser.test.ts +++ b/tests/integration/MUser.test.ts @@ -161,8 +161,8 @@ describe('MUser', () => { group_activity: false, data: { userID: user.id, - clueID: tier.id, - quantity: randInt(1, 10) + ci: tier.id, + q: randInt(1, 10) } }); } diff --git a/tests/integration/clArrayUpdate.test.ts b/tests/integration/clArrayUpdate.test.ts new file mode 100644 index 0000000000..15f0917f16 --- /dev/null +++ b/tests/integration/clArrayUpdate.test.ts @@ -0,0 +1,37 @@ +import { expect, test } from 'vitest'; + +import { Bank } from 'oldschooljs'; +import { itemID } from 'oldschooljs/dist/util'; +import { roboChimpSyncData } from '../../src/lib/roboChimp'; +import { createTestUser } from './util'; + +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(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(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(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/commands/dice.test.ts b/tests/integration/commands/dice.test.ts index ab20bbd5ee..a903a0186d 100644 --- a/tests/integration/commands/dice.test.ts +++ b/tests/integration/commands/dice.test.ts @@ -27,7 +27,7 @@ describe('Dice Command', async () => { await user.gpMatch(100_000_000); const unmock = mockMathRandom(0.1); const result = await user.runCommand(gambleCommand, { dice: { amount: '100m' } }); - expect(result).toMatchObject(` <@${user.id}> rolled **11** on the percentile dice, and you lost -100m GP.`); + expect(result).toMatchObject('Unknown rolled **11** on the percentile dice, and you lost -100m GP.'); await user.gpMatch(0); await user.statsMatch('dice_losses', 1); await user.statsMatch('gp_dice', BigInt(-100_000_000)); @@ -39,7 +39,7 @@ describe('Dice Command', async () => { const unmock = mockMathRandom(0.9); await user.gpMatch(100_000_000); const result = await user.runCommand(gambleCommand, { dice: { amount: '100m' } }); - expect(result).toMatchObject(` <@${user.id}> rolled **91** on the percentile dice, and you won 100m GP.`); + expect(result).toMatchObject('Unknown rolled **91** on the percentile dice, and you won 100m GP.'); await user.gpMatch(200_000_000); await user.statsMatch('dice_wins', 1); await user.statsMatch('gp_dice', BigInt(100_000_000)); diff --git a/tests/integration/commands/sacrifice.test.ts b/tests/integration/commands/sacrifice.test.ts index 4590b6b394..0abd4fc483 100644 --- a/tests/integration/commands/sacrifice.test.ts +++ b/tests/integration/commands/sacrifice.test.ts @@ -22,7 +22,7 @@ describe('Sacrifice Command', async () => { test('No items provided', async () => { const result = await user.runCommand(sacrificeCommand, { items: 'aaaa' }); - expect(result).toEqual('No items were provided.\nYour current sacrificed value is: 1,590 (1.59k)'); + expect(result).toEqual('No items were provided.\nYour current sacrificed value is: 1,590 (1.6k)'); }); test('Successful', async () => { @@ -30,12 +30,12 @@ describe('Sacrifice Command', async () => { const result = await user.runCommand(sacrificeCommand, { items: '1 trout, 10 coal' }); await user.sync(); expect(result).toEqual( - 'You sacrificed 10x Coal, 1x Trout, with a value of 1,590gp (1.59k). Your total amount sacrificed is now: 3,180. ' + 'You sacrificed 10x Coal, 1x Trout, with a value of 1,590gp (1.6k). Your total amount sacrificed is now: 3,180. ' ); const stats = await user.fetchStats({ sacrificed_bank: true }); expect(user.bank.toString()).toBe(new Bank().toString()); - expect(new Bank(stats.sacrificed_bank as ItemBank).equals(new Bank().add('Coal', 20).add('Trout', 2))).toBe( - true + expect(new Bank(stats.sacrificed_bank as ItemBank).toString()).toEqual( + new Bank().add('Coal', 20).add('Trout', 2).toString() ); expect(user.user.sacrificedValue).toEqual(BigInt(3180)); const clientSettings = await mahojiClientSettingsFetch({ economyStats_sacrificedBank: true }); 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/leaderboard.test.ts b/tests/integration/leaderboard.test.ts new file mode 100644 index 0000000000..d4445932ac --- /dev/null +++ b/tests/integration/leaderboard.test.ts @@ -0,0 +1,15 @@ +import { describe, test } from 'vitest'; + +import { leaderboardCommand } from '../../src/mahoji/commands/leaderboard'; +import { createTestUser } from './util'; + +describe('Leaderboard', async () => { + test('KC Leaderboard', async () => { + const user = await createTestUser(); + await user.runCommand(leaderboardCommand, { + kc: { + monster: 'man' + } + }); + }); +}); diff --git a/tests/integration/migrateUser.test.ts b/tests/integration/migrateUser.test.ts index 167f500138..62abb33ac8 100644 --- a/tests/integration/migrateUser.test.ts +++ b/tests/integration/migrateUser.test.ts @@ -1,25 +1,26 @@ -import type { - Activity, - Bingo, - BingoParticipant, - BuyCommandTransaction, - CommandUsage, - EconomyTransaction, - FarmedCrop, - GearPreset, - Giveaway, - HistoricalData, - LastManStandingGame, - LootTrack, - Minigame, - PinnedTrip, - PlayerOwnedHouse, - Prisma, - ReclaimableItem, - SlayerTask, - UserStats, - XPGain, - activity_type_enum +import { + type Activity, + type Bingo, + type BingoParticipant, + type BuyCommandTransaction, + type CommandUsage, + type EconomyTransaction, + type FarmedCrop, + type GearPreset, + type Giveaway, + type HistoricalData, + type LastManStandingGame, + type LootTrack, + type Minigame, + type PinnedTrip, + type PlayerOwnedHouse, + type Prisma, + type ReclaimableItem, + type SlayerTask, + type UserStats, + type XPGain, + type activity_type_enum, + command_name_enum } from '@prisma/client'; import { Time, deepClone, randArrItem, randInt, shuffleArr, sumArr } from 'e'; import { Bank } from 'oldschooljs'; @@ -52,7 +53,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 +689,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 => { @@ -943,17 +936,18 @@ const allTableCommands: TestCommand[] = [ user_id: user.id }); + const stats = await user.fetchStats({ items_sold_bank: true }); await Promise.all([ updateClientGPTrackSetting('gp_sell', totalPrice), updateBankSetting('sold_items_bank', bankToSell), userStatsUpdate( user.id, - userStats => ({ - items_sold_bank: new Bank(userStats.items_sold_bank as ItemBank).add(bankToSell).bank, + { + items_sold_bank: new Bank(stats.items_sold_bank as ItemBank).add(bankToSell).bank, sell_gp: { increment: totalPrice } - }), + }, {} ), global.prisma!.botItemSell.createMany({ data: botItemSellData }) @@ -1057,12 +1051,17 @@ const allTableCommands: TestCommand[] = [ { name: 'Command usage', cmd: async user => { - const randCommands = ['minion', 'runecraft', 'chop', 'mine', 'buy']; + const randCommands = [ + command_name_enum.minion, + command_name_enum.runecraft, + command_name_enum.chop, + command_name_enum.mine, + command_name_enum.buy + ]; await global.prisma!.commandUsage.create({ 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/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/preStartup.test.ts b/tests/integration/preStartup.test.ts new file mode 100644 index 0000000000..66911fbd99 --- /dev/null +++ b/tests/integration/preStartup.test.ts @@ -0,0 +1,9 @@ +import { test } from 'vitest'; + +import { preStartup } from '../../src/lib/preStartup'; +import { mockClient } from './util'; + +test.skip('PreStartup', async () => { + await mockClient(); + await preStartup(); +}); diff --git a/tests/integration/redis.test.ts b/tests/integration/redis.test.ts new file mode 100644 index 0000000000..1407d57583 --- /dev/null +++ b/tests/integration/redis.test.ts @@ -0,0 +1,108 @@ +import { expect, test } from 'vitest'; + +import { TSRedis } from '@oldschoolgg/toolkit/TSRedis'; +import { sleep } from 'e'; +import { BadgesEnum, BitField, globalConfig } from '../../src/lib/constants'; +import { roboChimpCache } from '../../src/lib/perkTier'; +import { getUsersPerkTier } from '../../src/lib/perkTiers'; +import { createTestUser } from './util'; + +function makeSender() { + return new TSRedis({ mocked: !globalConfig.redisPort, port: globalConfig.redisPort }); +} + +test('Should add patron badge', async () => { + const user = await createTestUser(); + expect(user.user.badges).not.includes(BadgesEnum.Patron); + const _redis = makeSender(); + await _redis.publish({ + type: 'patron_tier_change', + discord_ids: [user.id], + new_tier: 1, + old_tier: 0, + first_time_patron: false + }); + await sleep(250); + await user.sync(); + expect(user.user.badges).includes(BadgesEnum.Patron); +}); + +test('Should remove patron badge', async () => { + const user = await createTestUser(undefined, { badges: [BadgesEnum.Patron] }); + expect(user.user.badges).includes(BadgesEnum.Patron); + const _redis = makeSender(); + await _redis.publish({ + type: 'patron_tier_change', + discord_ids: [user.id], + new_tier: 0, + old_tier: 1, + first_time_patron: false + }); + await sleep(250); + await user.sync(); + expect(user.user.badges).not.includes(BadgesEnum.Patron); +}); + +test('Should add to cache', async () => { + const users = [await createTestUser(), await createTestUser(), await createTestUser()]; + await roboChimpClient.user.createMany({ + data: users.map(u => ({ + id: BigInt(u.id), + perk_tier: 5 + })) + }); + const _redis = makeSender(); + await _redis.publish({ + type: 'patron_tier_change', + discord_ids: users.map(u => u.id), + new_tier: 5, + old_tier: 2, + first_time_patron: false + }); + await sleep(250); + for (const user of users) { + const cached = roboChimpCache.get(user.id); + expect(getUsersPerkTier(user)).toEqual(5); + expect(cached!.perk_tier).toEqual(5); + } +}); + +test('Should remove from cache', async () => { + const users = [await createTestUser(), await createTestUser(), await createTestUser()]; + await roboChimpClient.user.createMany({ + data: users.map(u => ({ + id: BigInt(u.id), + perk_tier: 0 + })) + }); + const _redis = makeSender(); + await _redis.publish({ + type: 'patron_tier_change', + discord_ids: users.map(u => u.id), + new_tier: 0, + old_tier: 5, + first_time_patron: false + }); + await sleep(250); + for (const user of users) { + expect(getUsersPerkTier(user)).toEqual(0); + const cached = roboChimpCache.get(user.id); + expect(cached).toEqual(undefined); + } +}); + +test('Should recognize special bitfields', async () => { + const users = [ + await createTestUser(undefined, { bitfield: [BitField.HasPermanentTierOne] }), + await createTestUser(undefined, { bitfield: [BitField.BothBotsMaxedFreeTierOnePerks] }) + ]; + for (const user of users) { + expect(getUsersPerkTier(user)).toEqual(2); + } +}); + +test('Should sdffsddfss', async () => { + const user = await createTestUser(); + roboChimpCache.set(user.id, { perk_tier: 5 } as any); + expect(getUsersPerkTier(user)).toEqual(5); +}); diff --git a/tests/integration/rolesTask.test.ts b/tests/integration/rolesTask.test.ts index df57e5e208..dcd69d0dcc 100644 --- a/tests/integration/rolesTask.test.ts +++ b/tests/integration/rolesTask.test.ts @@ -11,10 +11,10 @@ import { createTestUser, mockedId, unMockedCyptoRand } from './util'; describe.skip('Roles Task', async () => { test('Should not throw', async () => { const user = await createTestUser(); - await userStatsBankUpdate(user.id, 'sacrificed_bank', new Bank().add('Coal', 10_000)); + await userStatsBankUpdate(user, 'sacrificed_bank', new Bank().add('Coal', 10_000)); const ironUser = await createTestUser(); await ironUser.update({ minion_ironman: true, sacrificedValue: 1_000_000 }); - await userStatsBankUpdate(ironUser.id, 'sacrificed_bank', new Bank().add('Coal', 10_000)); + await userStatsBankUpdate(ironUser, 'sacrificed_bank', new Bank().add('Coal', 10_000)); // Create minigame scores: const minigames = Minigames.map(game => game.column).filter(i => i !== 'tithe_farm'); @@ -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/tests/integration/tradeTransaction.test.ts b/tests/integration/tradeTransaction.test.ts index 0c47819447..77a3a5f743 100644 --- a/tests/integration/tradeTransaction.test.ts +++ b/tests/integration/tradeTransaction.test.ts @@ -120,7 +120,7 @@ describe('Transactionalized Trade Test', async () => { .freeze(); const magna = await createUserWithBank(magnaStartingBank); - const uCyr = await mUserFetch(cyr); + const uCyr = await mUserFetch(cyr, { username: 'Cyr' }); const uMagna = await mUserFetch(magna); expect(uCyr.GP).toBe(1_000_000); @@ -131,7 +131,7 @@ describe('Transactionalized Trade Test', async () => { const result = await tradePlayerItems(uCyr, uMagna, tradeFromCyr, tradeFromMagna); - const expectedResult = { success: false, message: `<@${cyr}> doesn't own all items.` }; + const expectedResult = { success: false, message: `Cyr doesn't own all items.` }; expect(result).toMatchObject(expectedResult); expect(uCyr.bankWithGP.toString()).toEqual(cyrStartingBank.toString()); @@ -155,14 +155,14 @@ describe('Transactionalized Trade Test', async () => { const magna = await createUserWithBank(magnaStartingBank); const uCyr = await mUserFetch(cyr); - const uMagna = await mUserFetch(magna); + const uMagna = await mUserFetch(magna, { username: 'magna' }); const tradeFromCyr = new Bank().add('Coins', 1_000_000).add('Twisted bow', 1).freeze(); const tradeFromMagna = new Bank().add('Coins', 2_000_000).add('Feather', 5000).add('Cannonball', 2000).freeze(); const result = await tradePlayerItems(uCyr, uMagna, tradeFromCyr, tradeFromMagna); - const expectedResult = { success: false, message: `<@${magna}> doesn't own all items.` }; + const expectedResult = { success: false, message: `magna doesn't own all items.` }; expect(result).toMatchObject(expectedResult); expect(uCyr.bankWithGP.equals(cyrStartingBank)).toBe(true); diff --git a/tests/integration/tripEffects.test.ts b/tests/integration/tripEffects.test.ts new file mode 100644 index 0000000000..ddf5c2c878 --- /dev/null +++ b/tests/integration/tripEffects.test.ts @@ -0,0 +1,30 @@ +import { activity_type_enum } from '@prisma/client'; +import { Time } from 'e'; +import { Monsters } from 'oldschooljs'; +import { expect, test } from 'vitest'; + +import { minionKCommand } from '../../src/mahoji/commands/k'; +import { createTestUser, mockClient, mockMathRandom } from './util'; + +test('Random Events', async () => { + const unmock = mockMathRandom(0.03); + const client = await mockClient(); + const user = await createTestUser(); + await user.runCommand(minionKCommand, { name: 'man' }); + await prisma.activity.updateMany({ + where: { + user_id: BigInt(user.id), + type: activity_type_enum.MonsterKilling + }, + data: { + duration: Time.Hour + } + }); + await client.processActivities(); + expect(await user.getKC(Monsters.Man.id)).toBeGreaterThan(1); + const userStats = await user.fetchStats({ random_event_completions_bank: true }); + await user.sync(); + expect(userStats.random_event_completions_bank).toEqual({ 1: 1 }); + expect(user.bank.amount("Beekeeper's hat")).toEqual(1); + unmock(); +}); diff --git a/tests/integration/userStats.test.ts b/tests/integration/userStats.test.ts index f6815ebaba..f0a74eef4c 100644 --- a/tests/integration/userStats.test.ts +++ b/tests/integration/userStats.test.ts @@ -23,11 +23,11 @@ describe('User Stats', async () => { expect(result).toEqual({ user_id: BigInt(userID) }); const result2 = await userStatsUpdate( userID, - () => ({ + { ash_sanctifier_prayer_xp: { increment: 100 } - }), + }, {} ); expect(result2).toEqual({ user_id: BigInt(userID) }); diff --git a/tests/unit/getUsersPerkTier.test.ts b/tests/unit/getUsersPerkTier.test.ts deleted file mode 100644 index 220df522cc..0000000000 --- a/tests/unit/getUsersPerkTier.test.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Time } from 'e'; -import { describe, expect, test } from 'vitest'; - -import { MUserClass } from '../../src/lib/MUser'; -import { BitField, PerkTier } from '../../src/lib/constants'; -import { getUsersPerkTier } from '../../src/lib/perkTiers'; -import { mockMUser } from './utils'; - -describe('getUsersPerkTier', () => { - test('general', () => { - expect(getUsersPerkTier([])).toEqual(0); - expect(getUsersPerkTier(mockMUser())).toEqual(0); - expect(getUsersPerkTier(mockMUser({ bitfield: [BitField.IsPatronTier3] }))).toEqual(PerkTier.Four); - expect(getUsersPerkTier(mockMUser({ bitfield: [BitField.isModerator] }))).toEqual(PerkTier.Four); - }); - test('balance', () => { - const user = mockMUser({ premium_balance_expiry_date: Date.now() + Time.Day, premium_balance_tier: 3 }); - expect(user instanceof MUserClass).toEqual(true); - expect(user.user.premium_balance_tier !== null).toEqual(true); - expect(user.perkTier()).toEqual(PerkTier.Four); - }); -}); 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/itemSwitches.test.ts b/tests/unit/itemSwitches.test.ts new file mode 100644 index 0000000000..39882024bb --- /dev/null +++ b/tests/unit/itemSwitches.test.ts @@ -0,0 +1,25 @@ +import { writeFileSync } from 'node:fs'; +import { EquipmentSlot } from 'oldschooljs/dist/meta/types'; +import { getItemOrThrow } from 'oldschooljs/dist/util'; +import { expect, test } from 'vitest'; + +import { BOT_TYPE } from '../../src/lib/constants'; +import { itemDataSwitches } from '../../src/lib/data/itemAliases'; +import { itemNameFromID } from '../../src/lib/util/smallUtils'; + +test('Item Switches', () => { + writeFileSync( + `tests/unit/snapshots/itemSwitches.${BOT_TYPE}.json`, + `${JSON.stringify( + itemDataSwitches.map(a => ({ + from: `${itemNameFromID(a.from)} [${a.from}]`, + to: `${itemNameFromID(a.to)} [${a.to}]` + })), + null, + ' ' + )}\n` + ); + expect(getItemOrThrow('Ultor ring').equipment?.melee_strength).toBe(12); + expect(getItemOrThrow('Ultor ring').id).toBe(25485); + expect(getItemOrThrow('Ultor ring').equipment?.slot).toBe(EquipmentSlot.Ring); +}); 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 f4d9e0aa2f..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..8a486bc15e 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) @@ -37,7 +37,7 @@ Elite Treasure Trails (59) Farming (141) Fishing Trawler (4) Forestry (23) -Fortis Colosseum (8) +Fortis Colosseum (9) Fossil Island Notes (10) General Graardor (8) Giant Mole (3) diff --git a/tests/unit/snapshots/cox.OSB.png b/tests/unit/snapshots/cox.OSB.png index fa4cdc96e9..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/itemSwitches.OSB.json b/tests/unit/snapshots/itemSwitches.OSB.json new file mode 100644 index 0000000000..5ea6ab447e --- /dev/null +++ b/tests/unit/snapshots/itemSwitches.OSB.json @@ -0,0 +1,18 @@ +[ + { + "from": "Bellator ring [25488]", + "to": "Bellator ring [28316]" + }, + { + "from": "Magus ring [25486]", + "to": "Magus ring [28313]" + }, + { + "from": "Venator ring [25487]", + "to": "Venator ring [28310]" + }, + { + "from": "Ultor ring [25485]", + "to": "Ultor ring [28307]" + } +] diff --git a/tests/unit/snapshots/toa.OSB.png b/tests/unit/snapshots/toa.OSB.png index 5518422d71..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..47d2a04c10 100644 --- a/tests/unit/utils.ts +++ b/tests/unit/utils.ts @@ -36,8 +36,6 @@ interface MockUserArgs { skills_prayer?: number; skills_fishing?: number; GP?: number; - premium_balance_tier?: number; - premium_balance_expiry_date?: number; bitfield?: BitField[]; id?: string; } @@ -81,15 +79,13 @@ const mockUser = (overrides?: MockUserArgs): User => { skills_slayer: 0, skills_hitpoints: overrides?.skills_hitpoints ?? convertLVLtoXP(10), GP: overrides?.GP ?? 0, - premium_balance_tier: overrides?.premium_balance_tier, - premium_balance_expiry_date: overrides?.premium_balance_expiry_date, - ironman_alts: [], bitfield: overrides?.bitfield ?? [], username: 'Magnaboy', QP: overrides?.QP ?? 0, sacrificedValue: 0, id: overrides?.id ?? '', - monsterScores: {} + monsterScores: {}, + badges: [] } as unknown as User; return r; diff --git a/types.ts b/types.ts new file mode 100644 index 0000000000..a6b11fee6f --- /dev/null +++ b/types.ts @@ -0,0 +1,362 @@ +import type { Bank } from 'oldschooljs'; +import type { Item } from 'oldschooljs/dist/meta/types'; +import type LootTable from 'oldschooljs/dist/structures/LootTable'; + +import type { Emoji } from '../constants'; +import type { SlayerTaskUnlocksEnum } from '../slayer/slayerUnlocks'; +import type { ItemBank } from '../types'; +import type { FarmingPatchName } from '../util/farmingHelpers'; + +export enum SkillsEnum { + Agility = 'agility', + Cooking = 'cooking', + Fishing = 'fishing', + Mining = 'mining', + Smithing = 'smithing', + Woodcutting = 'woodcutting', + Firemaking = 'firemaking', + Runecraft = 'runecraft', + Crafting = 'crafting', + Prayer = 'prayer', + Fletching = 'fletching', + Farming = 'farming', + Herblore = 'herblore', + Thieving = 'thieving', + Hunter = 'hunter', + Construction = 'construction', + Magic = 'magic', + Attack = 'attack', + Strength = 'strength', + Defence = 'defence', + Ranged = 'ranged', + Hitpoints = 'hitpoints', + Slayer = 'slayer' +} + +export const SkillsArray = [ + 'agility', + 'cooking', + 'fishing', + 'mining', + 'smithing', + 'woodcutting', + 'firemaking', + 'runecraft', + 'crafting', + 'prayer', + 'fletching', + 'farming', + 'herblore', + 'thieving', + 'hunter', + 'construction', + 'magic', + 'attack', + 'strength', + 'defence', + 'ranged', + 'hitpoints', + 'slayer' +] as const; + +export type SkillNameType = (typeof SkillsArray)[number]; +for (const skill of SkillsArray) { + const matching = Object.keys(SkillsEnum).find(key => key.toLowerCase() === skill); + if (!matching) throw new Error(`Missing skill enum for ${skill}`); +} +if (SkillsArray.length !== Object.keys(SkillsEnum).length) { + throw new Error('Not all skills have been added to the SkillsArray.'); +} + +export interface Ore { + level: number; + xp: number; + id: number; + name: string; + respawnTime: number; + bankingTime: number; + slope: number; + intercept: number; + petChance?: number; + minerals?: number; + clueScrollChance?: number; + aliases?: string[]; +} + +export interface Log { + level: number; + xp: number; + id: number; + lootTable?: LootTable; + name: string; + leaf?: number; + aliases?: string[]; + findNewTreeTime: number; + bankingTime: number; + slope: number; + intercept: number; + depletionChance: number; + wcGuild?: boolean; + petChance?: number; + qpRequired: number; + clueScrollChance?: number; + clueNestsOnly?: boolean; +} + +export interface Burnable { + level: number; + xp: number; + name: string; + inputLogs: number; +} + +export interface Fish { + name: string; + alias?: string[]; + level: number; + xp: number; + id: number; + chance1Lvl1?: number; + chance1Lvl99?: number; + level2?: number; + xp2?: number; + id2?: number; + chance2Lvl1?: number; + chance2Lvl99?: number; + level3?: number; + xp3?: number; + id3?: number; + chance3Lvl1?: number; + chance3Lvl99?: number; + + petChance?: number; + clueScrollChance?: number; + lostTicks?: number; + bankingTime?: number; + ticksPerRoll?: number; + + bait?: number; + qpRequired?: number; + bigFish?: number; + bigFishRate?: number; + + timePerFish?: number; +} + +export interface Course { + id: number; + name: string; + level: number; + xp: number | ((agilityLevel: number) => number); + marksPer60?: number; + lapTime: number; + petChance: number; + aliases: string[]; + qpRequired?: number; +} + +export interface Cookable { + level: number; + xp: number; + id: number; + name: string; + inputCookables: ItemBank; + stopBurnAt: number; + stopBurnAtCG?: number; + // Burn level with hosidius/diary: [ noGauntletsHosidius, noGauntletsElite, gauntletsHosidius, gauntletsElite ] + burnKourendBonus?: number[]; + burntCookable: number; + alias?: string[]; +} + +export interface Bar { + level: number; + xp: number; + id: number; + name: string; + inputOres: Bank; + /** + * Chance that the ore will fail to smelt (i.e iron), out of 100 + */ + chanceOfFail: number; + timeToUse: number; +} + +export interface BlastableBar { + level: number; + xp: number; + id: number; + name: string; + inputOres: Bank; + timeToUse: number; +} + +export interface SmithedItem { + level: number; + xp: number; + id: number; + name: string; + inputBars: ItemBank; + timeToUse: number; + outputMultiple: number; + qpRequired?: number; +} + +export interface Craftable { + name: string; + alias?: string[]; + id: number; + level: number; + xp: number; + inputItems: Bank; + tickRate: number; + crushChance?: number[]; + bankChest?: boolean; + outputMultiple?: number; + qpRequired?: number; + wcLvl?: number; +} + +export interface Fletchable { + name: string; + id: number; + level: number; + xp: number; + inputItems: Bank; + tickRate: number; + outputMultiple?: number; + requiredSlayerUnlocks?: SlayerTaskUnlocksEnum[]; + craftingXp?: number; +} + +export interface Mixable { + item: Item; + aliases: string[]; + level: number; + xp: number; + inputItems: Bank; + tickRate: number; + bankTimePerPotion: number; + outputMultiple?: number; + zahur?: boolean; + wesley?: boolean; + qpRequired?: number; +} + +export interface CutLeapingFish { + item: Item; + aliases: string[]; + tickRate: number; +} + +export interface Bone { + level: number; + xp: number; + name: string; + inputId: number; +} + +export interface Ash { + level: number; + xp: number; + name: string; + inputId: number; +} + +export type LevelRequirements = Partial<{ + [key in SkillsEnum]: number; +}>; + +export interface Skill { + aliases: string[]; + id: SkillsEnum; + emoji: Emoji; + name: string; +} + +export interface Plankable { + name: string; + inputItem: number; + outputItem: number; + gpCost: number; +} + +export interface Plant { + id: number; + level: number; + plantXp: number; + checkXp: number; + harvestXp: number; + name: string; + inputItems: Bank; + aliases: string[]; + outputCrop?: number; + cleanHerbCrop?: number; + herbXp?: number; + herbLvl?: number; + outputLogs?: number; + outputRoots?: number; + treeWoodcuttingLevel?: number; + fixedOutputAmount?: number; + variableYield?: boolean; + variableOutputAmount?: [string | null, number, number][]; + woodcuttingXp?: number; + needsChopForHarvest: boolean; + fixedOutput: boolean; + givesLogs: boolean; + givesCrops: boolean; + petChance: number; + seedType: FarmingPatchName; + growthTime: number; + numOfStages: number; + chance1: number; + chance99: number; + chanceOfDeath: number; + protectionPayment?: Bank; + defaultNumOfPatches: number; + canPayFarmer: boolean; + canCompostPatch: boolean; + canCompostandPay: boolean; + additionalPatchesByQP: number[][]; + additionalPatchesByFarmLvl: number[][]; + additionalPatchesByFarmGuildAndLvl: number[][]; + timePerPatchTravel: number; + timePerHarvest: number; +} + +export enum HunterTechniqueEnum { + AerialFishing = 'aerial fishing', + DriftNet = 'drift net fishing', + BirdSnaring = 'bird snaring', + BoxTrapping = 'box trapping', + ButterflyNetting = 'butterfly netting', + DeadfallTrapping = 'deadfall trapping', + Falconry = 'falconry', + MagicBoxTrapping = 'magic box trapping', + NetTrapping = 'net trapping', + PitfallTrapping = 'pitfall trapping', + RabbitSnaring = 'rabbit snaring', + Tracking = 'tracking' +} + +export interface Creature { + name: string; + id: number; + aliases: string[]; + level: number; + hunterXP: number; + fishLvl?: number; + fishingXP?: number; + itemsRequired?: Bank; + itemsConsumed?: Bank; + table: LootTable; + huntTechnique: HunterTechniqueEnum; + multiTraps?: boolean; + wildy?: boolean; + prayerLvl?: number; + herbloreLvl?: number; + catchTime: number; + qpRequired?: number; + slope: number; + intercept: number; +} diff --git a/vitest.integration.config.mts b/vitest.integration.config.mts index 46b6b535fe..91dc5c1bc9 100644 --- a/vitest.integration.config.mts +++ b/vitest.integration.config.mts @@ -7,8 +7,7 @@ export default defineConfig({ setupFiles: 'tests/integration/setup.ts', coverage: { provider: 'v8', - reporter: 'text', - include: ['src/lib/MUser.ts'] + reporter: 'text' }, testTimeout: 30_000, bail: 1, diff --git a/vitest.unit.config.mts b/vitest.unit.config.mts index a5315297c7..76d7b34b42 100644 --- a/vitest.unit.config.mts +++ b/vitest.unit.config.mts @@ -8,7 +8,7 @@ export default defineConfig({ include: ['tests/unit/**/*.test.ts'], coverage: { provider: 'v8', - reporter: 'html', + reporter: 'text', include: ['src/lib/structures/Gear.ts', 'src/lib/util/parseStringBank.ts', 'src/lib/util/equipMulti.ts'] }, setupFiles: 'tests/unit/setup.ts', diff --git a/yarn.lock b/yarn.lock index aaf16bf1b3..5dfb8015f6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5,20 +5,13 @@ __metadata: version: 8 cacheKey: 10c0 -"@ampproject/remapping@npm:^2.2.1": - version: 2.2.1 - resolution: "@ampproject/remapping@npm:2.2.1" +"@ampproject/remapping@npm:^2.3.0": + version: 2.3.0 + resolution: "@ampproject/remapping@npm:2.3.0" dependencies: - "@jridgewell/gen-mapping": "npm:^0.3.0" - "@jridgewell/trace-mapping": "npm:^0.3.9" - checksum: 10c0/92ce5915f8901d8c7cd4f4e6e2fe7b9fd335a29955b400caa52e0e5b12ca3796ada7c2f10e78c9c5b0f9c2539dff0ffea7b19850a56e1487aa083531e1e46d43 - languageName: node - linkType: hard - -"@babel/helper-string-parser@npm:^7.23.4": - version: 7.23.4 - resolution: "@babel/helper-string-parser@npm:7.23.4" - checksum: 10c0/f348d5637ad70b6b54b026d6544bd9040f78d24e7ec245a0fc42293968181f6ae9879c22d89744730d246ce8ec53588f716f102addd4df8bbc79b73ea10004ac + "@jridgewell/gen-mapping": "npm:^0.3.5" + "@jridgewell/trace-mapping": "npm:^0.3.24" + checksum: 10c0/81d63cca5443e0f0c72ae18b544cc28c7c0ec2cea46e7cb888bb0e0f411a1191d0d6b7af798d54e30777d8d1488b2ec0732aac2be342d3d7d3ffd271c6f489ed languageName: node linkType: hard @@ -29,10 +22,10 @@ __metadata: languageName: node linkType: hard -"@babel/helper-validator-identifier@npm:^7.22.20": - version: 7.22.20 - resolution: "@babel/helper-validator-identifier@npm:7.22.20" - checksum: 10c0/dcad63db345fb110e032de46c3688384b0008a42a4845180ce7cd62b1a9c0507a1bed727c4d1060ed1a03ae57b4d918570259f81724aaac1a5b776056f37504e +"@babel/helper-string-parser@npm:^7.24.8": + version: 7.24.8 + resolution: "@babel/helper-string-parser@npm:7.24.8" + checksum: 10c0/6361f72076c17fabf305e252bf6d580106429014b3ab3c1f5c4eb3e6d465536ea6b670cc0e9a637a77a9ad40454d3e41361a2909e70e305116a23d68ce094c08 languageName: node linkType: hard @@ -43,12 +36,12 @@ __metadata: languageName: node linkType: hard -"@babel/parser@npm:^7.23.6": - version: 7.23.9 - resolution: "@babel/parser@npm:7.23.9" +"@babel/parser@npm:^7.24.4": + version: 7.24.8 + resolution: "@babel/parser@npm:7.24.8" bin: parser: ./bin/babel-parser.js - checksum: 10c0/7df97386431366d4810538db4b9ec538f4377096f720c0591c7587a16f6810e62747e9fbbfa1ff99257fd4330035e4fb1b5b77c7bd3b97ce0d2e3780a6618975 + checksum: 10c0/ce69671de8fa6f649abf849be262707ac700b573b8b1ce1893c66cc6cd76aeb1294a19e8c290b0eadeb2f47d3f413a2e57a281804ffbe76bfb9fa50194cf3c52 languageName: node linkType: hard @@ -61,14 +54,14 @@ __metadata: languageName: node linkType: hard -"@babel/types@npm:^7.23.6": - version: 7.23.9 - resolution: "@babel/types@npm:7.23.9" +"@babel/types@npm:^7.24.0": + version: 7.24.9 + resolution: "@babel/types@npm:7.24.9" dependencies: - "@babel/helper-string-parser": "npm:^7.23.4" - "@babel/helper-validator-identifier": "npm:^7.22.20" + "@babel/helper-string-parser": "npm:^7.24.8" + "@babel/helper-validator-identifier": "npm:^7.24.7" to-fast-properties: "npm:^2.0.0" - checksum: 10c0/edc7bb180ce7e4d2aea10c6972fb10474341ac39ba8fdc4a27ffb328368dfdfbf40fca18e441bbe7c483774500d5c05e222cec276c242e952853dcaf4eb884f7 + checksum: 10c0/4970b3481cab39c5c3fdb7c28c834df5c7049f3c7f43baeafe121bb05270ebf0da7c65b097abf314877f213baa591109c82204f30d66cdd46c22ece4a2f32415 languageName: node linkType: hard @@ -456,30 +449,14 @@ __metadata: languageName: node linkType: hard -"@jest/schemas@npm:^29.6.3": - version: 29.6.3 - resolution: "@jest/schemas@npm:29.6.3" - dependencies: - "@sinclair/typebox": "npm:^0.27.8" - checksum: 10c0/b329e89cd5f20b9278ae1233df74016ebf7b385e0d14b9f4c1ad18d096c4c19d1e687aa113a9c976b16ec07f021ae53dea811fb8c1248a50ac34fbe009fdf6be - languageName: node - linkType: hard - -"@jridgewell/gen-mapping@npm:^0.3.0": - version: 0.3.3 - resolution: "@jridgewell/gen-mapping@npm:0.3.3" +"@jridgewell/gen-mapping@npm:^0.3.5": + version: 0.3.5 + resolution: "@jridgewell/gen-mapping@npm:0.3.5" dependencies: - "@jridgewell/set-array": "npm:^1.0.1" + "@jridgewell/set-array": "npm:^1.2.1" "@jridgewell/sourcemap-codec": "npm:^1.4.10" - "@jridgewell/trace-mapping": "npm:^0.3.9" - checksum: 10c0/376fc11cf5a967318ba3ddd9d8e91be528eab6af66810a713c49b0c3f8dc67e9949452c51c38ab1b19aa618fb5e8594da5a249977e26b1e7fea1ee5a1fcacc74 - languageName: node - linkType: hard - -"@jridgewell/resolve-uri@npm:3.1.0": - version: 3.1.0 - resolution: "@jridgewell/resolve-uri@npm:3.1.0" - checksum: 10c0/78055e2526108331126366572045355051a930f017d1904a4f753d3f4acee8d92a14854948095626f6163cffc24ea4e3efa30637417bb866b84743dec7ef6fd9 + "@jridgewell/trace-mapping": "npm:^0.3.24" + checksum: 10c0/1be4fd4a6b0f41337c4f5fdf4afc3bd19e39c3691924817108b82ffcb9c9e609c273f936932b9fba4b3a298ce2eb06d9bff4eb1cc3bd81c4f4ee1b4917e25feb languageName: node linkType: hard @@ -490,14 +467,14 @@ __metadata: languageName: node linkType: hard -"@jridgewell/set-array@npm:^1.0.1": - version: 1.1.2 - resolution: "@jridgewell/set-array@npm:1.1.2" - checksum: 10c0/bc7ab4c4c00470de4e7562ecac3c0c84f53e7ee8a711e546d67c47da7febe7c45cd67d4d84ee3c9b2c05ae8e872656cdded8a707a283d30bd54fbc65aef821ab +"@jridgewell/set-array@npm:^1.2.1": + version: 1.2.1 + resolution: "@jridgewell/set-array@npm:1.2.1" + checksum: 10c0/2a5aa7b4b5c3464c895c802d8ae3f3d2b92fcbe84ad12f8d0bfbb1f5ad006717e7577ee1fd2eac00c088abe486c7adb27976f45d2941ff6b0b92b2c3302c60f4 languageName: node linkType: hard -"@jridgewell/sourcemap-codec@npm:1.4.14, @jridgewell/sourcemap-codec@npm:^1.4.10": +"@jridgewell/sourcemap-codec@npm:^1.4.10": version: 1.4.14 resolution: "@jridgewell/sourcemap-codec@npm:1.4.14" checksum: 10c0/3fbaff1387c1338b097eeb6ff92890d7838f7de0dde259e4983763b44540bfd5ca6a1f7644dc8ad003a57f7e80670d5b96a8402f1386ba9aee074743ae9bad51 @@ -511,7 +488,7 @@ __metadata: languageName: node linkType: hard -"@jridgewell/trace-mapping@npm:^0.3.23": +"@jridgewell/trace-mapping@npm:^0.3.23, @jridgewell/trace-mapping@npm:^0.3.24": version: 0.3.25 resolution: "@jridgewell/trace-mapping@npm:0.3.25" dependencies: @@ -521,16 +498,6 @@ __metadata: languageName: node linkType: hard -"@jridgewell/trace-mapping@npm:^0.3.9": - version: 0.3.18 - resolution: "@jridgewell/trace-mapping@npm:0.3.18" - dependencies: - "@jridgewell/resolve-uri": "npm:3.1.0" - "@jridgewell/sourcemap-codec": "npm:1.4.14" - checksum: 10c0/e5045775f076022b6c7cc64a7b55742faa5442301cb3389fd0e6712fafc46a2bb13c68fa1ffaf7b8bb665a91196f050b4115885fc802094ebc06a1cf665935ac - languageName: node - linkType: hard - "@napi-rs/canvas-android-arm64@npm:0.1.53": version: 0.1.53 resolution: "@napi-rs/canvas-android-arm64@npm:0.1.53" @@ -679,14 +646,16 @@ __metadata: languageName: node linkType: hard -"@oldschoolgg/toolkit@git+https://github.com/oldschoolgg/toolkit.git#62f9f71274c6d36db6c56a2228ae3df9ab090dbd": +"@oldschoolgg/toolkit@git+https://github.com/oldschoolgg/toolkit.git#cd7c6865229ca7dc4a66b3816586f2d3f4a4fbed": version: 0.0.24 - resolution: "@oldschoolgg/toolkit@https://github.com/oldschoolgg/toolkit.git#commit=62f9f71274c6d36db6c56a2228ae3df9ab090dbd" + 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" deepmerge: "npm:4.3.1" e: "npm:0.2.33" emoji-regex: "npm:^10.2.1" + fast-deep-equal: "npm:^3.1.3" ioredis: "npm:^5.4.1" ioredis-mock: "npm:^8.9.0" math-expression-evaluator: "npm:^1.3.14" @@ -695,7 +664,7 @@ __metadata: peerDependencies: discord.js: ^14.15.3 oldschooljs: ^2.5.9 - checksum: 10c0/d23b39e8dfd6773b56ca1339a5a08c75b1173c4aa9f9d9140ee5339c131a5d111dca5f631992c973e406a4154eba82c5de348f7ad2003d3d08cf6f02ed12a08d + checksum: 10c0/42eaec1c99c671adab7b56ca7e11d37bf5a0e07d0f0da0a892cdf477a78c061ea131a43b1c578d09f1c6b02e05d1ce47db9586ad9a8de62679cc492c847c3fca languageName: node linkType: hard @@ -1034,61 +1003,61 @@ __metadata: languageName: node linkType: hard -"@prisma/client@npm:^5.16.1": - version: 5.16.1 - resolution: "@prisma/client@npm:5.16.1" +"@prisma/client@npm:^5.17.0": + version: 5.17.0 + resolution: "@prisma/client@npm:5.17.0" peerDependencies: prisma: "*" peerDependenciesMeta: prisma: optional: true - checksum: 10c0/4d1e140db1e0654564c9864d7b9e9f3dafe0794c7f86d6d7d9e25c51158f855f22d6e4c5e2ff13a15e3171b3bf9c9a8d3306bdd96f26dc8c87b3501e42a9d7ab + checksum: 10c0/cc6c5e9bfbc2f9a01fdf73e009c42298b8a9fea8c9b19db0089cad84a9ee94c3bb6f66f53f1e2f4b32b3506706bf16d23a8e3bcb4619a8bc76d0812a8382ae63 languageName: node linkType: hard -"@prisma/debug@npm:5.16.1": - version: 5.16.1 - resolution: "@prisma/debug@npm:5.16.1" - checksum: 10c0/f2e536b4b3479feee00adcacb5988d37027b771e682cf8f8a29c30b9216706aa39942fe14bf7fd8bb41c71f1b01074e3dca0632e359253def0bc4a86a5cfb3fd +"@prisma/debug@npm:5.17.0": + version: 5.17.0 + resolution: "@prisma/debug@npm:5.17.0" + checksum: 10c0/10aca89c8cd3a96c7f1153792110f33d96d1875e4af807002b9ca061eda255b1aa21e757b9e7a1690ac0676fb2312c441191cdb357acf45617dd658678984053 languageName: node linkType: hard -"@prisma/engines-version@npm:5.16.0-24.34ace0eb2704183d2c05b60b52fba5c43c13f303": - version: 5.16.0-24.34ace0eb2704183d2c05b60b52fba5c43c13f303 - resolution: "@prisma/engines-version@npm:5.16.0-24.34ace0eb2704183d2c05b60b52fba5c43c13f303" - checksum: 10c0/de02aed86429fbae0c98dbcb35baa9e1583a37aa222692a832a24153ed173a5559257c31296ecbd8ae5a8a99887f3aaf0797d819ca45e562905ce0bea411fe75 +"@prisma/engines-version@npm:5.17.0-31.393aa359c9ad4a4bb28630fb5613f9c281cde053": + version: 5.17.0-31.393aa359c9ad4a4bb28630fb5613f9c281cde053 + resolution: "@prisma/engines-version@npm:5.17.0-31.393aa359c9ad4a4bb28630fb5613f9c281cde053" + checksum: 10c0/164b4cd6965da770bcd085fa0596466b092060d19eb8a4ba3402e66bd9b2e813cae417eeca99422b66a3a05a65cfe6d0e0339083b53644acf553ac138693232d languageName: node linkType: hard -"@prisma/engines@npm:5.16.1": - version: 5.16.1 - resolution: "@prisma/engines@npm:5.16.1" +"@prisma/engines@npm:5.17.0": + version: 5.17.0 + resolution: "@prisma/engines@npm:5.17.0" dependencies: - "@prisma/debug": "npm:5.16.1" - "@prisma/engines-version": "npm:5.16.0-24.34ace0eb2704183d2c05b60b52fba5c43c13f303" - "@prisma/fetch-engine": "npm:5.16.1" - "@prisma/get-platform": "npm:5.16.1" - checksum: 10c0/062a97aee17734c937362672d5da730968e396f1e746ae1f146532324345093fdb01d243afba43843d3ca63b22f6beef83f49ec2649c0280597b875ad02bfe52 + "@prisma/debug": "npm:5.17.0" + "@prisma/engines-version": "npm:5.17.0-31.393aa359c9ad4a4bb28630fb5613f9c281cde053" + "@prisma/fetch-engine": "npm:5.17.0" + "@prisma/get-platform": "npm:5.17.0" + checksum: 10c0/b1d48c39fbe16680947685960be615894ccc1a2ca40263fc6d1ac4599e3100f2f31e71b02bd000c0f3269cd045f38817dfbddd37fefcb8a4dec6155a6df48e2f languageName: node linkType: hard -"@prisma/fetch-engine@npm:5.16.1": - version: 5.16.1 - resolution: "@prisma/fetch-engine@npm:5.16.1" +"@prisma/fetch-engine@npm:5.17.0": + version: 5.17.0 + resolution: "@prisma/fetch-engine@npm:5.17.0" dependencies: - "@prisma/debug": "npm:5.16.1" - "@prisma/engines-version": "npm:5.16.0-24.34ace0eb2704183d2c05b60b52fba5c43c13f303" - "@prisma/get-platform": "npm:5.16.1" - checksum: 10c0/d6fd9674948de65d2fbf23f4319d2ec584c97cbe34637109ae9e47b394a1dce33232f6a6739f7ca546c8ffd8217ba46b6a09ba3680a45a881f1cbfda34c9be3c + "@prisma/debug": "npm:5.17.0" + "@prisma/engines-version": "npm:5.17.0-31.393aa359c9ad4a4bb28630fb5613f9c281cde053" + "@prisma/get-platform": "npm:5.17.0" + checksum: 10c0/b5c554e8a637871fd6497e656d67e649d9eea3a06be325b68a686b707c78d200ba9ba20bd76b0a3408e5cb78f6e34bab535ce161174273db377353a01368806e languageName: node linkType: hard -"@prisma/get-platform@npm:5.16.1": - version: 5.16.1 - resolution: "@prisma/get-platform@npm:5.16.1" +"@prisma/get-platform@npm:5.17.0": + version: 5.17.0 + resolution: "@prisma/get-platform@npm:5.17.0" dependencies: - "@prisma/debug": "npm:5.16.1" - checksum: 10c0/3fc626175411fbc379189567c6a3cde39e98237918b82c30804086bf8969ecd9909633e567380c8b04146a231a9692f8947dde7086585e74272b1e5eeb3d016f + "@prisma/debug": "npm:5.17.0" + checksum: 10c0/8687736c6e18737e29544bc1f98653b75b4dcb85c1ffe02686da100e843bb30041dd9d00146a2178517d34b783a650c8b76bdde5029d1675bd28c2be6ee6565a languageName: node linkType: hard @@ -1215,6 +1184,13 @@ __metadata: languageName: node linkType: hard +"@sapphire/ratelimits@npm:^2.4.9": + version: 2.4.9 + resolution: "@sapphire/ratelimits@npm:2.4.9" + checksum: 10c0/e2e7da0ab8180914b42807044a14a786474f179e5e5e19b8b920a5ddc255b81263cbadf3d1c7ec60e2fbffc581d4de2e99b294bfec27d35874759d1d1b537cc7 + languageName: node + linkType: hard + "@sapphire/shapeshift@npm:^3.9.7": version: 3.9.7 resolution: "@sapphire/shapeshift@npm:3.9.7" @@ -1241,6 +1217,13 @@ __metadata: languageName: node linkType: hard +"@sapphire/timer-manager@npm:^1.0.2": + version: 1.0.2 + resolution: "@sapphire/timer-manager@npm:1.0.2" + checksum: 10c0/0c9fee9a94b5927020aa98dac0fbe7ab903fc8b028383ae28c625f262eec4b65973b5ba5a17d329f72e1abf2092e7655647517d8818378781d5b790cb8b2e62a + languageName: node + linkType: hard + "@sapphire/utilities@npm:^3.3.0": version: 3.3.0 resolution: "@sapphire/utilities@npm:3.3.0" @@ -1330,13 +1313,6 @@ __metadata: languageName: node linkType: hard -"@sinclair/typebox@npm:^0.27.8": - version: 0.27.8 - resolution: "@sinclair/typebox@npm:0.27.8" - checksum: 10c0/ef6351ae073c45c2ac89494dbb3e1f87cc60a93ce4cde797b782812b6f97da0d620ae81973f104b43c9b7eaa789ad20ba4f6a1359f1cc62f63729a55a7d22d4e - languageName: node - linkType: hard - "@types/accepts@npm:*": version: 1.3.7 resolution: "@types/accepts@npm:1.3.7" @@ -1633,80 +1609,89 @@ __metadata: languageName: node linkType: hard -"@vitest/coverage-v8@npm:^1.6.0": - version: 1.6.0 - resolution: "@vitest/coverage-v8@npm:1.6.0" +"@vitest/coverage-v8@npm:^2.0.3": + version: 2.0.3 + resolution: "@vitest/coverage-v8@npm:2.0.3" dependencies: - "@ampproject/remapping": "npm:^2.2.1" + "@ampproject/remapping": "npm:^2.3.0" "@bcoe/v8-coverage": "npm:^0.2.3" - debug: "npm:^4.3.4" + debug: "npm:^4.3.5" istanbul-lib-coverage: "npm:^3.2.2" istanbul-lib-report: "npm:^3.0.1" - istanbul-lib-source-maps: "npm:^5.0.4" - istanbul-reports: "npm:^3.1.6" - magic-string: "npm:^0.30.5" - magicast: "npm:^0.3.3" - picocolors: "npm:^1.0.0" - std-env: "npm:^3.5.0" - strip-literal: "npm:^2.0.0" - test-exclude: "npm:^6.0.0" + istanbul-lib-source-maps: "npm:^5.0.6" + istanbul-reports: "npm:^3.1.7" + magic-string: "npm:^0.30.10" + magicast: "npm:^0.3.4" + std-env: "npm:^3.7.0" + strip-literal: "npm:^2.1.0" + test-exclude: "npm:^7.0.1" + tinyrainbow: "npm:^1.2.0" peerDependencies: - vitest: 1.6.0 - checksum: 10c0/a7beaf2a88b628a9dc16ddca7589f2b2e4681598e6788d68423dffbb06c608edc52b2dd421ada069eb3cfd83f8f592ddd6e8b8db2d037bf13965a56c5e5835ac + vitest: 2.0.3 + checksum: 10c0/ac3bbe2ff7cb41a71d22d7174347bba81b789b8293da54a1cf266eef12b9b0f92a04ef3cdaddff176b11fa8db9ec3cbdc423bf51a5e847714a643a38b3da370f languageName: node linkType: hard -"@vitest/expect@npm:1.6.0": - version: 1.6.0 - resolution: "@vitest/expect@npm:1.6.0" +"@vitest/expect@npm:2.0.3": + version: 2.0.3 + resolution: "@vitest/expect@npm:2.0.3" dependencies: - "@vitest/spy": "npm:1.6.0" - "@vitest/utils": "npm:1.6.0" - chai: "npm:^4.3.10" - checksum: 10c0/a4351f912a70543e04960f5694f1f1ac95f71a856a46e87bba27d3eb72a08c5d11d35021cbdc6077452a152e7d93723fc804bba76c2cc53c8896b7789caadae3 + "@vitest/spy": "npm:2.0.3" + "@vitest/utils": "npm:2.0.3" + chai: "npm:^5.1.1" + tinyrainbow: "npm:^1.2.0" + checksum: 10c0/bc8dead850a8aeb84a0d5d8620e1437752cbfe10908c2d5ec9f80fc6d9c387d70c964abfd2d6caf76da2882022c0dd05b0fa09b7c2a44d65abdde2b6c73517fe languageName: node linkType: hard -"@vitest/runner@npm:1.6.0": - version: 1.6.0 - resolution: "@vitest/runner@npm:1.6.0" +"@vitest/pretty-format@npm:2.0.3, @vitest/pretty-format@npm:^2.0.3": + version: 2.0.3 + resolution: "@vitest/pretty-format@npm:2.0.3" dependencies: - "@vitest/utils": "npm:1.6.0" - p-limit: "npm:^5.0.0" - pathe: "npm:^1.1.1" - checksum: 10c0/27d67fa51f40effe0e41ee5f26563c12c0ef9a96161f806036f02ea5eb9980c5cdf305a70673942e7a1e3d472d4d7feb40093ae93024ef1ccc40637fc65b1d2f + tinyrainbow: "npm:^1.2.0" + checksum: 10c0/217fd176fa4d1e64e04bc6a187d146381e99921f46007f98f7132d0e31e2c14b9c6d050a150331b3368ee8004bbeab5b1b7d477522a4e4d71ad822d046debc16 languageName: node linkType: hard -"@vitest/snapshot@npm:1.6.0": - version: 1.6.0 - resolution: "@vitest/snapshot@npm:1.6.0" +"@vitest/runner@npm:2.0.3": + version: 2.0.3 + resolution: "@vitest/runner@npm:2.0.3" dependencies: - magic-string: "npm:^0.30.5" - pathe: "npm:^1.1.1" - pretty-format: "npm:^29.7.0" - checksum: 10c0/be027fd268d524589ff50c5fad7b4faa1ac5742b59ac6c1dc6f5a3930aad553560e6d8775e90ac4dfae4be746fc732a6f134ba95606a1519707ce70db3a772a5 + "@vitest/utils": "npm:2.0.3" + pathe: "npm:^1.1.2" + checksum: 10c0/efbf646457c29268f0d370985d8cbfcfc7d181693dfc2e061dd05ce911f43592957f2c866cde1b5b2e3078ae5d74b94dc28453e1c70b80e8467440223431e863 languageName: node linkType: hard -"@vitest/spy@npm:1.6.0": - version: 1.6.0 - resolution: "@vitest/spy@npm:1.6.0" +"@vitest/snapshot@npm:2.0.3": + version: 2.0.3 + resolution: "@vitest/snapshot@npm:2.0.3" dependencies: - tinyspy: "npm:^2.2.0" - checksum: 10c0/df66ea6632b44fb76ef6a65c1abbace13d883703aff37cd6d062add6dcd1b883f19ce733af8e0f7feb185b61600c6eb4042a518e4fb66323d0690ec357f9401c + "@vitest/pretty-format": "npm:2.0.3" + magic-string: "npm:^0.30.10" + pathe: "npm:^1.1.2" + checksum: 10c0/dc7e2e8f60d40c308c487effe2cd94c42bffa795c2d8c740c30b880b451637763891609a052afe29f0c9872e71141d439cb03118595e4a461fe6b4877ae99878 languageName: node linkType: hard -"@vitest/utils@npm:1.6.0": - version: 1.6.0 - resolution: "@vitest/utils@npm:1.6.0" +"@vitest/spy@npm:2.0.3": + version: 2.0.3 + resolution: "@vitest/spy@npm:2.0.3" dependencies: - diff-sequences: "npm:^29.6.3" + tinyspy: "npm:^3.0.0" + checksum: 10c0/4780aeed692c52756d70735b633ad58f201b2b8729b9e46c4cf968b8e4174e2c2cddd099de669019771bcd8e1ca32d0b9fa42d962e431fdf473b62393b9d2a0a + languageName: node + linkType: hard + +"@vitest/utils@npm:2.0.3": + version: 2.0.3 + resolution: "@vitest/utils@npm:2.0.3" + dependencies: + "@vitest/pretty-format": "npm:2.0.3" estree-walker: "npm:^3.0.3" - loupe: "npm:^2.3.7" - pretty-format: "npm:^29.7.0" - checksum: 10c0/8b0d19835866455eb0b02b31c5ca3d8ad45f41a24e4c7e1f064b480f6b2804dc895a70af332f14c11ed89581011b92b179718523f55f5b14787285a0321b1301 + loupe: "npm:^3.1.1" + tinyrainbow: "npm:^1.2.0" + checksum: 10c0/41b64c07814e7d576ebe7d11d277eb104a2aafb986497855a59f641b45fa53a30a2bfea525cd913e91b695f444a7a48b1f1e5909c27d5a989b0aea68f2242bd9 languageName: node linkType: hard @@ -1742,22 +1727,6 @@ __metadata: languageName: node linkType: hard -"acorn-walk@npm:^8.3.2": - version: 8.3.2 - resolution: "acorn-walk@npm:8.3.2" - checksum: 10c0/7e2a8dad5480df7f872569b9dccff2f3da7e65f5353686b1d6032ab9f4ddf6e3a2cb83a9b52cf50b1497fd522154dda92f0abf7153290cc79cd14721ff121e52 - languageName: node - linkType: hard - -"acorn@npm:^8.11.3": - version: 8.11.3 - resolution: "acorn@npm:8.11.3" - bin: - acorn: bin/acorn - checksum: 10c0/3ff155f8812e4a746fee8ecff1f227d527c4c45655bb1fad6347c3cb58e46190598217551b1500f18542d2bbe5c87120cb6927f5a074a59166fbdd9468f0a299 - languageName: node - linkType: hard - "acorn@npm:^8.8.2": version: 8.8.2 resolution: "acorn@npm:8.8.2" @@ -1809,13 +1778,6 @@ __metadata: languageName: node linkType: hard -"ansi-styles@npm:^5.0.0": - version: 5.2.0 - resolution: "ansi-styles@npm:5.2.0" - checksum: 10c0/9c4ca80eb3c2fb7b33841c210d2f20807f40865d27008d7c3f707b7f95cab7d67462a565e2388ac3285b71cb3d9bb2173de8da37c57692a362885ec34d6e27df - languageName: node - linkType: hard - "ansi-styles@npm:^6.1.0": version: 6.2.1 resolution: "ansi-styles@npm:6.2.1" @@ -1842,10 +1804,10 @@ __metadata: languageName: node linkType: hard -"assertion-error@npm:^1.1.0": - version: 1.1.0 - resolution: "assertion-error@npm:1.1.0" - checksum: 10c0/25456b2aa333250f01143968e02e4884a34588a8538fbbf65c91a637f1dbfb8069249133cd2f4e530f10f624d206a664e7df30207830b659e9f5298b00a4099b +"assertion-error@npm:^2.0.1": + version: 2.0.1 + resolution: "assertion-error@npm:2.0.1" + checksum: 10c0/bbbcb117ac6480138f8c93cf7f535614282dea9dc828f540cdece85e3c665e8f78958b96afac52f29ff883c72638e6a87d469ecc9fe5bc902df03ed24a55dba8 languageName: node linkType: hard @@ -1870,6 +1832,13 @@ __metadata: languageName: node linkType: hard +"base64-js@npm:^1.3.1": + version: 1.5.1 + resolution: "base64-js@npm:1.5.1" + checksum: 10c0/f23823513b63173a001030fae4f2dabe283b99a9d324ade3ad3d148e218134676f1ee8568c877cd79ec1c53158dcf2d2ba527a97c606618928ba99dd930102bf + languageName: node + linkType: hard + "binary-extensions@npm:^2.0.0": version: 2.3.0 resolution: "binary-extensions@npm:2.3.0" @@ -1877,6 +1846,17 @@ __metadata: languageName: node linkType: hard +"bl@npm:^4.1.0": + version: 4.1.0 + resolution: "bl@npm:4.1.0" + dependencies: + buffer: "npm:^5.5.0" + inherits: "npm:^2.0.4" + readable-stream: "npm:^3.4.0" + checksum: 10c0/02847e1d2cb089c9dc6958add42e3cdeaf07d13f575973963335ac0fdece563a50ac770ac4c8fa06492d2dd276f6cc3b7f08c7cd9c7a7ad0f8d388b2a28def5f + languageName: node + linkType: hard + "brace-expansion@npm:^1.1.7": version: 1.1.11 resolution: "brace-expansion@npm:1.1.11" @@ -1905,6 +1885,16 @@ __metadata: languageName: node linkType: hard +"buffer@npm:^5.5.0": + version: 5.7.1 + resolution: "buffer@npm:5.7.1" + dependencies: + base64-js: "npm:^1.3.1" + ieee754: "npm:^1.1.13" + checksum: 10c0/27cac81cff434ed2876058d72e7c4789d11ff1120ef32c9de48f59eab58179b66710c488987d295ae89a228f835fc66d088652dffeb8e3ba8659f80eb091d55e + languageName: node + linkType: hard + "bufferutil@npm:^4.0.8": version: 4.0.8 resolution: "bufferutil@npm:4.0.8" @@ -1942,22 +1932,20 @@ __metadata: languageName: node linkType: hard -"chai@npm:^4.3.10": - version: 4.4.1 - resolution: "chai@npm:4.4.1" +"chai@npm:^5.1.1": + version: 5.1.1 + resolution: "chai@npm:5.1.1" dependencies: - assertion-error: "npm:^1.1.0" - check-error: "npm:^1.0.3" - deep-eql: "npm:^4.1.3" - get-func-name: "npm:^2.0.2" - loupe: "npm:^2.3.6" - pathval: "npm:^1.1.1" - type-detect: "npm:^4.0.8" - checksum: 10c0/91590a8fe18bd6235dece04ccb2d5b4ecec49984b50924499bdcd7a95c02cb1fd2a689407c19bb854497bde534ef57525cfad6c7fdd2507100fd802fbc2aefbd + assertion-error: "npm:^2.0.1" + check-error: "npm:^2.1.1" + deep-eql: "npm:^5.0.1" + loupe: "npm:^3.1.0" + pathval: "npm:^2.0.0" + checksum: 10c0/e7f00e5881e3d5224f08fe63966ed6566bd9fdde175863c7c16dd5240416de9b34c4a0dd925f4fd64ad56256ca6507d32cf6131c49e1db65c62578eb31d4566c languageName: node linkType: hard -"chalk@npm:^4.1.2": +"chalk@npm:^4.1.0, chalk@npm:^4.1.2": version: 4.1.2 resolution: "chalk@npm:4.1.2" dependencies: @@ -1967,12 +1955,10 @@ __metadata: languageName: node linkType: hard -"check-error@npm:^1.0.3": - version: 1.0.3 - resolution: "check-error@npm:1.0.3" - dependencies: - get-func-name: "npm:^2.0.2" - checksum: 10c0/94aa37a7315c0e8a83d0112b5bfb5a8624f7f0f81057c73e4707729cdd8077166c6aefb3d8e2b92c63ee130d4a2ff94bad46d547e12f3238cc1d78342a973841 +"check-error@npm:^2.1.1": + version: 2.1.1 + resolution: "check-error@npm:2.1.1" + checksum: 10c0/979f13eccab306cf1785fa10941a590b4e7ea9916ea2a4f8c87f0316fc3eab07eabefb6e587424ef0f88cbcd3805791f172ea739863ca3d7ce2afc54641c7f0e languageName: node linkType: hard @@ -2016,6 +2002,22 @@ __metadata: languageName: node linkType: hard +"cli-cursor@npm:^3.1.0": + version: 3.1.0 + resolution: "cli-cursor@npm:3.1.0" + dependencies: + restore-cursor: "npm:^3.1.0" + checksum: 10c0/92a2f98ff9037d09be3dfe1f0d749664797fb674bf388375a2207a1203b69d41847abf16434203e0089212479e47a358b13a0222ab9fccfe8e2644a7ccebd111 + languageName: node + linkType: hard + +"cli-spinners@npm:^2.5.0": + version: 2.9.2 + resolution: "cli-spinners@npm:2.9.2" + checksum: 10c0/907a1c227ddf0d7a101e7ab8b300affc742ead4b4ebe920a5bf1bc6d45dce2958fcd195eb28fa25275062fe6fa9b109b93b63bc8033396ed3bcb50297008b3a3 + languageName: node + linkType: hard + "cliui@npm:^8.0.1": version: 8.0.1 resolution: "cliui@npm:8.0.1" @@ -2027,6 +2029,13 @@ __metadata: languageName: node linkType: hard +"clone@npm:^1.0.2": + version: 1.0.4 + resolution: "clone@npm:1.0.4" + checksum: 10c0/2176952b3649293473999a95d7bebfc9dc96410f6cbd3d2595cf12fd401f63a4bf41a7adbfd3ab2ff09ed60cb9870c58c6acdd18b87767366fabfc163700f13b + languageName: node + linkType: hard + "cluster-key-slot@npm:^1.1.0": version: 1.1.2 resolution: "cluster-key-slot@npm:1.1.2" @@ -2106,7 +2115,7 @@ __metadata: languageName: node linkType: hard -"debug@npm:4, debug@npm:^4": +"debug@npm:4, debug@npm:^4, debug@npm:^4.3.5": version: 4.3.5 resolution: "debug@npm:4.3.5" dependencies: @@ -2137,12 +2146,17 @@ __metadata: languageName: node linkType: hard -"deep-eql@npm:^4.1.3": - version: 4.1.3 - resolution: "deep-eql@npm:4.1.3" - dependencies: - type-detect: "npm:^4.0.0" - checksum: 10c0/ff34e8605d8253e1bf9fe48056e02c6f347b81d9b5df1c6650a1b0f6f847b4a86453b16dc226b34f853ef14b626e85d04e081b022e20b00cd7d54f079ce9bbdd +"deep-eql@npm:^5.0.1": + version: 5.0.2 + resolution: "deep-eql@npm:5.0.2" + checksum: 10c0/7102cf3b7bb719c6b9c0db2e19bf0aa9318d141581befe8c7ce8ccd39af9eaa4346e5e05adef7f9bd7015da0f13a3a25dcfe306ef79dc8668aedbecb658dd247 + languageName: node + linkType: hard + +"deep-object-diff@npm:^1.1.9": + version: 1.1.9 + resolution: "deep-object-diff@npm:1.1.9" + checksum: 10c0/12cfd1b000d16c9192fc649923c972f8aac2ddca4f71a292f8f2c1e2d5cf3c9c16c85e73ab3e7d8a89a5ec6918d6460677d0b05bd160f7bd50bb4816d496dc24 languageName: node linkType: hard @@ -2153,6 +2167,15 @@ __metadata: languageName: node linkType: hard +"defaults@npm:^1.0.3": + version: 1.0.4 + resolution: "defaults@npm:1.0.4" + dependencies: + clone: "npm:^1.0.2" + checksum: 10c0/9cfbe498f5c8ed733775db62dfd585780387d93c17477949e1670bfcfb9346e0281ce8c4bf9f4ac1fc0f9b851113bd6dc9e41182ea1644ccd97de639fa13c35a + languageName: node + linkType: hard + "delayed-stream@npm:~1.0.0": version: 1.0.0 resolution: "delayed-stream@npm:1.0.0" @@ -2167,13 +2190,6 @@ __metadata: languageName: node linkType: hard -"diff-sequences@npm:^29.6.3": - version: 29.6.3 - resolution: "diff-sequences@npm:29.6.3" - checksum: 10c0/32e27ac7dbffdf2fb0eb5a84efd98a9ad084fbabd5ac9abb8757c6770d5320d2acd172830b28c4add29bb873d59420601dfc805ac4064330ce59b1adfd0593b2 - languageName: node - linkType: hard - "discord-api-types@npm:0.37.83": version: 0.37.83 resolution: "discord-api-types@npm:0.37.83" @@ -2208,6 +2224,23 @@ __metadata: languageName: node linkType: hard +"dpdm@npm:^3.14.0": + version: 3.14.0 + resolution: "dpdm@npm:3.14.0" + dependencies: + chalk: "npm:^4.1.2" + fs-extra: "npm:^11.1.1" + glob: "npm:^10.3.4" + ora: "npm:^5.4.1" + tslib: "npm:^2.6.2" + typescript: "npm:^5.2.2" + yargs: "npm:^17.7.2" + bin: + dpdm: lib/bin/dpdm.js + checksum: 10c0/2d98064230d68bcc545da80783e9fab0cfb62e2862173af9d3c1648144376f6f7722bfccb5df91b59ed727928ed81d104ae74b53f35565cc5079e44d4d3a706c + languageName: node + linkType: hard + "e@npm:0.2.33, e@npm:^0.2.33": version: 0.2.33 resolution: "e@npm:0.2.33" @@ -2386,6 +2419,13 @@ __metadata: languageName: node linkType: hard +"exit-hook@npm:^4.0.0": + version: 4.0.0 + resolution: "exit-hook@npm:4.0.0" + checksum: 10c0/7fb33eaeb9050aee9479da9c93d42b796fb409c40e1d2b6ea2f40786ae7d7db6dc6a0f6ecc7bc24e479f957b7844bcb880044ded73320334743c64e3ecef48d7 + languageName: node + linkType: hard + "exponential-backoff@npm:^3.1.1": version: 3.1.1 resolution: "exponential-backoff@npm:3.1.1" @@ -2472,6 +2512,17 @@ __metadata: languageName: node linkType: hard +"fs-extra@npm:^11.1.1": + version: 11.2.0 + resolution: "fs-extra@npm:11.2.0" + dependencies: + graceful-fs: "npm:^4.2.0" + jsonfile: "npm:^6.0.1" + universalify: "npm:^2.0.0" + checksum: 10c0/d77a9a9efe60532d2e790e938c81a02c1b24904ef7a3efb3990b835514465ba720e99a6ea56fd5e2db53b4695319b644d76d5a0e9988a2beef80aa7b1da63398 + languageName: node + linkType: hard + "fs-minipass@npm:^2.0.0": version: 2.1.0 resolution: "fs-minipass@npm:2.1.0" @@ -2490,13 +2541,6 @@ __metadata: languageName: node linkType: hard -"fs.realpath@npm:^1.0.0": - version: 1.0.0 - resolution: "fs.realpath@npm:1.0.0" - checksum: 10c0/444cf1291d997165dfd4c0d58b69f0e4782bfd9149fd72faa4fe299e68e0e93d6db941660b37dd29153bf7186672ececa3b50b7e7249477b03fdf850f287c948 - languageName: node - linkType: hard - "fsevents@npm:~2.3.2": version: 2.3.2 resolution: "fsevents@npm:2.3.2" @@ -2549,7 +2593,7 @@ __metadata: languageName: node linkType: hard -"get-func-name@npm:^2.0.0, get-func-name@npm:^2.0.1, get-func-name@npm:^2.0.2": +"get-func-name@npm:^2.0.1": version: 2.0.2 resolution: "get-func-name@npm:2.0.2" checksum: 10c0/89830fd07623fa73429a711b9daecdb304386d237c71268007f788f113505ef1d4cc2d0b9680e072c5082490aec9df5d7758bf5ac6f1c37062855e8e3dc0b9df @@ -2597,21 +2641,23 @@ __metadata: languageName: node linkType: hard -"glob@npm:^7.1.4": - version: 7.2.3 - resolution: "glob@npm:7.2.3" +"glob@npm:^10.3.4, glob@npm:^10.4.1": + version: 10.4.5 + resolution: "glob@npm:10.4.5" dependencies: - fs.realpath: "npm:^1.0.0" - inflight: "npm:^1.0.4" - inherits: "npm:2" - minimatch: "npm:^3.1.1" - once: "npm:^1.3.0" - path-is-absolute: "npm:^1.0.0" - checksum: 10c0/65676153e2b0c9095100fe7f25a778bf45608eeb32c6048cf307f579649bcc30353277b3b898a3792602c65764e5baa4f643714dfbdfd64ea271d210c7a425fe + foreground-child: "npm:^3.1.0" + jackspeak: "npm:^3.1.2" + minimatch: "npm:^9.0.4" + minipass: "npm:^7.1.2" + package-json-from-dist: "npm:^1.0.0" + path-scurry: "npm:^1.11.1" + bin: + glob: dist/esm/bin.mjs + checksum: 10c0/19a9759ea77b8e3ca0a43c2f07ecddc2ad46216b786bb8f993c445aee80d345925a21e5280c7b7c6c59e860a0154b84e4b2b60321fea92cd3c56b4a7489f160e languageName: node linkType: hard -"graceful-fs@npm:^4.2.6": +"graceful-fs@npm:^4.1.6, graceful-fs@npm:^4.2.0, graceful-fs@npm:^4.2.6": version: 4.2.11 resolution: "graceful-fs@npm:4.2.11" checksum: 10c0/386d011a553e02bc594ac2ca0bd6d9e4c22d7fa8cfbfc448a6d148c59ea881b092db9dbe3547ae4b88e55f1b01f7c4a2ecc53b310c042793e63aa44cf6c257f2 @@ -2691,6 +2737,13 @@ __metadata: languageName: node linkType: hard +"ieee754@npm:^1.1.13": + version: 1.2.1 + resolution: "ieee754@npm:1.2.1" + checksum: 10c0/b0782ef5e0935b9f12883a2e2aa37baa75da6e66ce6515c168697b42160807d9330de9a32ec1ed73149aea02e0d822e572bca6f1e22bdcbd2149e13b050b17bb + languageName: node + linkType: hard + "ignore-by-default@npm:^1.0.1": version: 1.0.1 resolution: "ignore-by-default@npm:1.0.1" @@ -2736,17 +2789,7 @@ __metadata: languageName: node linkType: hard -"inflight@npm:^1.0.4": - version: 1.0.6 - resolution: "inflight@npm:1.0.6" - dependencies: - once: "npm:^1.3.0" - wrappy: "npm:1" - checksum: 10c0/7faca22584600a9dc5b9fca2cd5feb7135ac8c935449837b315676b4c90aa4f391ec4f42240178244b5a34e8bede1948627fda392ca3191522fc46b34e985ab2 - languageName: node - linkType: hard - -"inherits@npm:2": +"inherits@npm:^2.0.3, inherits@npm:^2.0.4": version: 2.0.4 resolution: "inherits@npm:2.0.4" checksum: 10c0/4e531f648b29039fb7426fb94075e6545faa1eb9fe83c29f0b6d9e7263aceb4289d2d4557db0d428188eeb449cc7c5e77b0a0b2c4e248ff2a65933a0dee49ef2 @@ -2837,6 +2880,13 @@ __metadata: languageName: node linkType: hard +"is-interactive@npm:^1.0.0": + version: 1.0.0 + resolution: "is-interactive@npm:1.0.0" + checksum: 10c0/dd47904dbf286cd20aa58c5192161be1a67138485b9836d5a70433b21a45442e9611b8498b8ab1f839fc962c7620667a50535fdfb4a6bc7989b8858645c06b4d + languageName: node + linkType: hard + "is-lambda@npm:^1.0.1": version: 1.0.1 resolution: "is-lambda@npm:1.0.1" @@ -2858,6 +2908,13 @@ __metadata: languageName: node linkType: hard +"is-unicode-supported@npm:^0.1.0": + version: 0.1.0 + resolution: "is-unicode-supported@npm:0.1.0" + checksum: 10c0/00cbe3455c3756be68d2542c416cab888aebd5012781d6819749fefb15162ff23e38501fe681b3d751c73e8ff561ac09a5293eba6f58fdf0178462ce6dcb3453 + languageName: node + linkType: hard + "isexe@npm:^2.0.0": version: 2.0.0 resolution: "isexe@npm:2.0.0" @@ -2908,18 +2965,18 @@ __metadata: languageName: node linkType: hard -"istanbul-lib-source-maps@npm:^5.0.4": - version: 5.0.5 - resolution: "istanbul-lib-source-maps@npm:5.0.5" +"istanbul-lib-source-maps@npm:^5.0.6": + version: 5.0.6 + resolution: "istanbul-lib-source-maps@npm:5.0.6" dependencies: "@jridgewell/trace-mapping": "npm:^0.3.23" debug: "npm:^4.1.1" istanbul-lib-coverage: "npm:^3.0.0" - checksum: 10c0/6b7ee06e82cbd5cf7bb9359e59d5e654b50e84260394de2e345d545823c474ac1df356b53a0453629ede8c20c9eb145bb4ea2afa8d55117f7e7413133bcdfe6f + checksum: 10c0/ffe75d70b303a3621ee4671554f306e0831b16f39ab7f4ab52e54d356a5d33e534d97563e318f1333a6aae1d42f91ec49c76b6cd3f3fb378addcb5c81da0255f languageName: node linkType: hard -"istanbul-reports@npm:^3.1.6": +"istanbul-reports@npm:^3.1.7": version: 3.1.7 resolution: "istanbul-reports@npm:3.1.7" dependencies: @@ -2942,10 +2999,10 @@ __metadata: languageName: node linkType: hard -"js-tokens@npm:^8.0.2": - version: 8.0.3 - resolution: "js-tokens@npm:8.0.3" - checksum: 10c0/b50ba7d926b087ad31949d8155c7bc84374e0785019b17bdddeb2c4f98f5dea04ba464651fe23a8be4f7d15f50d06ce8bb536087b24ce3ebfbaea4a1dc5869f0 +"js-tokens@npm:^9.0.0": + version: 9.0.0 + resolution: "js-tokens@npm:9.0.0" + checksum: 10c0/4ad1c12f47b8c8b2a3a99e29ef338c1385c7b7442198a425f3463f3537384dab6032012791bfc2f056ea5ecdb06b1ed4f70e11a3ab3f388d3dcebfe16a52b27d languageName: node linkType: hard @@ -2956,20 +3013,16 @@ __metadata: languageName: node linkType: hard -"jsonc-parser@npm:^3.2.0": - version: 3.2.0 - resolution: "jsonc-parser@npm:3.2.0" - checksum: 10c0/5a12d4d04dad381852476872a29dcee03a57439574e4181d91dca71904fcdcc5e8e4706c0a68a2c61ad9810e1e1c5806b5100d52d3e727b78f5cdc595401045b - languageName: node - linkType: hard - -"local-pkg@npm:^0.5.0": - version: 0.5.0 - resolution: "local-pkg@npm:0.5.0" +"jsonfile@npm:^6.0.1": + version: 6.1.0 + resolution: "jsonfile@npm:6.1.0" dependencies: - mlly: "npm:^1.4.2" - pkg-types: "npm:^1.0.3" - checksum: 10c0/f61cbd00d7689f275558b1a45c7ff2a3ddf8472654123ed880215677b9adfa729f1081e50c27ffb415cdb9fa706fb755fec5e23cdd965be375c8059e87ff1cc9 + graceful-fs: "npm:^4.1.6" + universalify: "npm:^2.0.0" + dependenciesMeta: + graceful-fs: + optional: true + checksum: 10c0/4f95b5e8a5622b1e9e8f33c96b7ef3158122f595998114d1e7f03985649ea99cb3cd99ce1ed1831ae94c8c8543ab45ebd044207612f31a56fd08462140e46865 languageName: node linkType: hard @@ -3008,21 +3061,22 @@ __metadata: languageName: node linkType: hard -"loupe@npm:^2.3.6": - version: 2.3.6 - resolution: "loupe@npm:2.3.6" +"log-symbols@npm:^4.1.0": + version: 4.1.0 + resolution: "log-symbols@npm:4.1.0" dependencies: - get-func-name: "npm:^2.0.0" - checksum: 10c0/a974841ce94ef2a35aac7144e7f9e789e3887f82286cd9ffe7ff00f2ac9d117481989948657465e2b0b102f23136d89ae0a18fd4a32d9015012cd64464453289 + chalk: "npm:^4.1.0" + is-unicode-supported: "npm:^0.1.0" + checksum: 10c0/67f445a9ffa76db1989d0fa98586e5bc2fd5247260dafb8ad93d9f0ccd5896d53fb830b0e54dade5ad838b9de2006c826831a3c528913093af20dff8bd24aca6 languageName: node linkType: hard -"loupe@npm:^2.3.7": - version: 2.3.7 - resolution: "loupe@npm:2.3.7" +"loupe@npm:^3.1.0, loupe@npm:^3.1.1": + version: 3.1.1 + resolution: "loupe@npm:3.1.1" dependencies: get-func-name: "npm:^2.0.1" - checksum: 10c0/71a781c8fc21527b99ed1062043f1f2bb30bdaf54fa4cf92463427e1718bc6567af2988300bc243c1f276e4f0876f29e3cbf7b58106fdc186915687456ce5bf4 + checksum: 10c0/99f88badc47e894016df0c403de846fedfea61154aadabbf776c8428dd59e8d8378007135d385d737de32ae47980af07d22ba7bec5ef7beebd721de9baa0a0af languageName: node linkType: hard @@ -3056,23 +3110,23 @@ __metadata: languageName: node linkType: hard -"magic-string@npm:^0.30.5": - version: 0.30.7 - resolution: "magic-string@npm:0.30.7" +"magic-string@npm:^0.30.10": + version: 0.30.10 + resolution: "magic-string@npm:0.30.10" dependencies: "@jridgewell/sourcemap-codec": "npm:^1.4.15" - checksum: 10c0/d1d949f7a53c37c6e685f4ea7b2b151c2fe0cc5af8f1f979ecba916f7d60d58f35309aaf4c8b09ce1aef7c160b957be39a38b52b478a91650750931e4ddd5daf + checksum: 10c0/aa9ca17eae571a19bce92c8221193b6f93ee8511abb10f085e55ffd398db8e4c089a208d9eac559deee96a08b7b24d636ea4ab92f09c6cf42a7d1af51f7fd62b languageName: node linkType: hard -"magicast@npm:^0.3.3": - version: 0.3.3 - resolution: "magicast@npm:0.3.3" +"magicast@npm:^0.3.4": + version: 0.3.4 + resolution: "magicast@npm:0.3.4" dependencies: - "@babel/parser": "npm:^7.23.6" - "@babel/types": "npm:^7.23.6" - source-map-js: "npm:^1.0.2" - checksum: 10c0/2eeba19545ac4328433be817bd81fcfa8a517ec67599260541e13ce5ce18b27ff8830f1b87d54a1392d408d1b96e44938bf026920f0110edbdfecc96980919b3 + "@babel/parser": "npm:^7.24.4" + "@babel/types": "npm:^7.24.0" + source-map-js: "npm:^1.2.0" + checksum: 10c0/7ebaaac397b13c31ca05e6d9649296751d76749b945d10a0800107872119fbdf267acdb604571d25e38ec6fd7ab3568a951b6e76eaef1caba9eaa11778fd9783 languageName: node linkType: hard @@ -3161,6 +3215,13 @@ __metadata: languageName: node linkType: hard +"mimic-fn@npm:^2.1.0": + version: 2.1.0 + resolution: "mimic-fn@npm:2.1.0" + checksum: 10c0/b26f5479d7ec6cc2bce275a08f146cf78f5e7b661b18114e2506dd91ec7ec47e7a25bf4360e5438094db0560bcc868079fb3b1fb3892b833c1ecbf63f80c95a4 + languageName: node + linkType: hard + "mimic-fn@npm:^4.0.0": version: 4.0.0 resolution: "mimic-fn@npm:4.0.0" @@ -3168,7 +3229,7 @@ __metadata: languageName: node linkType: hard -"minimatch@npm:^3.0.4, minimatch@npm:^3.1.1, minimatch@npm:^3.1.2": +"minimatch@npm:^3.1.2": version: 3.1.2 resolution: "minimatch@npm:3.1.2" dependencies: @@ -3286,30 +3347,6 @@ __metadata: languageName: node linkType: hard -"mlly@npm:^1.2.0": - version: 1.2.1 - resolution: "mlly@npm:1.2.1" - dependencies: - acorn: "npm:^8.8.2" - pathe: "npm:^1.1.0" - pkg-types: "npm:^1.0.3" - ufo: "npm:^1.1.2" - checksum: 10c0/4d2972801ad002876d8acd9c7b9cbf27969c4a6ee977217fe3c402dcc6b135f7259918baf0e3a3835b56f5b8e02b608d8fea49a2299ea6d70e97c7deaa440420 - languageName: node - linkType: hard - -"mlly@npm:^1.4.2": - version: 1.6.1 - resolution: "mlly@npm:1.6.1" - dependencies: - acorn: "npm:^8.11.3" - pathe: "npm:^1.1.2" - pkg-types: "npm:^1.0.3" - ufo: "npm:^1.3.2" - checksum: 10c0/a7bf26b3d4f83b0f5a5232caa3af44be08b464f562f31c11d885d1bc2d43b7d717137d47b0c06fdc69e1b33ffc09f902b6d2b18de02c577849d40914e8785092 - languageName: node - linkType: hard - "module-details-from-path@npm:^1.0.3": version: 1.0.3 resolution: "module-details-from-path@npm:1.0.3" @@ -3495,23 +3532,23 @@ __metadata: languageName: node linkType: hard -"oldschooljs@npm:^2.5.9": - version: 2.5.9 - resolution: "oldschooljs@npm:2.5.9" +"oldschooljs@npm:^2.5.10": + version: 2.5.10 + resolution: "oldschooljs@npm:2.5.10" dependencies: deepmerge: "npm:4.3.1" e: "npm:^0.2.33" node-fetch: "npm:2.6.7" - checksum: 10c0/6e9f44153a876c2fe389a5c65d437b9526d0cfb0a574696823144f59dee84e36330dcb88a76fdaa3199f9c00c7ba9d57559bf6d6b511e792be90e0783091eac1 + checksum: 10c0/c8ff7fcaee60ebcc27c918fb315e226c3b0de93d45d2779733b6530156761a94aca760f7b247c8d4812cc46b865bc9a9916359be0fee996e018c37c927ec2696 languageName: node linkType: hard -"once@npm:^1.3.0": - version: 1.4.0 - resolution: "once@npm:1.4.0" +"onetime@npm:^5.1.0": + version: 5.1.2 + resolution: "onetime@npm:5.1.2" dependencies: - wrappy: "npm:1" - checksum: 10c0/5d48aca287dfefabd756621c5dfce5c91a549a93e9fdb7b8246bc4c4790aa2ec17b34a260530474635147aeb631a2dcc8b32c613df0675f96041cbb8244517d0 + mimic-fn: "npm:^2.1.0" + checksum: 10c0/ffcef6fbb2692c3c40749f31ea2e22677a876daea92959b8a80b521d95cca7a668c884d8b2045d1d8ee7d56796aa405c405462af112a1477594cc63531baeb8f languageName: node linkType: hard @@ -3535,6 +3572,23 @@ __metadata: languageName: node linkType: hard +"ora@npm:^5.4.1": + version: 5.4.1 + resolution: "ora@npm:5.4.1" + dependencies: + bl: "npm:^4.1.0" + chalk: "npm:^4.1.0" + cli-cursor: "npm:^3.1.0" + cli-spinners: "npm:^2.5.0" + is-interactive: "npm:^1.0.0" + is-unicode-supported: "npm:^0.1.0" + log-symbols: "npm:^4.1.0" + strip-ansi: "npm:^6.0.0" + wcwidth: "npm:^1.0.1" + checksum: 10c0/10ff14aace236d0e2f044193362b22edce4784add08b779eccc8f8ef97195cae1248db8ec1ec5f5ff076f91acbe573f5f42a98c19b78dba8c54eefff983cae85 + languageName: node + linkType: hard + "os-tmpdir@npm:~1.0.2": version: 1.0.2 resolution: "os-tmpdir@npm:1.0.2" @@ -3549,15 +3603,6 @@ __metadata: languageName: node linkType: hard -"p-limit@npm:^5.0.0": - version: 5.0.0 - resolution: "p-limit@npm:5.0.0" - dependencies: - yocto-queue: "npm:^1.0.0" - checksum: 10c0/574e93b8895a26e8485eb1df7c4b58a1a6e8d8ae41b1750cc2cc440922b3d306044fc6e9a7f74578a883d46802d9db72b30f2e612690fcef838c173261b1ed83 - languageName: node - linkType: hard - "p-map@npm:^4.0.0": version: 4.0.0 resolution: "p-map@npm:4.0.0" @@ -3593,13 +3638,6 @@ __metadata: languageName: node linkType: hard -"path-is-absolute@npm:^1.0.0": - version: 1.0.1 - resolution: "path-is-absolute@npm:1.0.1" - checksum: 10c0/127da03c82172a2a50099cddbf02510c1791fc2cc5f7713ddb613a56838db1e8168b121a920079d052e0936c23005562059756d653b7c544c53185efe53be078 - languageName: node - linkType: hard - "path-key@npm:^3.1.0": version: 3.1.1 resolution: "path-key@npm:3.1.1" @@ -3631,24 +3669,17 @@ __metadata: languageName: node linkType: hard -"pathe@npm:^1.1.0": - version: 1.1.0 - resolution: "pathe@npm:1.1.0" - checksum: 10c0/1c5d07378475bcdf4f435684566190d35d06be2db8b8e61cf9e866ae649941fdb093d732fa01b0f51d86e3f94140543c2571b0bf65a87ca7b5d1f52152aabe03 - languageName: node - linkType: hard - -"pathe@npm:^1.1.1, pathe@npm:^1.1.2": +"pathe@npm:^1.1.2": version: 1.1.2 resolution: "pathe@npm:1.1.2" checksum: 10c0/64ee0a4e587fb0f208d9777a6c56e4f9050039268faaaaecd50e959ef01bf847b7872785c36483fa5cdcdbdfdb31fef2ff222684d4fc21c330ab60395c681897 languageName: node linkType: hard -"pathval@npm:^1.1.1": - version: 1.1.1 - resolution: "pathval@npm:1.1.1" - checksum: 10c0/f63e1bc1b33593cdf094ed6ff5c49c1c0dc5dc20a646ca9725cc7fe7cd9995002d51d5685b9b2ec6814342935748b711bafa840f84c0bb04e38ff40a335c94dc +"pathval@npm:^2.0.0": + version: 2.0.0 + resolution: "pathval@npm:2.0.0" + checksum: 10c0/602e4ee347fba8a599115af2ccd8179836a63c925c23e04bd056d0674a64b39e3a081b643cc7bc0b84390517df2d800a46fcc5598d42c155fe4977095c2f77c5 languageName: node linkType: hard @@ -3734,17 +3765,6 @@ __metadata: languageName: node linkType: hard -"pkg-types@npm:^1.0.3": - version: 1.0.3 - resolution: "pkg-types@npm:1.0.3" - dependencies: - jsonc-parser: "npm:^3.2.0" - mlly: "npm:^1.2.0" - pathe: "npm:^1.1.0" - checksum: 10c0/7f692ff2005f51b8721381caf9bdbc7f5461506ba19c34f8631660a215c8de5e6dca268f23a319dd180b8f7c47a0dc6efea14b376c485ff99e98d810b8f786c4 - languageName: node - linkType: hard - "postcss@npm:^8.4.38": version: 8.4.38 resolution: "postcss@npm:8.4.38" @@ -3832,17 +3852,6 @@ __metadata: languageName: node linkType: hard -"pretty-format@npm:^29.7.0": - version: 29.7.0 - resolution: "pretty-format@npm:29.7.0" - dependencies: - "@jest/schemas": "npm:^29.6.3" - ansi-styles: "npm:^5.0.0" - react-is: "npm:^18.0.0" - checksum: 10c0/edc5ff89f51916f036c62ed433506b55446ff739358de77207e63e88a28ca2894caac6e73dcb68166a606e51c8087d32d400473e6a9fdd2dbe743f46c9c0276f - languageName: node - linkType: hard - "printable-characters@npm:^1.0.42": version: 1.0.42 resolution: "printable-characters@npm:1.0.42" @@ -3850,14 +3859,14 @@ __metadata: languageName: node linkType: hard -"prisma@npm:^5.16.1": - version: 5.16.1 - resolution: "prisma@npm:5.16.1" +"prisma@npm:^5.17.0": + version: 5.17.0 + resolution: "prisma@npm:5.17.0" dependencies: - "@prisma/engines": "npm:5.16.1" + "@prisma/engines": "npm:5.17.0" bin: prisma: build/index.js - checksum: 10c0/f30b3f4f5c1c0bb918bd6bc69a8e0af01d21a1b9ad4529f8abe2dc4efff0a24bf7ac34eb2e6fb7bd9159d789a16dbe58a0f1b973478194feb5ecd1048262bf47 + checksum: 10c0/30546a8576ffadf66d6f34cd833e25e21eec99847db92c4d88f6c9dbbc401abbd3f699f9e0f0dbcd9d5229ccba47c6aadb42ba6cd6e29afb7335689c7257c964 languageName: node linkType: hard @@ -3913,10 +3922,14 @@ __metadata: languageName: node linkType: hard -"react-is@npm:^18.0.0": - version: 18.2.0 - resolution: "react-is@npm:18.2.0" - checksum: 10c0/6eb5e4b28028c23e2bfcf73371e72cd4162e4ac7ab445ddae2afe24e347a37d6dc22fae6e1748632cd43c6d4f9b8f86dcf26bf9275e1874f436d129952528ae0 +"readable-stream@npm:^3.4.0": + version: 3.6.2 + resolution: "readable-stream@npm:3.6.2" + dependencies: + inherits: "npm:^2.0.3" + string_decoder: "npm:^1.1.1" + util-deprecate: "npm:^1.0.1" + checksum: 10c0/e37be5c79c376fdd088a45fa31ea2e423e5d48854be7a22a58869b4e84d25047b193f6acb54f1012331e1bcd667ffb569c01b99d36b0bd59658fb33f513511b7 languageName: node linkType: hard @@ -3959,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" @@ -4010,6 +4032,16 @@ __metadata: languageName: node linkType: hard +"restore-cursor@npm:^3.1.0": + version: 3.1.0 + resolution: "restore-cursor@npm:3.1.0" + dependencies: + onetime: "npm:^5.1.0" + signal-exit: "npm:^3.0.2" + checksum: 10c0/8051a371d6aa67ff21625fa94e2357bd81ffdc96267f3fb0fc4aaf4534028343836548ef34c240ffa8c25b280ca35eb36be00b3cb2133fa4f51896d7e73c6b4f + languageName: node + linkType: hard + "retry@npm:^0.12.0": version: 0.12.0 resolution: "retry@npm:0.12.0" @@ -4090,23 +4122,27 @@ __metadata: dependencies: "@biomejs/biome": "npm:^1.8.3" "@napi-rs/canvas": "npm:^0.1.53" - "@oldschoolgg/toolkit": "git+https://github.com/oldschoolgg/toolkit.git#62f9f71274c6d36db6c56a2228ae3df9ab090dbd" - "@prisma/client": "npm:^5.16.1" + "@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" "@sapphire/time-utilities": "npm:^1.6.0" + "@sapphire/timer-manager": "npm:^1.0.2" "@sentry/node": "npm:^8.15.0" "@types/lodash": "npm:^4.14.195" "@types/node": "npm:^20.14.9" "@types/node-cron": "npm:^3.0.7" "@types/node-fetch": "npm:^2.6.1" - "@vitest/coverage-v8": "npm:^1.6.0" + "@vitest/coverage-v8": "npm:^2.0.3" ascii-table3: "npm:^0.9.0" bufferutil: "npm:^4.0.8" concurrently: "npm:^8.2.2" discord.js: "npm:^14.15.3" dotenv: "npm:^16.4.5" + dpdm: "npm:^3.14.0" e: "npm:0.2.33" esbuild: "npm:0.21.5" + exit-hook: "npm:^4.0.0" fast-deep-equal: "npm:^3.1.3" fast-glob: "npm:^3.3.2" lodash: "npm:^4.17.21" @@ -4115,17 +4151,18 @@ __metadata: node-cron: "npm:^3.0.3" node-fetch: "npm:^2.6.7" nodemon: "npm:^3.1.4" - oldschooljs: "npm:^2.5.9" + oldschooljs: "npm:^2.5.10" p-queue: "npm:^6.6.2" piscina: "npm:^4.6.1" prettier: "npm:^3.3.2" - prisma: "npm:^5.16.1" + 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" typescript: "npm:^5.5.3" - vitest: "npm:^1.6.0" + vitest: "npm:^2.0.3" zlib-sync: "npm:^0.1.9" zod: "npm:^3.23.8" languageName: unknown @@ -4149,6 +4186,13 @@ __metadata: languageName: node linkType: hard +"safe-buffer@npm:~5.2.0": + version: 5.2.1 + resolution: "safe-buffer@npm:5.2.1" + checksum: 10c0/6501914237c0a86e9675d4e51d89ca3c21ffd6a31642efeba25ad65720bce6921c9e7e974e5be91a786b25aa058b5303285d3c15dbabf983a919f5f630d349f3 + languageName: node + linkType: hard + "safer-buffer@npm:>= 2.1.2 < 3.0.0": version: 2.1.2 resolution: "safer-buffer@npm:2.1.2" @@ -4222,6 +4266,13 @@ __metadata: languageName: node linkType: hard +"signal-exit@npm:^3.0.2": + version: 3.0.7 + resolution: "signal-exit@npm:3.0.7" + checksum: 10c0/25d272fa73e146048565e08f3309d5b942c1979a6f4a58a8c59d5fa299728e9c2fcd1a759ec870863b1fd38653670240cd420dad2ad9330c71f36608a6a1c912 + languageName: node + linkType: hard + "signal-exit@npm:^4.0.1, signal-exit@npm:^4.1.0": version: 4.1.0 resolution: "signal-exit@npm:4.1.0" @@ -4282,13 +4333,6 @@ __metadata: languageName: node linkType: hard -"source-map-js@npm:^1.0.2": - version: 1.0.2 - resolution: "source-map-js@npm:1.0.2" - checksum: 10c0/32f2dfd1e9b7168f9a9715eb1b4e21905850f3b50cf02cf476e47e4eebe8e6b762b63a64357896aa29b37e24922b4282df0f492e0d2ace572b43d15525976ff8 - languageName: node - linkType: hard - "source-map-js@npm:^1.2.0": version: 1.2.0 resolution: "source-map-js@npm:1.2.0" @@ -4333,7 +4377,7 @@ __metadata: languageName: node linkType: hard -"std-env@npm:^3.5.0": +"std-env@npm:^3.7.0": version: 3.7.0 resolution: "std-env@npm:3.7.0" checksum: 10c0/60edf2d130a4feb7002974af3d5a5f3343558d1ccf8d9b9934d225c638606884db4a20d2fe6440a09605bca282af6b042ae8070a10490c0800d69e82e478f41e @@ -4362,6 +4406,15 @@ __metadata: languageName: node linkType: hard +"string_decoder@npm:^1.1.1": + version: 1.3.0 + resolution: "string_decoder@npm:1.3.0" + dependencies: + safe-buffer: "npm:~5.2.0" + checksum: 10c0/810614ddb030e271cd591935dcd5956b2410dd079d64ff92a1844d6b7588bf992b3e1b69b0f4d34a3e06e0bd73046ac646b5264c1987b20d0601f81ef35d731d + languageName: node + linkType: hard + "strip-ansi-cjs@npm:strip-ansi@^6.0.1, strip-ansi@npm:^6.0.0, strip-ansi@npm:^6.0.1": version: 6.0.1 resolution: "strip-ansi@npm:6.0.1" @@ -4387,12 +4440,12 @@ __metadata: languageName: node linkType: hard -"strip-literal@npm:^2.0.0": - version: 2.0.0 - resolution: "strip-literal@npm:2.0.0" +"strip-literal@npm:^2.1.0": + version: 2.1.0 + resolution: "strip-literal@npm:2.1.0" dependencies: - js-tokens: "npm:^8.0.2" - checksum: 10c0/63a6e4224ac7088ff93fd19fc0f6882705020da2f0767dbbecb929cbf9d49022e72350420f47be635866823608da9b9a5caf34f518004721895b6031199fc3c8 + js-tokens: "npm:^9.0.0" + checksum: 10c0/bc8b8c8346125ae3c20fcdaf12e10a498ff85baf6f69597b4ab2b5fbf2e58cfd2827f1a44f83606b852da99a5f6c8279770046ddea974c510c17c98934c9cc24 languageName: node linkType: hard @@ -4444,35 +4497,42 @@ __metadata: languageName: node linkType: hard -"test-exclude@npm:^6.0.0": - version: 6.0.0 - resolution: "test-exclude@npm:6.0.0" +"test-exclude@npm:^7.0.1": + version: 7.0.1 + resolution: "test-exclude@npm:7.0.1" dependencies: "@istanbuljs/schema": "npm:^0.1.2" - glob: "npm:^7.1.4" - minimatch: "npm:^3.0.4" - checksum: 10c0/019d33d81adff3f9f1bfcff18125fb2d3c65564f437d9be539270ee74b994986abb8260c7c2ce90e8f30162178b09dbbce33c6389273afac4f36069c48521f57 + glob: "npm:^10.4.1" + minimatch: "npm:^9.0.4" + checksum: 10c0/6d67b9af4336a2e12b26a68c83308c7863534c65f27ed4ff7068a56f5a58f7ac703e8fc80f698a19bb154fd8f705cdf7ec347d9512b2c522c737269507e7b263 + languageName: node + linkType: hard + +"tinybench@npm:^2.8.0": + version: 2.8.0 + resolution: "tinybench@npm:2.8.0" + checksum: 10c0/5a9a642351fa3e4955e0cbf38f5674be5f3ba6730fd872fd23a5c953ad6c914234d5aba6ea41ef88820180a81829ceece5bd8d3967c490c5171bca1141c2f24d languageName: node linkType: hard -"tinybench@npm:^2.5.1": - version: 2.6.0 - resolution: "tinybench@npm:2.6.0" - checksum: 10c0/60ea35699bf8bac9bc8cf279fa5877ab5b335b4673dcd07bf0fbbab9d7953a02c0ccded374677213eaa13aa147f54eb75d3230139ddbeec3875829ebe73db310 +"tinypool@npm:^1.0.0": + version: 1.0.0 + resolution: "tinypool@npm:1.0.0" + checksum: 10c0/71b20b9c54366393831c286a0772380c20f8cad9546d724c484edb47aea3228f274c58e98cf51d28c40869b39f5273209ef3ea94a9d2a23f8b292f4731cd3e4e languageName: node linkType: hard -"tinypool@npm:^0.8.3": - version: 0.8.4 - resolution: "tinypool@npm:0.8.4" - checksum: 10c0/779c790adcb0316a45359652f4b025958c1dff5a82460fe49f553c864309b12ad732c8288be52f852973bc76317f5e7b3598878aee0beb8a33322c0e72c4a66c +"tinyrainbow@npm:^1.2.0": + version: 1.2.0 + resolution: "tinyrainbow@npm:1.2.0" + checksum: 10c0/7f78a4b997e5ba0f5ecb75e7ed786f30bab9063716e7dff24dd84013fb338802e43d176cb21ed12480561f5649a82184cf31efb296601a29d38145b1cdb4c192 languageName: node linkType: hard -"tinyspy@npm:^2.2.0": - version: 2.2.1 - resolution: "tinyspy@npm:2.2.1" - checksum: 10c0/0b4cfd07c09871e12c592dfa7b91528124dc49a4766a0b23350638c62e6a483d5a2a667de7e6282246c0d4f09996482ddaacbd01f0c05b7ed7e0f79d32409bdc +"tinyspy@npm:^3.0.0": + version: 3.0.0 + resolution: "tinyspy@npm:3.0.0" + checksum: 10c0/eb0dec264aa5370efd3d29743825eb115ed7f1ef8a72a431e9a75d5c9e7d67e99d04b0d61d86b8cd70c79ec27863f241ad0317bc453f78762e0cbd76d2c332d0 languageName: node linkType: hard @@ -4563,14 +4623,14 @@ __metadata: languageName: node linkType: hard -"type-detect@npm:^4.0.0, type-detect@npm:^4.0.8": - version: 4.0.8 - resolution: "type-detect@npm:4.0.8" - checksum: 10c0/8fb9a51d3f365a7de84ab7f73b653534b61b622aa6800aecdb0f1095a4a646d3f5eb295322127b6573db7982afcd40ab492d038cf825a42093a58b1e1353e0bd +"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.5.3": +"typescript@npm:^5.2.2, typescript@npm:^5.5.3": version: 5.5.3 resolution: "typescript@npm:5.5.3" bin: @@ -4580,7 +4640,7 @@ __metadata: languageName: node linkType: hard -"typescript@patch:typescript@npm%3A^5.5.3#optional!builtin": +"typescript@patch:typescript@npm%3A^5.2.2#optional!builtin, typescript@patch:typescript@npm%3A^5.5.3#optional!builtin": version: 5.5.3 resolution: "typescript@patch:typescript@npm%3A5.5.3#optional!builtin::version=5.5.3&hash=379a07" bin: @@ -4590,20 +4650,6 @@ __metadata: languageName: node linkType: hard -"ufo@npm:^1.1.2": - version: 1.1.2 - resolution: "ufo@npm:1.1.2" - checksum: 10c0/f19c5e0093447dbebb33cb84bfc2073e4a8a5d3535f44ca3aaa1470a93f394f14896b3a11cd4258fb986ba86b566543ea079244205a06ce35a480d4e613aca6e - languageName: node - linkType: hard - -"ufo@npm:^1.3.2": - version: 1.4.0 - resolution: "ufo@npm:1.4.0" - checksum: 10c0/d9a3cb8c5fd13356e0af661362244fd0a901edcdd08996f42553271007cae01e85dcec29a3303a87ddab6aa705cbd630332aaa8c268d037483536b198fa67a7c - languageName: node - linkType: hard - "undefsafe@npm:^2.0.5": version: 2.0.5 resolution: "undefsafe@npm:2.0.5" @@ -4643,6 +4689,20 @@ __metadata: languageName: node linkType: hard +"universalify@npm:^2.0.0": + version: 2.0.1 + resolution: "universalify@npm:2.0.1" + checksum: 10c0/73e8ee3809041ca8b818efb141801a1004e3fc0002727f1531f4de613ea281b494a40909596dae4a042a4fb6cd385af5d4db2e137b1362e0e91384b828effd3a + languageName: node + linkType: hard + +"util-deprecate@npm:^1.0.1": + version: 1.0.2 + resolution: "util-deprecate@npm:1.0.2" + checksum: 10c0/41a5bdd214df2f6c3ecf8622745e4a366c4adced864bc3c833739791aeeeb1838119af7daed4ba36428114b5c67dcda034a79c882e97e43c03e66a4dd7389942 + languageName: node + linkType: hard + "uuid@npm:8.3.2": version: 8.3.2 resolution: "uuid@npm:8.3.2" @@ -4652,18 +4712,18 @@ __metadata: languageName: node linkType: hard -"vite-node@npm:1.6.0": - version: 1.6.0 - resolution: "vite-node@npm:1.6.0" +"vite-node@npm:2.0.3": + version: 2.0.3 + resolution: "vite-node@npm:2.0.3" dependencies: cac: "npm:^6.7.14" - debug: "npm:^4.3.4" - pathe: "npm:^1.1.1" - picocolors: "npm:^1.0.0" + debug: "npm:^4.3.5" + pathe: "npm:^1.1.2" + tinyrainbow: "npm:^1.2.0" vite: "npm:^5.0.0" bin: vite-node: vite-node.mjs - checksum: 10c0/0807e6501ac7763e0efa2b4bd484ce99fb207e92c98624c9f8999d1f6727ac026e457994260fa7fdb7060d87546d197081e46a705d05b0136a38b6f03715cbc2 + checksum: 10c0/a1bcc110aeb49e79a50ae0df41ca692d39e0d992702f7c5b095c969f622eb72636543bed79efb7131fdedaa4c44a6c9c19daf6fca909240acc1f27f79b978c11 languageName: node linkType: hard @@ -4707,35 +4767,34 @@ __metadata: languageName: node linkType: hard -"vitest@npm:^1.6.0": - version: 1.6.0 - resolution: "vitest@npm:1.6.0" - dependencies: - "@vitest/expect": "npm:1.6.0" - "@vitest/runner": "npm:1.6.0" - "@vitest/snapshot": "npm:1.6.0" - "@vitest/spy": "npm:1.6.0" - "@vitest/utils": "npm:1.6.0" - acorn-walk: "npm:^8.3.2" - chai: "npm:^4.3.10" - debug: "npm:^4.3.4" +"vitest@npm:^2.0.3": + version: 2.0.3 + resolution: "vitest@npm:2.0.3" + dependencies: + "@ampproject/remapping": "npm:^2.3.0" + "@vitest/expect": "npm:2.0.3" + "@vitest/pretty-format": "npm:^2.0.3" + "@vitest/runner": "npm:2.0.3" + "@vitest/snapshot": "npm:2.0.3" + "@vitest/spy": "npm:2.0.3" + "@vitest/utils": "npm:2.0.3" + chai: "npm:^5.1.1" + debug: "npm:^4.3.5" execa: "npm:^8.0.1" - local-pkg: "npm:^0.5.0" - magic-string: "npm:^0.30.5" - pathe: "npm:^1.1.1" - picocolors: "npm:^1.0.0" - std-env: "npm:^3.5.0" - strip-literal: "npm:^2.0.0" - tinybench: "npm:^2.5.1" - tinypool: "npm:^0.8.3" + magic-string: "npm:^0.30.10" + pathe: "npm:^1.1.2" + std-env: "npm:^3.7.0" + tinybench: "npm:^2.8.0" + tinypool: "npm:^1.0.0" + tinyrainbow: "npm:^1.2.0" vite: "npm:^5.0.0" - vite-node: "npm:1.6.0" + vite-node: "npm:2.0.3" why-is-node-running: "npm:^2.2.2" peerDependencies: "@edge-runtime/vm": "*" "@types/node": ^18.0.0 || >=20.0.0 - "@vitest/browser": 1.6.0 - "@vitest/ui": 1.6.0 + "@vitest/browser": 2.0.3 + "@vitest/ui": 2.0.3 happy-dom: "*" jsdom: "*" peerDependenciesMeta: @@ -4753,7 +4812,16 @@ __metadata: optional: true bin: vitest: vitest.mjs - checksum: 10c0/065da5b8ead51eb174d93dac0cd50042ca9539856dc25e340ea905d668c41961f7e00df3e388e6c76125b2c22091db2e8465f993d0f6944daf9598d549e562e7 + checksum: 10c0/1801ec31eb144063d14a03d054ff573869732dcaf69abd4fefdabe011d183599a7493e49d8e180b29808675309814421c4a12271fb140c708e7c9f68c4a37a3c + languageName: node + linkType: hard + +"wcwidth@npm:^1.0.1": + version: 1.0.1 + resolution: "wcwidth@npm:1.0.1" + dependencies: + defaults: "npm:^1.0.3" + checksum: 10c0/5b61ca583a95e2dd85d7078400190efd452e05751a64accb8c06ce4db65d7e0b0cde9917d705e826a2e05cc2548f61efde115ffa374c3e436d04be45c889e5b4 languageName: node linkType: hard @@ -4830,13 +4898,6 @@ __metadata: languageName: node linkType: hard -"wrappy@npm:1": - version: 1.0.2 - resolution: "wrappy@npm:1.0.2" - checksum: 10c0/56fece1a4018c6a6c8e28fbc88c87e0fbf4ea8fd64fc6c63b18f4acc4bd13e0ad2515189786dd2c30d3eec9663d70f4ecf699330002f8ccb547e4a18231fc9f0 - languageName: node - linkType: hard - "ws@npm:^8.16.0": version: 8.18.0 resolution: "ws@npm:8.18.0" @@ -4895,13 +4956,6 @@ __metadata: languageName: node linkType: hard -"yocto-queue@npm:^1.0.0": - version: 1.0.0 - resolution: "yocto-queue@npm:1.0.0" - checksum: 10c0/856117aa15cf5103d2a2fb173f0ab4acb12b4b4d0ed3ab249fdbbf612e55d1cadfd27a6110940e24746fb0a78cf640b522cc8bca76f30a3b00b66e90cf82abe0 - languageName: node - linkType: hard - "zlib-sync@npm:^0.1.9": version: 0.1.9 resolution: "zlib-sync@npm:0.1.9"