├── art ├── show.png └── intents.png ├── env.default ├── .gitignore ├── Dockerfile ├── tsconfig.json ├── bin ├── update └── dockerize ├── src ├── bot │ ├── model │ │ ├── id.ts │ │ ├── listener.ts │ │ └── MessageEventType.ts │ ├── discord │ │ └── format.ts │ ├── message │ │ ├── Reaction.ts │ │ ├── validate.ts │ │ ├── Msg.ts │ │ ├── MessageHandler.ts │ │ ├── MessageCache.ts │ │ ├── messages.ts │ │ └── communicate.ts │ ├── logger.ts │ └── index.ts ├── util │ ├── array.ts │ └── api.ts ├── commands │ ├── outputs │ │ ├── dateerror.ts │ │ ├── watch.ts │ │ ├── cancel.ts │ │ ├── availability.ts │ │ ├── help.ts │ │ └── status.ts │ ├── model │ │ ├── ParkCalendarResponse.ts │ │ ├── MagicKeyType.ts │ │ ├── WatchEntry.ts │ │ └── WatchResult.ts │ ├── help.ts │ ├── status.ts │ ├── cancel.ts │ ├── show.ts │ ├── stop.ts │ ├── command.ts │ └── watch.ts ├── config.ts ├── health.ts ├── looper │ ├── WatchAlertMessageCache.ts │ ├── ParkCalendarLooupHandler.ts │ ├── DateParser.ts │ ├── ParkWatchCache.ts │ └── ParkCalendarLookupLooper.ts ├── reactions │ └── stopwatch.ts ├── network │ └── magickeycalendar.ts └── index.ts ├── eslint.config.mjs ├── package.json ├── README.md ├── LICENSE └── pnpm-lock.yaml /art/show.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyamsoft/mousewatch/HEAD/art/show.png -------------------------------------------------------------------------------- /art/intents.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyamsoft/mousewatch/HEAD/art/intents.png -------------------------------------------------------------------------------- /env.default: -------------------------------------------------------------------------------- 1 | BOT_TOKEN=YOUR_TOKEN_HERE 2 | BOT_PREFIX=!mouse 3 | BOT_CHANNEL_ID= 4 | BOT_HEALTHCHECK_URL= 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .idea 3 | 4 | # Config 5 | config.json 6 | .env 7 | .env.* 8 | 9 | dist 10 | yarn-error.log 11 | 12 | 13 | .yarn/* 14 | !.yarn/cache 15 | !.yarn/patches 16 | !.yarn/plugins 17 | !.yarn/releases 18 | !.yarn/sdks 19 | !.yarn/versions 20 | 21 | .pnp* 22 | 23 | # flatpak corepack local 24 | lib 25 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:24-slim 2 | 3 | WORKDIR /mousewatch 4 | 5 | RUN umask 0022 6 | 7 | COPY package.json ./ 8 | COPY pnpm-lock.yaml ./ 9 | COPY eslint.config.mjs ./ 10 | COPY .env.prod ./.env 11 | COPY src ./src 12 | 13 | # Enable corepack 14 | RUN chmod 644 .env && corepack enable 15 | 16 | # build 17 | RUN pnpm install 18 | 19 | # run 20 | CMD [ "pnpm", "start" ] 21 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "lib": ["ESNext"], 5 | "module": "ESNext", 6 | "skipLibCheck": true, 7 | 8 | /* Bundler mode */ 9 | "moduleResolution": "bundler", 10 | "allowImportingTsExtensions": true, 11 | "resolveJsonModule": true, 12 | "isolatedModules": true, 13 | "noEmit": true, 14 | "jsx": "react-jsx", 15 | 16 | /* Linting */ 17 | "strict": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | "noFallthroughCasesInSwitch": true, 21 | "noImplicitAny": true, 22 | "noImplicitOverride": true, 23 | "noImplicitThis": true 24 | }, 25 | "include": ["src"] 26 | } 27 | -------------------------------------------------------------------------------- /bin/update: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright 2025 pyamsoft 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | exec ./node_modules/.bin/ncu -u || exit 1 20 | exit 0 21 | -------------------------------------------------------------------------------- /src/bot/model/id.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 pyamsoft 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | export const generateRandomId = function (): string { 18 | return crypto.randomUUID().toString(); 19 | }; 20 | -------------------------------------------------------------------------------- /src/util/array.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 pyamsoft 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | export const ensureArray = function (oneOrMany: T | T[]): T[] { 18 | let list; 19 | if (Array.isArray(oneOrMany)) { 20 | list = oneOrMany; 21 | } else { 22 | list = [oneOrMany]; 23 | } 24 | return list; 25 | }; 26 | -------------------------------------------------------------------------------- /src/util/api.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 pyamsoft 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | export const jsonApi = async function ( 18 | url: string, 19 | headers?: RequestInit["headers"], 20 | ): Promise { 21 | return fetch(url, { 22 | method: "GET", 23 | headers, 24 | }).then((r) => r.json() as T); 25 | }; 26 | -------------------------------------------------------------------------------- /src/bot/model/listener.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 pyamsoft 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | export interface Listener { 18 | stop: () => void; 19 | } 20 | 21 | export const newListener = function (stopListening: () => void): Listener { 22 | return Object.freeze({ 23 | stop: function () { 24 | stopListening(); 25 | }, 26 | }); 27 | }; 28 | -------------------------------------------------------------------------------- /bin/dockerize: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright 2025 pyamsoft 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | _cmd="sudo docker" 20 | if command -v podman > /dev/null; then 21 | _cmd="podman" 22 | fi 23 | 24 | # shellcheck disable=SC2086 25 | exec ${_cmd} build . \ 26 | -t pyamsoft/mousewatch \ 27 | -f ./Dockerfile \ 28 | --rm \ 29 | --layers \ 30 | --force-rm \ 31 | "$@" 32 | -------------------------------------------------------------------------------- /src/bot/model/MessageEventType.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 pyamsoft 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | export type MessageEventType = 18 | | MessageEventTypes.CREATE 19 | | MessageEventTypes.UPDATE 20 | | MessageEventTypes.REACTION; 21 | 22 | export enum MessageEventTypes { 23 | CREATE = "messageCreate", 24 | UPDATE = "messageUpdate", 25 | REACTION = "messageReactionAdd", 26 | } 27 | -------------------------------------------------------------------------------- /src/bot/discord/format.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 pyamsoft 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | export const codeBlock = function (message: string): string { 18 | return `\`\`\`${message}\`\`\``; 19 | }; 20 | 21 | export const bold = function (message: string): string { 22 | return `**${message}**`; 23 | }; 24 | 25 | export const italic = function (message: string): string { 26 | return `*${message}*`; 27 | }; 28 | -------------------------------------------------------------------------------- /src/commands/outputs/dateerror.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 pyamsoft 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { bold } from "../../bot/discord/format"; 18 | 19 | export const outputDateErrorText = function (dateString: string): string { 20 | return `:cry: Unable to figure out what date you want: "${bold(dateString)}"`; 21 | }; 22 | 23 | export const outputDateMissingText = function (): string { 24 | return `:cry: You need to tell me a Date`; 25 | }; 26 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 pyamsoft 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import tseslint from "typescript-eslint"; 18 | import js from "@eslint/js"; 19 | import globals from "globals"; 20 | 21 | export default tseslint.config( 22 | { ignores: ["dist"] }, 23 | { 24 | extends: [js.configs.recommended, ...tseslint.configs.recommended], 25 | files: ["**/*.{ts,tsx}"], 26 | languageOptions: { 27 | ecmaVersion: 2020, 28 | globals: globals.node, 29 | }, 30 | }, 31 | ); 32 | -------------------------------------------------------------------------------- /src/commands/model/ParkCalendarResponse.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 pyamsoft 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { DateTime } from "luxon"; 18 | 19 | export interface ParkCalendarResponse { 20 | objectType: "ParkCalendarResponse"; 21 | 22 | /** 23 | * Raw JSON 24 | */ 25 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 26 | json: any; 27 | 28 | /** 29 | * Date 30 | */ 31 | date: DateTime; 32 | 33 | /** 34 | * Is the day available 35 | */ 36 | available: boolean; 37 | } 38 | -------------------------------------------------------------------------------- /src/commands/outputs/watch.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 pyamsoft 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { DateTime } from "luxon"; 18 | import { bold, italic } from "../../bot/discord/format"; 19 | import { magicKeyName, MagicKeyType } from "../model/MagicKeyType"; 20 | 21 | export const outputWatchStarted = function ( 22 | magicKey: MagicKeyType, 23 | date: DateTime, 24 | ): string { 25 | return `:thumbsup: Watching ${italic( 26 | magicKeyName(magicKey), 27 | )} reservations on ${bold( 28 | date.toLocaleString(DateTime.DATE_MED_WITH_WEEKDAY), 29 | )}`; 30 | }; 31 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mousewatch", 3 | "version": "2.3.1", 4 | "main": "index.js", 5 | "license": "Apache-2.0", 6 | "scripts": { 7 | "start": "vite-node ./src/index.js", 8 | "lint:eslint": "eslint --report-unused-disable-directives --max-warnings 0 .", 9 | "lint:tsc": "tsc --noEmit", 10 | "lint": "$npm_execpath run lint:eslint && $npm_execpath run lint:tsc", 11 | "format": "prettier --write ." 12 | }, 13 | "engines": { 14 | "node": ">=24.x" 15 | }, 16 | "dependencies": { 17 | "discord.js": "14.25.1", 18 | "dotenv": "17.2.3", 19 | "luxon": "3.7.2" 20 | }, 21 | "devDependencies": { 22 | "@eslint/eslintrc": "3.3.1", 23 | "@eslint/js": "9.39.1", 24 | "@types/luxon": "3.7.1", 25 | "@types/node": "24.10.1", 26 | "@typescript-eslint/eslint-plugin": "8.47.0", 27 | "@typescript-eslint/parser": "8.47.0", 28 | "eslint": "9.39.1", 29 | "globals": "16.5.0", 30 | "npm-check-updates": "19.1.2", 31 | "prettier": "3.6.2", 32 | "tslib": "2.8.1", 33 | "typescript": "5.9.3", 34 | "typescript-eslint": "8.47.0", 35 | "vite-node": "5.2.0" 36 | }, 37 | "packageManager": "pnpm@10.23.0" 38 | } 39 | -------------------------------------------------------------------------------- /src/bot/message/Reaction.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 pyamsoft 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { Message, MessageReaction, PartialMessage } from "discord.js"; 18 | 19 | export interface ReactionChannel { 20 | /** 21 | * @param emoji - either a custom emoji.id or a unicode emoji.name 22 | */ 23 | react: (emoji: string) => Promise; 24 | } 25 | 26 | export const reactionChannelFromMessage = function ( 27 | message: Message | PartialMessage, 28 | ): ReactionChannel { 29 | return { 30 | react: function (emoji: string) { 31 | return message.react(emoji); 32 | }, 33 | }; 34 | }; 35 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 pyamsoft 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { newLogger } from "./bot/logger"; 18 | import { configDotenv } from "dotenv"; 19 | 20 | const logger = newLogger("BotConfig"); 21 | 22 | export interface BotConfig { 23 | prefix: string; 24 | token: string; 25 | targetedChannels: ReadonlyArray; 26 | healthCheckUrl: string; 27 | } 28 | 29 | export const sourceConfig = function (): BotConfig { 30 | configDotenv(); 31 | 32 | const rawSpecificChannel = process.env.BOT_TARGET_CHANNEL_IDS || ""; 33 | const config: BotConfig = Object.freeze({ 34 | prefix: process.env.BOT_PREFIX || "$", 35 | token: process.env.BOT_TOKEN || "", 36 | healthCheckUrl: process.env.BOT_HEALTHCHECK_URL || "", 37 | targetedChannels: rawSpecificChannel 38 | .split(",") 39 | .map((s) => s.trim()) 40 | .filter((s) => s), 41 | }); 42 | logger.log("Bot Config: ", config); 43 | return config; 44 | }; 45 | -------------------------------------------------------------------------------- /src/health.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 pyamsoft 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { newLogger } from "./bot/logger"; 18 | 19 | const logger = newLogger("HealthCheck"); 20 | 21 | const fireHealthCheck = function (url: string) { 22 | logger.log(`Attempt healthcheck: ${url}`); 23 | fetch(url, { 24 | method: "GET", 25 | }) 26 | .then(() => { 27 | logger.log(`Healthcheck success!`); 28 | }) 29 | .catch((e) => { 30 | logger.error(`Healthcheck failed!`, e); 31 | }); 32 | }; 33 | 34 | export const registerPeriodicHealthCheck = function (url: string) { 35 | let timer: NodeJS.Timeout | undefined = undefined; 36 | 37 | if (url) { 38 | timer = setInterval(() => { 39 | fireHealthCheck(url); 40 | }, 60 * 1000); 41 | 42 | fireHealthCheck(url); 43 | } 44 | 45 | return { 46 | unregister: function () { 47 | if (timer) { 48 | clearInterval(timer); 49 | timer = undefined; 50 | } 51 | }, 52 | }; 53 | }; 54 | -------------------------------------------------------------------------------- /src/commands/help.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 pyamsoft 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { newLogger } from "../bot/logger"; 18 | import { 19 | messageHandlerHelpText, 20 | newMessageHandler, 21 | } from "../bot/message/MessageHandler"; 22 | import { Msg } from "../bot/message/Msg"; 23 | import { BotConfig } from "../config"; 24 | import { ParkCommand } from "./command"; 25 | import { outputHelpText } from "./outputs/help"; 26 | 27 | const TAG = "HelpHandler"; 28 | const logger = newLogger(TAG); 29 | 30 | export const HelpHandler = newMessageHandler( 31 | TAG, 32 | function ( 33 | config: BotConfig, 34 | command: { 35 | currentCommand: ParkCommand; 36 | oldCommand?: ParkCommand; 37 | message: Msg; 38 | }, 39 | ) { 40 | // Only handle help 41 | const { currentCommand } = command; 42 | if (!currentCommand.isHelpCommand) { 43 | return; 44 | } 45 | 46 | logger.log("Handle help message", currentCommand); 47 | return Promise.resolve(messageHandlerHelpText(outputHelpText(config))); 48 | }, 49 | ); 50 | -------------------------------------------------------------------------------- /src/commands/model/MagicKeyType.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 pyamsoft 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | export enum MagicKeyType { 18 | DREAM = "dream-key-pass", 19 | BELIEVE = "believe-key-pass", 20 | ENCHANT = "enchant-key-pass", 21 | IMAGINE = "imagine-key-pass", 22 | INSPIRE = "inspire-key-pass", 23 | NONE = "", 24 | } 25 | 26 | export const allMagicKeys = function (): MagicKeyType[] { 27 | return [ 28 | MagicKeyType.DREAM, 29 | MagicKeyType.BELIEVE, 30 | MagicKeyType.ENCHANT, 31 | MagicKeyType.IMAGINE, 32 | MagicKeyType.INSPIRE, 33 | ]; 34 | }; 35 | 36 | export const magicKeyName = function (key: MagicKeyType): string { 37 | switch (key) { 38 | case MagicKeyType.BELIEVE: 39 | return "Believe Key"; 40 | case MagicKeyType.DREAM: 41 | return "Dream Key"; 42 | case MagicKeyType.INSPIRE: 43 | return "Inspire Key"; 44 | case MagicKeyType.ENCHANT: 45 | return "Enchant Key"; 46 | case MagicKeyType.IMAGINE: 47 | return "Imagine Key"; 48 | default: 49 | throw new Error(`Invalid MagicKey type: ${key}`); 50 | } 51 | }; 52 | -------------------------------------------------------------------------------- /src/bot/logger.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 pyamsoft 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | const isDebug = process.env.BOT_ENV !== "production"; 18 | 19 | export interface Logger { 20 | print: (...args: unknown[]) => void; 21 | log: (...args: unknown[]) => void; 22 | warn: (...args: unknown[]) => void; 23 | error: (...args: unknown[]) => void; 24 | } 25 | 26 | const logTag = function (prefix: string): string { 27 | return `<${prefix}>`; 28 | }; 29 | 30 | export const newLogger = function (prefix: string): Logger { 31 | const tag = prefix ? logTag(prefix) : ""; 32 | return { 33 | print: function print(...args: unknown[]) { 34 | console.log(tag, ...args); 35 | }, 36 | 37 | log: function log(...args: unknown[]) { 38 | if (isDebug) { 39 | this.print(...args); 40 | } 41 | }, 42 | 43 | warn: function warn(...args: unknown[]) { 44 | if (isDebug) { 45 | console.warn(tag, ...args); 46 | } 47 | }, 48 | 49 | error: function error(e, ...args: unknown[]) { 50 | if (isDebug) { 51 | console.error(tag, e, ...args); 52 | } 53 | }, 54 | }; 55 | }; 56 | -------------------------------------------------------------------------------- /src/commands/status.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 pyamsoft 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { newLogger } from "../bot/logger"; 18 | import { 19 | messageHandlerHelpText, 20 | newMessageHandler, 21 | } from "../bot/message/MessageHandler"; 22 | import { Msg } from "../bot/message/Msg"; 23 | import { BotConfig } from "../config"; 24 | import { ParkCommand, ParkCommandType } from "./command"; 25 | import { outputStatusText } from "./outputs/status"; 26 | 27 | const TAG = "StatusHandler"; 28 | const logger = newLogger(TAG); 29 | 30 | export const StatusHandler = newMessageHandler( 31 | TAG, 32 | function ( 33 | config: BotConfig, 34 | command: { 35 | currentCommand: ParkCommand; 36 | oldCommand?: ParkCommand; 37 | message: Msg; 38 | }, 39 | ) { 40 | // Only handle status 41 | const { currentCommand } = command; 42 | if ( 43 | currentCommand.isHelpCommand || 44 | currentCommand.type !== ParkCommandType.STATUS 45 | ) { 46 | return; 47 | } 48 | 49 | logger.log("Handle status message", currentCommand); 50 | return outputStatusText(config).then((text) => 51 | messageHandlerHelpText(text), 52 | ); 53 | }, 54 | ); 55 | -------------------------------------------------------------------------------- /src/commands/model/WatchEntry.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 pyamsoft 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { DateTime } from "luxon"; 18 | import { Msg } from "../../bot/message/Msg"; 19 | import { MagicKeyType } from "./MagicKeyType"; 20 | 21 | export interface WatchEntry { 22 | objectType: "WatchEntry"; 23 | 24 | userId: string; 25 | userName: string; 26 | messageId: string; 27 | channelId: string; 28 | 29 | // Info 30 | magicKey: MagicKeyType; 31 | targetDate: DateTime; 32 | } 33 | 34 | export const createWatchEntry = function (data: { 35 | userId: string; 36 | userName: string; 37 | messageId: string; 38 | channelId: string; 39 | 40 | // Info 41 | magicKey: MagicKeyType; 42 | targetDate: DateTime; 43 | }): WatchEntry { 44 | return { 45 | objectType: "WatchEntry", 46 | ...data, 47 | }; 48 | }; 49 | 50 | export const watchEntryFromMessage = function (data: { 51 | message: Msg; 52 | magicKey: MagicKeyType; 53 | targetDate: DateTime; 54 | }): WatchEntry { 55 | const { message, ...rest } = data; 56 | return createWatchEntry({ 57 | ...rest, 58 | userId: message.author.id, 59 | userName: message.author.username, 60 | messageId: message.id, 61 | channelId: message.channel.id, 62 | }); 63 | }; 64 | -------------------------------------------------------------------------------- /src/looper/WatchAlertMessageCache.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 pyamsoft 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { WatchResult } from "../commands/model/WatchResult"; 18 | 19 | const cache: Record = {}; 20 | 21 | export const WatchAlertMessageCache = { 22 | cacheAlert: function (messageId: string, result: WatchResult) { 23 | cache[messageId] = result; 24 | }, 25 | 26 | getCachedAlert: function (messageId: string): WatchResult | undefined { 27 | return cache[messageId]; 28 | }, 29 | 30 | removeCachedAlert: function (messageId: string) { 31 | const cached = cache[messageId]; 32 | cache[messageId] = undefined; 33 | 34 | // If we have removed a cached message for a result, go through the cache and remove all other results that 35 | // look like it 36 | if (cached) { 37 | const removeOthers: string[] = []; 38 | for (const key of Object.keys(cached)) { 39 | const value = cache[key]; 40 | if (value) { 41 | if ( 42 | value.userId === cached.userId && 43 | value.magicKey === cached.magicKey && 44 | value.targetDate.valueOf() === cached.targetDate.valueOf() 45 | ) { 46 | removeOthers.push(key); 47 | } 48 | } 49 | } 50 | 51 | for (const removeKey of removeOthers) { 52 | cache[removeKey] = undefined; 53 | } 54 | } 55 | }, 56 | }; 57 | -------------------------------------------------------------------------------- /src/commands/outputs/cancel.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 pyamsoft 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { DateTime } from "luxon"; 18 | import { bold, italic } from "../../bot/discord/format"; 19 | import { magicKeyName, MagicKeyType } from "../model/MagicKeyType"; 20 | 21 | export const outputStopWatch = function ( 22 | magicKey: MagicKeyType, 23 | date: DateTime, 24 | ): string { 25 | return `:negative_squared_cross_mark: Stopped watching ${italic( 26 | magicKeyName(magicKey), 27 | )} reservations on ${bold( 28 | date.toLocaleString(DateTime.DATE_MED_WITH_WEEKDAY), 29 | )}`; 30 | }; 31 | 32 | export const outputClearWatch = function ( 33 | magicKey: MagicKeyType, 34 | userName: string, 35 | ): string { 36 | return `:negative_squared_cross_mark: Stopped watching ${italic( 37 | magicKeyName(magicKey), 38 | )} reservations for ${userName}`; 39 | }; 40 | 41 | export const outputCancelFailed = function ( 42 | magicKey: MagicKeyType, 43 | date: DateTime, 44 | ): string { 45 | return `:x: Unable to stop watching ${italic( 46 | magicKeyName(magicKey), 47 | )} reservations on ${bold( 48 | date.toLocaleString(DateTime.DATE_MED_WITH_WEEKDAY), 49 | )}`; 50 | }; 51 | 52 | export const outputClearFailed = function ( 53 | magicKey: MagicKeyType, 54 | userName: string, 55 | ): string { 56 | return `:x: Unable to stop watching ${italic( 57 | magicKeyName(magicKey), 58 | )} reservations for ${userName}`; 59 | }; 60 | -------------------------------------------------------------------------------- /src/looper/ParkCalendarLooupHandler.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 pyamsoft 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { DateTime } from "luxon"; 18 | import { newLogger } from "../bot/logger"; 19 | import { MagicKeyType } from "../commands/model/MagicKeyType"; 20 | import { 21 | createLookupResult, 22 | LookupResult, 23 | } from "../commands/model/WatchResult"; 24 | import { MagicKeyCalendarApi } from "../network/magickeycalendar"; 25 | 26 | const logger = newLogger("ParkCalendarLookupHandler"); 27 | 28 | export const ParkCalendarLookupHandler = { 29 | lookup: function ( 30 | magicKey: MagicKeyType, 31 | dates: ReadonlyArray, 32 | ): Promise> { 33 | return new Promise((resolve, reject) => { 34 | const result: LookupResult[] = []; 35 | MagicKeyCalendarApi.getCalendar(magicKey) 36 | .then((response) => { 37 | for (const res of response) { 38 | for (const date of dates) { 39 | if (date.valueOf() === res.date.valueOf()) { 40 | result.push(createLookupResult(magicKey, date, res)); 41 | } 42 | } 43 | } 44 | 45 | resolve(result); 46 | }) 47 | .catch((e) => { 48 | logger.error(e, "Unable to lookup magic key calendar: ", { 49 | magicKey, 50 | dates, 51 | }); 52 | reject(e); 53 | }); 54 | return result; 55 | }); 56 | }, 57 | }; 58 | -------------------------------------------------------------------------------- /src/commands/outputs/availability.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 pyamsoft 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { DateTime } from "luxon"; 18 | import { bold, italic } from "../../bot/discord/format"; 19 | import { magicKeyName, MagicKeyType } from "../model/MagicKeyType"; 20 | import { BaseResult } from "../model/WatchResult"; 21 | 22 | const RESERVE_LINK = "https://disneyland.disney.go.com/entry-reservation/"; 23 | 24 | export const outputParkAvailability = function ( 25 | userId: string | undefined, 26 | result: BaseResult, 27 | ): string { 28 | const { parkResponse, magicKey } = result; 29 | 30 | const link = parkResponse.available ? `\n${RESERVE_LINK.trim()}` : ""; 31 | const stopWatching = userId 32 | ? `\n(React to this message with an emoji to stop watching, otherwise I will assume you did not get a reservation spot, and will keep watching.)` 33 | : ""; 34 | 35 | return `${userId ? `<@${userId}> ` : ""}${bold( 36 | parkResponse.date.toLocaleString(DateTime.DATE_MED_WITH_WEEKDAY), 37 | )}: ${italic(magicKeyName(magicKey))} reservations are ${bold( 38 | parkResponse.available ? "AVAILABLE" : "BLOCKED", 39 | )}${link}${stopWatching}`; 40 | }; 41 | 42 | export const outputParkUnknown = function ( 43 | magicKey: MagicKeyType, 44 | date: DateTime, 45 | ): string { 46 | return `${bold( 47 | date.toLocaleString(DateTime.DATE_MED_WITH_WEEKDAY), 48 | )}: ${italic(magicKeyName(magicKey))} reservations are ${bold("UNKNOWN")}`; 49 | }; 50 | -------------------------------------------------------------------------------- /src/commands/model/WatchResult.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 pyamsoft 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { DateTime } from "luxon"; 18 | import { MagicKeyType } from "./MagicKeyType"; 19 | import { ParkCalendarResponse } from "./ParkCalendarResponse"; 20 | import { WatchEntry } from "./WatchEntry"; 21 | 22 | export interface BaseResult { 23 | magicKey: MagicKeyType; 24 | targetDate: DateTime; 25 | parkResponse: ParkCalendarResponse; 26 | } 27 | 28 | export interface LookupResult extends BaseResult { 29 | objectType: "LookupResult"; 30 | } 31 | 32 | export interface WatchResult extends BaseResult { 33 | objectType: "WatchResult"; 34 | userId: string; 35 | userName: string; 36 | messageId: string; 37 | channelId: string; 38 | } 39 | 40 | export const createLookupResult = function ( 41 | magicKey: MagicKeyType, 42 | date: DateTime, 43 | response: ParkCalendarResponse, 44 | ): LookupResult { 45 | return { 46 | objectType: "LookupResult", 47 | 48 | magicKey: magicKey, 49 | targetDate: date, 50 | parkResponse: response, 51 | }; 52 | }; 53 | 54 | export const createResultFromEntry = function ( 55 | entry: WatchEntry, 56 | response: ParkCalendarResponse, 57 | ): WatchResult { 58 | return { 59 | objectType: "WatchResult", 60 | userId: entry.userId, 61 | userName: entry.userName, 62 | messageId: entry.messageId, 63 | channelId: entry.channelId, 64 | 65 | magicKey: entry.magicKey, 66 | targetDate: entry.targetDate, 67 | 68 | parkResponse: response, 69 | }; 70 | }; 71 | -------------------------------------------------------------------------------- /src/commands/outputs/help.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 pyamsoft 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { BotConfig } from "../../config"; 18 | import { codeBlock } from "../../bot/discord/format"; 19 | 20 | export const outputHelpText = function (config: BotConfig): string { 21 | const { prefix } = config; 22 | 23 | return codeBlock(` 24 | Beep Boop. 25 | 26 | [COMMANDS] 27 | ${prefix} This help 28 | ${prefix} help This help 29 | ${prefix} status Prints the status of the bot 30 | 31 | ${prefix} show Show park availability for a DATE 32 | ${prefix} watch Watch park availability for a DATE 33 | ${prefix} stop Stop watching a DATE 34 | ${prefix} cancel Cancel all pending watch commands 35 | 36 | [NOTES] 37 | DATE must look like MM/DD/YYYY 38 | 39 | 01/12/2023 40 | 12/14/2022 41 | 12/06/2023 42 | 43 | [EXAMPLE] 44 | 45 | me > ${prefix} show 01/24/2023 46 | 47 | bot > Tue, Jan 24, 2023: Inspire Key 48 | reservations are AVAILABLE 49 | https://disneyland.disney.go.com/entry-reservation/ 50 | 51 | me > ${prefix} watch 01/04/2021 52 | 53 | bot > :thumbsup: Watching Inspire Key 54 | reservations on Sun, Dec 18, 2022 55 | 56 | * A few minutes later... * 57 | 58 | bot > @You Sun, Dec 18, 2022: Inspire Key 59 | reservations are AVAILABLE 60 | https://disneyland.disney.go.com/entry-reservation/ 61 | 62 | (React to this message with an emoji to stop 63 | watching, otherwise I will assume you did 64 | not get a reservation spot, and will keep 65 | watching.) 66 | `); 67 | }; 68 | -------------------------------------------------------------------------------- /src/looper/DateParser.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 pyamsoft 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { DateTime, SystemZone } from "luxon"; 18 | import { newLogger } from "../bot/logger"; 19 | 20 | const logger = newLogger("DateParser"); 21 | 22 | const options = { zone: SystemZone.instance }; 23 | 24 | const parseRawDate = function (content: string): DateTime | undefined { 25 | try { 26 | let date = DateTime.fromISO(content, options); 27 | if (date.isValid) { 28 | return date; 29 | } 30 | 31 | date = DateTime.fromHTTP(content, options); 32 | if (date.isValid) { 33 | return date; 34 | } 35 | 36 | date = DateTime.fromRFC2822(content, options); 37 | if (date.isValid) { 38 | return date; 39 | } 40 | 41 | date = DateTime.fromFormat(content, "MM/dd/yyyy", options); 42 | if (date.isValid) { 43 | return date; 44 | } 45 | 46 | date = DateTime.fromFormat(content, "MM-dd-yyyy", options); 47 | if (date.isValid) { 48 | return date; 49 | } 50 | 51 | date = DateTime.fromFormat(content, "MM dd yyyy", options); 52 | if (date.isValid) { 53 | return date; 54 | } 55 | 56 | return undefined; 57 | } catch (e) { 58 | logger.error(e, "Unable to parse date: ", content); 59 | return undefined; 60 | } 61 | }; 62 | 63 | export const cleanDate = function (date: DateTime): DateTime { 64 | return date.set({ 65 | hour: 0, 66 | minute: 0, 67 | second: 0, 68 | millisecond: 0, 69 | }); 70 | }; 71 | 72 | export const parseDate = function (content: string): DateTime | undefined { 73 | const date = parseRawDate(content); 74 | if (!date) { 75 | return undefined; 76 | } 77 | 78 | return cleanDate(date); 79 | }; 80 | -------------------------------------------------------------------------------- /src/commands/cancel.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 pyamsoft 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { newLogger } from "../bot/logger"; 18 | import { 19 | messageHandlerOutput, 20 | newMessageHandler, 21 | } from "../bot/message/MessageHandler"; 22 | import { Msg } from "../bot/message/Msg"; 23 | import { BotConfig } from "../config"; 24 | import { ParkCalendarLookupLooper } from "../looper/ParkCalendarLookupLooper"; 25 | import { ParkWatchCache } from "../looper/ParkWatchCache"; 26 | import { ParkCommand, ParkCommandType } from "./command"; 27 | import { outputClearFailed, outputClearWatch } from "./outputs/cancel"; 28 | 29 | const TAG = "CancelHandler"; 30 | const logger = newLogger(TAG); 31 | 32 | export const CancelHandler = newMessageHandler( 33 | TAG, 34 | function ( 35 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 36 | // @ts-ignore 37 | config: BotConfig, 38 | command: { 39 | currentCommand: ParkCommand; 40 | oldCommand?: ParkCommand; 41 | message: Msg; 42 | }, 43 | ) { 44 | // Only handle status 45 | const { currentCommand, message } = command; 46 | if ( 47 | currentCommand.isHelpCommand || 48 | currentCommand.type !== ParkCommandType.CANCEL 49 | ) { 50 | return; 51 | } 52 | 53 | logger.log("Handle cancel message", currentCommand); 54 | const { magicKey } = currentCommand; 55 | 56 | return new Promise((resolve) => { 57 | const { author } = message; 58 | const userId = author.id; 59 | const userName = author.username; 60 | 61 | const outputs: Record = {}; 62 | if (ParkWatchCache.clearWatches(userId, magicKey)) { 63 | outputs[userId] = outputClearWatch(magicKey, userName); 64 | } else { 65 | outputs[userId] = outputClearFailed(magicKey, userName); 66 | } 67 | 68 | // If removing causes us no more watches, then stop the looper 69 | if (ParkWatchCache.targetCalendars().length <= 0) { 70 | ParkCalendarLookupLooper.stop(); 71 | } 72 | 73 | resolve(messageHandlerOutput(outputs)); 74 | }); 75 | }, 76 | ); 77 | -------------------------------------------------------------------------------- /src/commands/show.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 pyamsoft 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { newLogger } from "../bot/logger"; 18 | import { 19 | messageHandlerOutput, 20 | newMessageHandler, 21 | } from "../bot/message/MessageHandler"; 22 | import { Msg } from "../bot/message/Msg"; 23 | import { BotConfig } from "../config"; 24 | import { ParkCalendarLookupHandler } from "../looper/ParkCalendarLooupHandler"; 25 | import { ParkCommand, ParkCommandType, parseCommandDates } from "./command"; 26 | import { 27 | outputParkAvailability, 28 | outputParkUnknown, 29 | } from "./outputs/availability"; 30 | 31 | const TAG = "ShowHandler"; 32 | const logger = newLogger(TAG); 33 | 34 | export const ShowHandler = newMessageHandler( 35 | TAG, 36 | function ( 37 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 38 | // @ts-ignore 39 | config: BotConfig, 40 | command: { 41 | currentCommand: ParkCommand; 42 | oldCommand?: ParkCommand; 43 | message: Msg; 44 | }, 45 | ) { 46 | // Only handle status 47 | const { currentCommand } = command; 48 | if ( 49 | currentCommand.isHelpCommand || 50 | currentCommand.type !== ParkCommandType.SHOW 51 | ) { 52 | return; 53 | } 54 | 55 | logger.log("Handle show message", currentCommand); 56 | const { magicKey } = currentCommand; 57 | const { dateList, error } = parseCommandDates(currentCommand); 58 | 59 | if (error) { 60 | return error; 61 | } 62 | 63 | return ParkCalendarLookupHandler.lookup(magicKey, dateList).then( 64 | (results) => { 65 | const messages: Record = {}; 66 | for (const d of dateList) { 67 | const res = results.find((r) => d === r.targetDate); 68 | const key = d.toISO(); 69 | if (!key) { 70 | continue; 71 | } 72 | 73 | if (res) { 74 | messages[key] = outputParkAvailability(undefined, res); 75 | } else { 76 | messages[key] = outputParkUnknown(magicKey, d); 77 | } 78 | } 79 | 80 | return messageHandlerOutput(messages); 81 | }, 82 | ); 83 | }, 84 | ); 85 | -------------------------------------------------------------------------------- /src/commands/outputs/status.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 pyamsoft 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { DateTime } from "luxon"; 18 | import { codeBlock } from "../../bot/discord/format"; 19 | import { BotConfig } from "../../config"; 20 | import { ParkWatchCache } from "../../looper/ParkWatchCache"; 21 | import { allMagicKeys, magicKeyName } from "../model/MagicKeyType"; 22 | 23 | interface MagicKeyPayload { 24 | userId: string; 25 | userName: string; 26 | dates: DateTime[]; 27 | } 28 | 29 | export const outputStatusText = function (config: BotConfig): Promise { 30 | const { prefix } = config; 31 | 32 | return new Promise((resolve) => { 33 | const statusBlocks: Record = {}; 34 | for (const magicKey of allMagicKeys()) { 35 | const entries = ParkWatchCache.magicKeyWatches(magicKey); 36 | const keyName = magicKeyName(magicKey); 37 | for (const entry of entries) { 38 | if (!statusBlocks[keyName]) { 39 | statusBlocks[keyName] = []; 40 | } 41 | 42 | const userBlock = statusBlocks[keyName].find( 43 | (b) => b.userId === entry.userId, 44 | ); 45 | if (userBlock) { 46 | userBlock.dates.push(entry.targetDate); 47 | } else { 48 | statusBlocks[keyName].push({ 49 | userId: entry.userId, 50 | userName: entry.userName, 51 | dates: [entry.targetDate], 52 | }); 53 | } 54 | } 55 | } 56 | 57 | let textBlock = ""; 58 | const keyList = Object.keys(statusBlocks); 59 | if (keyList.length <= 0) { 60 | textBlock = "No active Watch loops"; 61 | } else { 62 | for (const keyName of keyList) { 63 | const userBlocks = statusBlocks[keyName]; 64 | textBlock += `${keyName}\n`; 65 | for (const block of userBlocks) { 66 | textBlock += ` ${block.userName}: ${block.dates 67 | // Use DATE_SHORT so user knows the format we expect 68 | .map((d) => d.toLocaleString(DateTime.DATE_SHORT)) 69 | .join(", ")}`; 70 | } 71 | textBlock += `\n\n`; 72 | } 73 | } 74 | 75 | resolve( 76 | codeBlock(` 77 | COMMAND: ${prefix} 78 | =========================== 79 | ${textBlock.trim()} 80 | `), 81 | ); 82 | }); 83 | }; 84 | -------------------------------------------------------------------------------- /src/commands/stop.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 pyamsoft 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { newLogger } from "../bot/logger"; 18 | import { 19 | messageHandlerOutput, 20 | newMessageHandler, 21 | } from "../bot/message/MessageHandler"; 22 | import { Msg } from "../bot/message/Msg"; 23 | import { BotConfig } from "../config"; 24 | import { ParkCalendarLookupLooper } from "../looper/ParkCalendarLookupLooper"; 25 | import { ParkWatchCache } from "../looper/ParkWatchCache"; 26 | import { ParkCommand, ParkCommandType, parseCommandDates } from "./command"; 27 | import { outputCancelFailed, outputStopWatch } from "./outputs/cancel"; 28 | 29 | const TAG = "StopHandler"; 30 | const logger = newLogger(TAG); 31 | 32 | export const StopHandler = newMessageHandler( 33 | TAG, 34 | function ( 35 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 36 | // @ts-ignore 37 | config: BotConfig, 38 | command: { 39 | currentCommand: ParkCommand; 40 | oldCommand?: ParkCommand; 41 | message: Msg; 42 | }, 43 | ) { 44 | // Only handle status 45 | const { currentCommand, message } = command; 46 | if ( 47 | currentCommand.isHelpCommand || 48 | currentCommand.type !== ParkCommandType.STOP 49 | ) { 50 | return; 51 | } 52 | 53 | logger.log("Handle stop message", currentCommand); 54 | const { magicKey } = currentCommand; 55 | const { dateList, error } = parseCommandDates(currentCommand); 56 | 57 | if (error) { 58 | return error; 59 | } 60 | 61 | return new Promise((resolve) => { 62 | const { author } = message; 63 | const userId = author.id; 64 | 65 | const outputs: Record = {}; 66 | for (const d of dateList) { 67 | const key = d.toISO(); 68 | if (!key) { 69 | continue; 70 | } 71 | 72 | if (ParkWatchCache.removeWatch(userId, magicKey, d)) { 73 | outputs[key] = outputStopWatch(magicKey, d); 74 | } else { 75 | outputs[key] = outputCancelFailed(magicKey, d); 76 | } 77 | } 78 | 79 | // If removing causes us no more watches, then stop the looper 80 | if (ParkWatchCache.targetCalendars().length <= 0) { 81 | ParkCalendarLookupLooper.stop(); 82 | } 83 | 84 | resolve(messageHandlerOutput(outputs)); 85 | }); 86 | }, 87 | ); 88 | -------------------------------------------------------------------------------- /src/looper/ParkWatchCache.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 pyamsoft 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { DateTime } from "luxon"; 18 | import { MagicKeyType } from "../commands/model/MagicKeyType"; 19 | import { WatchEntry } from "../commands/model/WatchEntry"; 20 | 21 | const cache = new Set(); 22 | 23 | const removeWatches = function ( 24 | userId: string, 25 | magicKey: MagicKeyType, 26 | optionalDate: DateTime | undefined, 27 | ): boolean { 28 | const oldSize = cache.size; 29 | const deleteMe: WatchEntry[] = []; 30 | 31 | cache.forEach((e) => { 32 | if (e.userId === userId && e.magicKey === magicKey) { 33 | if (!optionalDate || optionalDate.valueOf() === e.targetDate.valueOf()) { 34 | deleteMe.push(e); 35 | } 36 | } 37 | }); 38 | 39 | for (const d of deleteMe) { 40 | cache.delete(d); 41 | } 42 | 43 | return oldSize !== cache.size; 44 | }; 45 | 46 | export const ParkWatchCache = { 47 | /** 48 | * Compile a list of the MagicKeys we need to watch 49 | */ 50 | targetCalendars: function (): MagicKeyType[] { 51 | const magicKeys: MagicKeyType[] = []; 52 | 53 | cache.forEach((entry) => { 54 | if (!magicKeys.includes(entry.magicKey)) { 55 | magicKeys.push(entry.magicKey); 56 | } 57 | }); 58 | 59 | return magicKeys; 60 | }, 61 | 62 | magicKeyWatches: function (magicKey: MagicKeyType): WatchEntry[] { 63 | const entries: WatchEntry[] = []; 64 | 65 | cache.forEach((entry) => { 66 | if (entry.magicKey === magicKey) { 67 | entries.push(entry); 68 | } 69 | }); 70 | 71 | return entries; 72 | }, 73 | 74 | addWatch: function (entry: WatchEntry): boolean { 75 | let dupe = false; 76 | 77 | cache.forEach((e) => { 78 | if ( 79 | e.userId === entry.userId && 80 | e.magicKey === entry.magicKey && 81 | e.targetDate.valueOf() === entry.targetDate.valueOf() 82 | ) { 83 | dupe = true; 84 | } 85 | }); 86 | 87 | if (!dupe) { 88 | cache.add(entry); 89 | } 90 | 91 | return !dupe; 92 | }, 93 | 94 | removeWatch: function ( 95 | userId: string, 96 | magicKey: MagicKeyType, 97 | date: DateTime, 98 | ): boolean { 99 | return removeWatches(userId, magicKey, date); 100 | }, 101 | 102 | clearWatches: function (userId: string, magicKey: MagicKeyType): boolean { 103 | return removeWatches(userId, magicKey, undefined); 104 | }, 105 | }; 106 | -------------------------------------------------------------------------------- /src/bot/message/validate.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 pyamsoft 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { Msg } from "./Msg"; 18 | import { BotConfig } from "../../config"; 19 | import { Channel, ChannelType } from "discord.js"; 20 | 21 | const validateMessageHasId = function (message: Msg): boolean { 22 | return !!message.id; 23 | }; 24 | 25 | const validateMessageIsNotFromBot = function (message: Msg): boolean { 26 | return !message.author.bot; 27 | }; 28 | 29 | const validateMessageHasChannel = function (message: Msg): boolean { 30 | return !!message.channel; 31 | }; 32 | 33 | const validateMessageIsTextChannel = function (message: Msg): boolean { 34 | const type = message.channel.type; 35 | 36 | // This does exist in the source? 37 | // noinspection JSUnresolvedReference 38 | return type === ChannelType.GuildText || type === ChannelType.DM; 39 | }; 40 | 41 | const validateMessageIsTargetedChannel = function ( 42 | config: BotConfig, 43 | message: Msg, 44 | ): boolean { 45 | if (config.targetedChannels && config.targetedChannels.length > 0) { 46 | // I know this works, discord is dumb 47 | const ch = message.channel as unknown as Channel; 48 | return config.targetedChannels.some((c) => ch.id === c); 49 | } else { 50 | return true; 51 | } 52 | }; 53 | 54 | const validateMessageIsWatched = function ( 55 | config: BotConfig, 56 | message: Msg, 57 | ): boolean { 58 | // Should be "!mouse " like "!mouse show" not "!mouseshow" 59 | const content = message.content; 60 | return content === config.prefix || content.startsWith(`${config.prefix} `); 61 | }; 62 | 63 | export const validateMessage = function ( 64 | config: BotConfig, 65 | message: Msg, 66 | ): boolean { 67 | if (!validateMessageHasId(message)) { 68 | return false; 69 | } 70 | 71 | if (!validateMessageIsNotFromBot(message)) { 72 | return false; 73 | } 74 | 75 | if (!validateMessageHasChannel(message)) { 76 | return false; 77 | } 78 | 79 | if (!validateMessageIsTextChannel(message)) { 80 | return false; 81 | } 82 | 83 | const type = message.channel.type; 84 | 85 | // This does exist in the source? 86 | // noinspection JSUnresolvedReference 87 | if (type === ChannelType.GuildText) { 88 | if (!validateMessageIsTargetedChannel(config, message)) { 89 | return false; 90 | } 91 | } 92 | 93 | if (!validateMessageIsWatched(config, message)) { 94 | return false; 95 | } 96 | 97 | // Looks good 98 | return true; 99 | }; 100 | -------------------------------------------------------------------------------- /src/bot/message/Msg.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 pyamsoft 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { 18 | Message, 19 | PartialMessage, 20 | PartialTextBasedChannelFields, 21 | TextChannel, 22 | User, 23 | } from "discord.js"; 24 | 25 | interface DiscordMsg extends Msg { 26 | raw: Message; 27 | } 28 | 29 | interface LoggableMsg { 30 | id: string; 31 | content: string; 32 | } 33 | 34 | export interface Msg extends LoggableMsg { 35 | channel: TextChannel; 36 | author: User; 37 | } 38 | 39 | export interface MessageEditor { 40 | edit: (newMessageText: string) => Promise; 41 | } 42 | 43 | export interface MessageRemover { 44 | remove: () => Promise; 45 | } 46 | 47 | export interface SendChannel { 48 | send: (messageText: string) => Promise; 49 | } 50 | 51 | export const logMsg = function (m: Msg): LoggableMsg { 52 | return { 53 | id: m.id, 54 | content: m.content, 55 | }; 56 | }; 57 | 58 | export const msgFromMessage = function ( 59 | message: Message | PartialMessage, 60 | ): Msg { 61 | return { 62 | id: message.id, 63 | author: message.author as User, 64 | channel: message.channel as TextChannel, 65 | content: message.content as string, 66 | raw: message as Message, 67 | } as DiscordMsg; 68 | }; 69 | 70 | export const editorFromMessage = function ( 71 | message: Message | PartialMessage, 72 | ): MessageEditor { 73 | return { 74 | edit: async function (newMessageText: string) { 75 | return (message as Message) 76 | .edit(newMessageText) 77 | .then((msg) => msgFromMessage(msg)); 78 | }, 79 | }; 80 | }; 81 | 82 | export const removerFromMessage = function ( 83 | message: Message | PartialMessage, 84 | ): MessageRemover { 85 | return { 86 | remove: async function () { 87 | return (message as Message).delete().then((msg) => msg.id); 88 | }, 89 | }; 90 | }; 91 | 92 | export const messageFromMsg = function (message: Msg): Message { 93 | return (message as DiscordMsg).raw; 94 | }; 95 | 96 | export const sendChannelFromMessage = function ( 97 | message: Message | PartialMessage, 98 | ): SendChannel { 99 | return { 100 | send: async function (messageText: string) { 101 | const channel = (message as Message).channel; 102 | // Typescript is lame but I know this field exists on a message channel 103 | return (channel as unknown as PartialTextBasedChannelFields) 104 | .send(messageText) 105 | .then((msg) => msgFromMessage(msg)); 106 | }, 107 | }; 108 | }; 109 | -------------------------------------------------------------------------------- /src/bot/message/MessageHandler.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 pyamsoft 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { 18 | MessageReaction, 19 | PartialMessageReaction, 20 | PartialUser, 21 | User, 22 | } from "discord.js"; 23 | import { ParkCommand } from "../../commands/command"; 24 | import { BotConfig } from "../../config"; 25 | import { MessageEventType } from "../model/MessageEventType"; 26 | import { Msg } from "./Msg"; 27 | 28 | export interface KeyedMessageHandler { 29 | id: string; 30 | type: MessageEventType; 31 | handler: MessageHandler | ReactionHandler; 32 | } 33 | 34 | export interface MessageHandlerOutput { 35 | objectType: "MessageHandlerOutput"; 36 | helpOutput: string; 37 | messages: Record; 38 | } 39 | 40 | export const messageHandlerOutput = function ( 41 | messages: Record, 42 | ): MessageHandlerOutput { 43 | return { 44 | objectType: "MessageHandlerOutput", 45 | helpOutput: "", 46 | messages, 47 | }; 48 | }; 49 | 50 | export const messageHandlerHelpText = function ( 51 | message: string, 52 | ): MessageHandlerOutput { 53 | return { 54 | objectType: "MessageHandlerOutput", 55 | helpOutput: message, 56 | messages: {}, 57 | }; 58 | }; 59 | 60 | export interface MessageHandler { 61 | objectType: "MessageHandler"; 62 | 63 | tag: string; 64 | 65 | handle: ( 66 | config: BotConfig, 67 | command: { 68 | currentCommand: ParkCommand; 69 | oldCommand?: ParkCommand; 70 | message: Msg; 71 | }, 72 | ) => Promise | undefined; 73 | } 74 | 75 | export const newMessageHandler = function ( 76 | tag: string, 77 | handle: ( 78 | config: BotConfig, 79 | command: { 80 | currentCommand: ParkCommand; 81 | oldCommand?: ParkCommand; 82 | message: Msg; 83 | }, 84 | ) => Promise | undefined, 85 | ): MessageHandler { 86 | return { 87 | objectType: "MessageHandler", 88 | tag, 89 | handle, 90 | }; 91 | }; 92 | 93 | export interface ReactionHandler { 94 | objectType: "ReactionHandler"; 95 | 96 | tag: string; 97 | 98 | handle: ( 99 | config: BotConfig, 100 | reaction: MessageReaction | PartialMessageReaction, 101 | user: User | PartialUser, 102 | ) => void; 103 | } 104 | 105 | export const newReactionHandler = function ( 106 | tag: string, 107 | handle: ( 108 | config: BotConfig, 109 | reaction: MessageReaction | PartialMessageReaction, 110 | user: User | PartialUser, 111 | ) => void, 112 | ): ReactionHandler { 113 | return { 114 | objectType: "ReactionHandler", 115 | tag, 116 | handle, 117 | }; 118 | }; 119 | -------------------------------------------------------------------------------- /src/reactions/stopwatch.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 pyamsoft 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { MessageReaction, PartialMessageReaction } from "discord.js"; 18 | import { newLogger } from "../bot/logger"; 19 | import { newReactionHandler } from "../bot/message/MessageHandler"; 20 | import { reactionChannelFromMessage } from "../bot/message/Reaction"; 21 | import { BotConfig } from "../config"; 22 | import { ParkCalendarLookupLooper } from "../looper/ParkCalendarLookupLooper"; 23 | import { ParkWatchCache } from "../looper/ParkWatchCache"; 24 | import { WatchAlertMessageCache } from "../looper/WatchAlertMessageCache"; 25 | 26 | const TAG = "ReactionStopWatchHandler"; 27 | const logger = newLogger(TAG); 28 | 29 | export const ReactionStopWatchHandler = newReactionHandler( 30 | TAG, 31 | 32 | function ( 33 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 34 | // @ts-ignore Unused but needed for function signature 35 | config: BotConfig, 36 | reaction: MessageReaction | PartialMessageReaction, 37 | ) { 38 | const { message, emoji } = reaction; 39 | const targetMessageId = message.id; 40 | 41 | // If its a custom emoji, we need the id, otherwise the name as a unicode emoji works fine 42 | const emojiContent = emoji ? emoji.id || emoji.name : undefined; 43 | 44 | logger.log( 45 | "Reaction captured for message: ", 46 | targetMessageId, 47 | emojiContent, 48 | ); 49 | 50 | // See if we have sent the alert message 51 | const cachedAlert = WatchAlertMessageCache.getCachedAlert(targetMessageId); 52 | if (!cachedAlert) { 53 | logger.warn( 54 | "Can't handle reaction for a non-alert message", 55 | targetMessageId, 56 | ); 57 | return; 58 | } 59 | 60 | // We have, remove the watch and the alert 61 | WatchAlertMessageCache.removeCachedAlert(targetMessageId); 62 | 63 | // If we have removed the Watch, send a reaction to our own message using the same emoji 64 | if ( 65 | ParkWatchCache.removeWatch( 66 | cachedAlert.userId, 67 | cachedAlert.magicKey, 68 | cachedAlert.targetDate, 69 | ) 70 | ) { 71 | logger.log("Reaction clears watch", { 72 | alert: cachedAlert, 73 | emoji, 74 | }); 75 | 76 | // If removing causes us no more watches, then stop the looper 77 | if (ParkWatchCache.targetCalendars().length <= 0) { 78 | ParkCalendarLookupLooper.stop(); 79 | } 80 | 81 | // Additional user feedback 82 | if (emojiContent) { 83 | reactionChannelFromMessage(message) 84 | .react(emojiContent) 85 | .then(() => { 86 | logger.log("Closed the loop with a reaction", emojiContent); 87 | }) 88 | .catch((e) => { 89 | logger.error( 90 | e, 91 | "Error closing the loop with a reaction", 92 | emojiContent, 93 | ); 94 | }); 95 | } 96 | } 97 | }, 98 | ); 99 | -------------------------------------------------------------------------------- /src/looper/ParkCalendarLookupLooper.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 pyamsoft 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { newLogger } from "../bot/logger"; 18 | import { 19 | createResultFromEntry, 20 | WatchResult, 21 | } from "../commands/model/WatchResult"; 22 | import { ParkCalendarLookupHandler } from "./ParkCalendarLooupHandler"; 23 | import { ParkWatchCache } from "./ParkWatchCache"; 24 | 25 | const logger = newLogger("ParkCalendarLookupLooper"); 26 | 27 | /** 28 | * Loop every 30 seconds 29 | */ 30 | const LOOP_TIME = 30 * 1000; 31 | 32 | // Have we started? 33 | let looping = false; 34 | 35 | // The timer 36 | let loopTimer: NodeJS.Timeout | undefined = undefined; 37 | 38 | const beginLooping = function (command: () => void) { 39 | stopLooping(); 40 | 41 | loopTimer = setInterval(() => { 42 | logger.log("Loop firing command"); 43 | command(); 44 | }, LOOP_TIME); 45 | }; 46 | 47 | const stopLooping = function () { 48 | if (loopTimer) { 49 | logger.log("Clearing loop timer"); 50 | clearInterval(loopTimer); 51 | loopTimer = undefined; 52 | } 53 | }; 54 | 55 | export const ParkCalendarLookupLooper = { 56 | loop: function ( 57 | onResultsReceived: (results: ReadonlyArray) => void, 58 | ) { 59 | if (looping) { 60 | logger.log("loop() called but already looping"); 61 | return; 62 | } 63 | looping = true; 64 | 65 | logger.log("Begin loop!"); 66 | beginLooping(() => { 67 | logger.log("Loop run: Get all keys and fetch calendar info"); 68 | const magicKeys = ParkWatchCache.targetCalendars(); 69 | const jobs: Promise>[] = []; 70 | 71 | for (const magicKey of magicKeys) { 72 | const entries = ParkWatchCache.magicKeyWatches(magicKey); 73 | const dates = entries.map((e) => e.targetDate); 74 | 75 | logger.log("Lookup dates for watch entries: ", entries); 76 | 77 | jobs.push( 78 | ParkCalendarLookupHandler.lookup(magicKey, dates).then((lookup) => { 79 | const results: WatchResult[] = []; 80 | 81 | for (const res of lookup) { 82 | const entry = entries.find( 83 | (e) => 84 | e.targetDate.valueOf() === res.targetDate.valueOf() && 85 | e.magicKey === res.magicKey, 86 | ); 87 | if (entry) { 88 | results.push(createResultFromEntry(entry, res.parkResponse)); 89 | } 90 | } 91 | 92 | return results; 93 | }), 94 | ); 95 | } 96 | 97 | Promise.all(jobs).then((results) => { 98 | for (const watchResults of results) { 99 | onResultsReceived(watchResults); 100 | } 101 | }); 102 | }); 103 | }, 104 | 105 | stop: function () { 106 | if (!looping) { 107 | logger.log("stop() called but not looping"); 108 | return; 109 | } 110 | looping = false; 111 | 112 | logger.log("Stop loop!"); 113 | stopLooping(); 114 | }, 115 | }; 116 | -------------------------------------------------------------------------------- /src/network/magickeycalendar.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 pyamsoft 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { newLogger } from "../bot/logger"; 18 | import { MagicKeyType } from "../commands/model/MagicKeyType"; 19 | import { ParkCalendarResponse } from "../commands/model/ParkCalendarResponse"; 20 | import { parseDate } from "../looper/DateParser"; 21 | import { jsonApi } from "../util/api"; 22 | 23 | const logger = newLogger("MagicKeyCalendarApi"); 24 | 25 | /** 26 | * The Disney API blocks us unless we spoof our user agent to Firefox 27 | */ 28 | const DISNEY_HEADERS = { 29 | "User-Agent": 30 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:93.0) Gecko/20100101 Firefox/93.0", 31 | }; 32 | 33 | /** 34 | * For some reason, it only works like the website with 13. 35 | */ 36 | const NUMBER_MONTHS = 13; 37 | 38 | /** 39 | * String data returned when there is no availability 40 | */ 41 | const AVAILABILITY_BLOCKED = "cms-key-no-availability"; 42 | 43 | const createAvailability = function ( 44 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 45 | json: any, 46 | ): ParkCalendarResponse | undefined { 47 | const date = parseDate(json.date); 48 | if (!date) { 49 | return undefined; 50 | } 51 | 52 | // Hooray anytype! 53 | // noinspection JSUnresolvedReference 54 | return { 55 | objectType: "ParkCalendarResponse", 56 | json, 57 | date, 58 | available: 59 | !!json.availability && json.availability !== AVAILABILITY_BLOCKED, 60 | }; 61 | }; 62 | 63 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 64 | const createAvailabilityList = function (json: any): ParkCalendarResponse[] { 65 | const cal = json[0]; 66 | if (!cal) { 67 | logger.warn("Missing cal object in json list response: ", json); 68 | return []; 69 | } 70 | 71 | const list = cal["calendar-availabilities"]; 72 | if (!list || list.length <= 0) { 73 | return []; 74 | } 75 | return list 76 | .map(createAvailability) 77 | .filter((a: ParkCalendarResponse | undefined) => !!a); 78 | }; 79 | 80 | const lookupCalendar = async function ( 81 | magicKey: MagicKeyType, 82 | numberMonths: number, 83 | ): Promise> { 84 | logger.log("Hit upstream calendar endpoint", { 85 | magicKey, 86 | numberMonths, 87 | }); 88 | // There is a different authenticated URL that disney uses when you are signed in and go to pick your reservation date. 89 | // This endpoint may return different data that the "global" one 90 | return jsonApi( 91 | `https://disneyland.disney.go.com/passes/blockout-dates/api/get-availability/?product-types=${magicKey}&destinationId=DLR&numMonths=${numberMonths}`, 92 | DISNEY_HEADERS, 93 | ) 94 | .then(createAvailabilityList) 95 | .catch((e) => { 96 | logger.error(e, "Error getting DLR availability", { 97 | magicKey, 98 | numberMonths, 99 | }); 100 | return []; 101 | }); 102 | }; 103 | 104 | export const MagicKeyCalendarApi = { 105 | getCalendar: function (magicKey: MagicKeyType) { 106 | return lookupCalendar(magicKey, NUMBER_MONTHS); 107 | }, 108 | }; 109 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 pyamsoft 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { initializeBot } from "./bot"; 18 | import { newLogger } from "./bot/logger"; 19 | import { MessageEventTypes } from "./bot/model/MessageEventType"; 20 | import { CancelHandler } from "./commands/cancel"; 21 | import { HelpHandler } from "./commands/help"; 22 | import { ShowHandler } from "./commands/show"; 23 | import { StatusHandler } from "./commands/status"; 24 | import { StopHandler } from "./commands/stop"; 25 | import { WatchHandler } from "./commands/watch"; 26 | import { sourceConfig } from "./config"; 27 | import { registerPeriodicHealthCheck } from "./health"; 28 | import { ReactionStopWatchHandler } from "./reactions/stopwatch"; 29 | 30 | const logger = newLogger("MouseWatch"); 31 | 32 | const main = function () { 33 | const config = sourceConfig(); 34 | const bot = initializeBot(config); 35 | 36 | const createHelpHandler = bot.addHandler( 37 | MessageEventTypes.CREATE, 38 | HelpHandler, 39 | ); 40 | const updateHelpHandler = bot.addHandler( 41 | MessageEventTypes.UPDATE, 42 | HelpHandler, 43 | ); 44 | 45 | const createStatusHandler = bot.addHandler( 46 | MessageEventTypes.CREATE, 47 | StatusHandler, 48 | ); 49 | const updateStatusHandler = bot.addHandler( 50 | MessageEventTypes.UPDATE, 51 | StatusHandler, 52 | ); 53 | 54 | const createShowHandler = bot.addHandler( 55 | MessageEventTypes.CREATE, 56 | ShowHandler, 57 | ); 58 | const updateShowHandler = bot.addHandler( 59 | MessageEventTypes.UPDATE, 60 | ShowHandler, 61 | ); 62 | 63 | const createWatchHandler = bot.addHandler( 64 | MessageEventTypes.CREATE, 65 | WatchHandler, 66 | ); 67 | const updateWatchHandler = bot.addHandler( 68 | MessageEventTypes.UPDATE, 69 | WatchHandler, 70 | ); 71 | 72 | const createStopHandler = bot.addHandler( 73 | MessageEventTypes.CREATE, 74 | StopHandler, 75 | ); 76 | const updateStopHandler = bot.addHandler( 77 | MessageEventTypes.UPDATE, 78 | StopHandler, 79 | ); 80 | 81 | const createCancelHandler = bot.addHandler( 82 | MessageEventTypes.CREATE, 83 | CancelHandler, 84 | ); 85 | const updateCancelHandler = bot.addHandler( 86 | MessageEventTypes.UPDATE, 87 | CancelHandler, 88 | ); 89 | 90 | const stopWatchReactionHandler = bot.addHandler( 91 | MessageEventTypes.REACTION, 92 | ReactionStopWatchHandler, 93 | ); 94 | 95 | const health = registerPeriodicHealthCheck(config.healthCheckUrl); 96 | 97 | const watcher = bot.watchMessages(() => { 98 | health.unregister(); 99 | 100 | bot.removeHandler(createHelpHandler); 101 | bot.removeHandler(updateHelpHandler); 102 | 103 | bot.removeHandler(createStatusHandler); 104 | bot.removeHandler(updateStatusHandler); 105 | 106 | bot.removeHandler(createShowHandler); 107 | bot.removeHandler(updateShowHandler); 108 | 109 | bot.removeHandler(createWatchHandler); 110 | bot.removeHandler(updateWatchHandler); 111 | 112 | bot.removeHandler(createStopHandler); 113 | bot.removeHandler(updateStopHandler); 114 | 115 | bot.removeHandler(createCancelHandler); 116 | bot.removeHandler(updateCancelHandler); 117 | 118 | bot.removeHandler(stopWatchReactionHandler); 119 | }); 120 | 121 | bot.login().then((loggedIn) => { 122 | if (loggedIn) { 123 | logger.log("Bot logged in: ", loggedIn); 124 | } else { 125 | logger.warn("Bot failed to login!"); 126 | watcher.stop(); 127 | } 128 | }); 129 | }; 130 | 131 | main(); 132 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Discord DLR Magic Key Calendar watcher 2 | 3 | Watches the Magic key endpoint for availability 4 | 5 | This bot will never auto-book your reservation. The only goal is to automate watching 6 | and refreshing the magic key calendar page. 7 | 8 | # Install 9 | 10 | This is not currently a public bot, you need 11 | to make your own Discord App in the developer portal, 12 | and then pass your own bot tokens into `.env` 13 | along with a `prefix` (I use `!mouse`) 14 | 15 | Copy the `env.default` file to `.env` to get started! 16 | 17 | # Running 18 | 19 | You will need to create a Bot in the 20 | [Discord Developer Portal](https://discord.com/developers/applications/). At 21 | a minimum, the bot must have a `TOKEN` and you must set the bot up with the 22 | `Message Content` intent. 23 | 24 | [![Intents](https://raw.githubusercontent.com/pyamsoft/mousewatch/main/art/intents.png)][1] 25 | 26 | ```bash 27 | # You'll need pnpm installed, either via a local corepack directory or globally 28 | $ pnpm install && pnpm run start 29 | 30 | OR 31 | 32 | $ ./bin/dockerize 33 | ``` 34 | 35 | # Usage 36 | 37 | In any channel the bot is present in, type `` 38 | followed by the date you wish to check for availability 39 | 40 | [![Example Bot Command](https://raw.githubusercontent.com/pyamsoft/mousewatch/main/art/show.png)][2] 41 | 42 | Calling the bot with the `show` command and a `` formatted properly will make it show MK 43 | availability for that given date. 44 | 45 | You can call the bot with `watch` and a `` formatted properly and it will constantly watch the 46 | MK availability calendar for an opening on your given date. Upon seeing availability, the bot will @ reply 47 | to you in the same channel or DM you originally started the `watch` command in. You can always `stop` 48 | watching a specific ``, or you can `cancel` all watched dates at any time. 49 | 50 | For additional help and options, type the `` and the bot will display all of its commands. 51 | 52 | ## Customization 53 | 54 | You can have the bot only watch and reply in a designated channels by providing the 55 | `BOT_TARGET_CHANNEL_IDS` variable in the `.env` file, otherwise the bot will watch and reply from 56 | all channels. The `BOT_TARGET_CHANNEL_IDS` is a comma-seperated list of channel IDs. 57 | 58 | The bot will always watch and reply in individual DMs. 59 | 60 | ## Quirks 61 | 62 | DLA reservation website is intentionally, sneakily, broken 63 | 64 | tl;dr - not logged in the calendar is a complete lie from the webpage (the robot is more legit), 65 | and logged in you must be on a specific page for the calendar to refresh for real. 66 | 67 | when you are checking the MK calendar if you are not logged in, the days are different from when you are logged in. 68 | 69 | if you are not logged in, the calendar fetches dates randomly, anywhere between once a minute and once an hour. 70 | 71 | if you are logged in, you MUST refresh the page from this [URL](https://disneyland.disney.go.com/entry-reservation/) 72 | 73 | if you do not go from this page and click the button, the website does not actually ask the calendar for new 74 | reservation data, it will instead feed you saved data. No matter who you try to check or uncheck in your party, 75 | it doesn't matter. You must return to that starting page URL and click the button or the calendar won't refresh. 76 | 77 | happy reserving. 78 | 79 | ## License 80 | 81 | Apache 2 82 | 83 | ``` 84 | Copyright 2024 pyamsoft 85 | 86 | Licensed under the Apache License, Version 2.0 (the "License"); 87 | you may not use this file except in compliance with the License. 88 | You may obtain a copy of the License at 89 | 90 | http://www.apache.org/licenses/LICENSE-2.0 91 | 92 | Unless required by applicable law or agreed to in writing, software 93 | distributed under the License is distributed on an "AS IS" BASIS, 94 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 95 | See the License for the specific language governing permissions and 96 | limitations under the License. 97 | ``` 98 | 99 | [1]: https://raw.githubusercontent.com/pyamsoft/mouswatch/main/art/intents.png 100 | [2]: https://raw.githubusercontent.com/pyamsoft/mouswatch/main/art/show.png 101 | -------------------------------------------------------------------------------- /src/bot/message/MessageCache.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 pyamsoft 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { Msg } from "./Msg"; 18 | import { newLogger } from "../logger"; 19 | 20 | const logger = newLogger("MessageCache"); 21 | const DEFAULT_TIMEOUT = 2 * 60 * 60 * 1000; 22 | 23 | export interface MessageCache { 24 | insert: (messageId: string, key: string, message: Msg) => void; 25 | 26 | get: (messageId: string, key: string) => Msg | undefined; 27 | 28 | getAll: (messageId: string) => Record; 29 | 30 | remove: (messageId: string, key: string) => void; 31 | 32 | removeAll: (messageId: string) => void; 33 | } 34 | 35 | interface CachedMsg { 36 | msg: Msg; 37 | lastUsed: Date; 38 | } 39 | 40 | export const createMessageCache = function ( 41 | timeout: number = DEFAULT_TIMEOUT, 42 | ): MessageCache { 43 | const cache: Record< 44 | string, 45 | Record | undefined 46 | > = {}; 47 | 48 | const invalidateCache = function () { 49 | const now = new Date(); 50 | 51 | // Clear out any stale entries 52 | for (const id of Object.keys(cache)) { 53 | const oldData = cache[id]; 54 | 55 | // Already cleared out 56 | if (!oldData) { 57 | continue; 58 | } 59 | 60 | for (const keyed of Object.keys(oldData)) { 61 | const checking = oldData[keyed]; 62 | 63 | // Already cleared out 64 | if (!checking) { 65 | continue; 66 | } 67 | 68 | if (now.valueOf() - timeout > checking.lastUsed.valueOf()) { 69 | logger.log("Evicted old data from cache: ", { 70 | now, 71 | id, 72 | keyed, 73 | checking, 74 | }); 75 | oldData[keyed] = undefined; 76 | } 77 | } 78 | } 79 | }; 80 | 81 | const filterCached = function ( 82 | cached: Record, 83 | ): Record { 84 | const cleaned: Record = {}; 85 | for (const key of Object.keys(cached)) { 86 | const data = cached[key]; 87 | if (data) { 88 | cleaned[key] = data.msg; 89 | } 90 | } 91 | 92 | return cleaned; 93 | }; 94 | 95 | const getAllForId = function ( 96 | id: string, 97 | ): Record { 98 | if (!id) { 99 | return {}; 100 | } 101 | 102 | const cached = cache[id]; 103 | if (!cached) { 104 | return {}; 105 | } 106 | 107 | return cached; 108 | }; 109 | 110 | return { 111 | get: function (messageId: string, key: string) { 112 | // Remove stale 113 | invalidateCache(); 114 | 115 | const keyed = filterCached(getAllForId(messageId)); 116 | return keyed[key] || undefined; 117 | }, 118 | getAll: function (messageId: string) { 119 | // Remove stale 120 | invalidateCache(); 121 | 122 | return filterCached(getAllForId(messageId)); 123 | }, 124 | insert: function (messageId: string, key: string, message: Msg) { 125 | if (!cache[messageId]) { 126 | cache[messageId] = {}; 127 | } 128 | 129 | // We know this is truthy because of above 130 | const writeable = cache[messageId]; 131 | 132 | writeable[key] = { 133 | msg: message, 134 | lastUsed: new Date(), 135 | }; 136 | 137 | // Remove any stale 138 | invalidateCache(); 139 | }, 140 | remove: function (messageId: string, key: string) { 141 | const keyed = getAllForId(messageId); 142 | keyed[key] = undefined; 143 | 144 | // Remove stale 145 | invalidateCache(); 146 | }, 147 | removeAll: function (messageId: string) { 148 | cache[messageId] = undefined; 149 | 150 | // Remove stale 151 | invalidateCache(); 152 | }, 153 | }; 154 | }; 155 | -------------------------------------------------------------------------------- /src/commands/command.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 pyamsoft 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { DateTime } from "luxon"; 18 | import { newLogger } from "../bot/logger"; 19 | import { 20 | messageHandlerHelpText, 21 | MessageHandlerOutput, 22 | } from "../bot/message/MessageHandler"; 23 | import { BotConfig } from "../config"; 24 | import { cleanDate, parseDate } from "../looper/DateParser"; 25 | import { MagicKeyType } from "./model/MagicKeyType"; 26 | import { 27 | outputDateErrorText, 28 | outputDateMissingText, 29 | } from "./outputs/dateerror"; 30 | 31 | const logger = newLogger("Commands"); 32 | 33 | export enum ParkCommandType { 34 | SHOW = "SHOW", 35 | WATCH = "WATCH", 36 | STOP = "STOP", 37 | CANCEL = "CANCEL", 38 | STATUS = "STATUS", 39 | HELP = "HELP", 40 | NONE = "", 41 | } 42 | 43 | export interface ParkCommand { 44 | isHelpCommand: boolean; 45 | type: ParkCommandType; 46 | 47 | magicKey: MagicKeyType; 48 | dates: ReadonlyArray; 49 | } 50 | 51 | const stringContentToArray = function ( 52 | config: BotConfig, 53 | sliceOut: number, 54 | content: string, 55 | ): ParkCommand { 56 | const { prefix } = config; 57 | // This is just the prefix 58 | if (content.split("").every((s) => s === prefix)) { 59 | return { 60 | isHelpCommand: true, 61 | type: ParkCommandType.NONE, 62 | magicKey: MagicKeyType.NONE, 63 | dates: [], 64 | }; 65 | } 66 | 67 | // Here we separate our "command" name, and our "arguments" for the command. 68 | // e.g. if we have the message "+say Is this the real life?" , we'll get the following: 69 | // symbol = say 70 | // args = ["Is", "this", "the", "real", "life?"] 71 | const args = content.slice(sliceOut).trim().split(/\s+/g); 72 | 73 | // Shift out the first element, which is just the ${prefix} 74 | args.shift(); 75 | 76 | // Type of command should be first, dates are the rest 77 | let rawType = args.shift(); 78 | 79 | // No further command was passed, this is just the prefix 80 | if (!rawType) { 81 | return { 82 | isHelpCommand: true, 83 | type: ParkCommandType.NONE, 84 | magicKey: MagicKeyType.NONE, 85 | dates: [], 86 | }; 87 | } 88 | 89 | let type = ParkCommandType.NONE; 90 | rawType = rawType.toUpperCase(); 91 | if (rawType === ParkCommandType.SHOW) { 92 | type = ParkCommandType.SHOW; 93 | } else if (rawType === ParkCommandType.WATCH) { 94 | type = ParkCommandType.WATCH; 95 | } else if (rawType === ParkCommandType.STATUS) { 96 | type = ParkCommandType.STATUS; 97 | } else if (rawType === ParkCommandType.STOP) { 98 | type = ParkCommandType.STOP; 99 | } else if (rawType === ParkCommandType.CANCEL) { 100 | type = ParkCommandType.CANCEL; 101 | } else if (rawType === ParkCommandType.HELP) { 102 | type = ParkCommandType.HELP; 103 | } 104 | 105 | return { 106 | isHelpCommand: 107 | type === ParkCommandType.HELP || type === ParkCommandType.NONE, 108 | type, 109 | dates: args, 110 | 111 | // Magic key is hardcoded - change to take optional Key 112 | magicKey: MagicKeyType.INSPIRE, 113 | }; 114 | }; 115 | 116 | export const stringContentToParkCommand = function ( 117 | config: BotConfig, 118 | content: string, 119 | ): ParkCommand { 120 | const { isHelpCommand, type, dates, magicKey } = stringContentToArray( 121 | config, 122 | 0, 123 | content, 124 | ); 125 | 126 | return { 127 | isHelpCommand: 128 | isHelpCommand || 129 | type === ParkCommandType.NONE || 130 | type === ParkCommandType.HELP, 131 | type, 132 | magicKey, 133 | dates, 134 | }; 135 | }; 136 | 137 | const specialParseDate = function (dateString: string): DateTime | undefined { 138 | const specialCaseString = dateString.toLowerCase().trim(); 139 | let date: DateTime | undefined; 140 | if (specialCaseString === "tomorrow") { 141 | date = DateTime.now().plus({ day: 1 }); 142 | } else if (specialCaseString === "next-week") { 143 | date = DateTime.now().plus({ week: 1 }); 144 | } else if (specialCaseString === "next-month") { 145 | date = DateTime.now().plus({ month: 1 }); 146 | } 147 | 148 | if (date) { 149 | return cleanDate(date); 150 | } 151 | 152 | return parseDate(dateString); 153 | }; 154 | 155 | export const parseCommandDates = function (command: ParkCommand): { 156 | dateList: ReadonlyArray; 157 | error: Promise | undefined; 158 | } { 159 | const { dates } = command; 160 | 161 | const dateList: DateTime[] = []; 162 | for (const d of dates) { 163 | const parsedDate = specialParseDate(d); 164 | if (parsedDate) { 165 | dateList.push(parsedDate); 166 | } else { 167 | logger.warn("Failed to parse date string: ", d); 168 | return { 169 | dateList: [], 170 | error: Promise.resolve(messageHandlerHelpText(outputDateErrorText(d))), 171 | }; 172 | } 173 | } 174 | 175 | if (dateList.length <= 0) { 176 | logger.warn("Need at least one date to watch"); 177 | return { 178 | dateList: [], 179 | error: Promise.resolve(messageHandlerHelpText(outputDateMissingText())), 180 | }; 181 | } 182 | 183 | return { dateList, error: undefined }; 184 | }; 185 | -------------------------------------------------------------------------------- /src/bot/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 pyamsoft 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { 18 | Client, 19 | GatewayIntentBits, 20 | Message, 21 | MessageReaction, 22 | PartialMessage, 23 | PartialMessageReaction, 24 | Partials, 25 | PartialUser, 26 | User, 27 | } from "discord.js"; 28 | import { BotConfig } from "../config"; 29 | import { newLogger } from "./logger"; 30 | import { createMessageCache } from "./message/MessageCache"; 31 | import { 32 | KeyedMessageHandler, 33 | MessageHandler, 34 | ReactionHandler, 35 | } from "./message/MessageHandler"; 36 | import { handleBotMessage, handleBotMessageReaction } from "./message/messages"; 37 | import { generateRandomId } from "./model/id"; 38 | import { Listener, newListener } from "./model/listener"; 39 | import { MessageEventType, MessageEventTypes } from "./model/MessageEventType"; 40 | 41 | const logger = newLogger("DiscordBot"); 42 | 43 | export interface DiscordBot { 44 | login: () => Promise; 45 | 46 | addHandler: ( 47 | type: MessageEventType, 48 | handler: MessageHandler | ReactionHandler, 49 | ) => string; 50 | 51 | removeHandler: (id: string) => boolean; 52 | 53 | watchMessages: (onStop: () => void) => Listener; 54 | } 55 | 56 | export const initializeBot = function (config: BotConfig): DiscordBot { 57 | // This does exist in the source? 58 | // noinspection JSUnresolvedReference 59 | const client = new Client({ 60 | intents: [ 61 | GatewayIntentBits.Guilds, 62 | 63 | // Needed to read messages 64 | GatewayIntentBits.GuildMessages, 65 | GatewayIntentBits.DirectMessages, 66 | 67 | // Needed for reactions 68 | GatewayIntentBits.GuildMessageReactions, 69 | GatewayIntentBits.DirectMessageReactions, 70 | 71 | // Need to read message content 72 | GatewayIntentBits.MessageContent, 73 | ], 74 | partials: [Partials.Message, Partials.Channel], 75 | }); 76 | 77 | const handlers: Record = {}; 78 | const messageCache = createMessageCache(); 79 | 80 | // Keep this cached to avoid having to recalculate it each time 81 | let handlerList: KeyedMessageHandler[] = []; 82 | 83 | const messageHandler = function (message: Message | PartialMessage) { 84 | handleBotMessage(config, MessageEventTypes.CREATE, message, undefined, { 85 | handlers: handlerList, 86 | cache: messageCache, 87 | }); 88 | }; 89 | 90 | const messageUpdateHandler = function ( 91 | oldMessage: Message | PartialMessage, 92 | newMessage: Message | PartialMessage, 93 | ) { 94 | handleBotMessage(config, MessageEventTypes.UPDATE, newMessage, oldMessage, { 95 | handlers: handlerList, 96 | cache: messageCache, 97 | }); 98 | }; 99 | 100 | const messageReactionHandler = function ( 101 | reaction: MessageReaction | PartialMessageReaction, 102 | user: User | PartialUser, 103 | ) { 104 | handleBotMessageReaction( 105 | config, 106 | MessageEventTypes.REACTION, 107 | reaction, 108 | user, 109 | { 110 | handlers: handlerList, 111 | cache: messageCache, 112 | }, 113 | ); 114 | }; 115 | 116 | return Object.freeze({ 117 | addHandler: function ( 118 | type: MessageEventType, 119 | handler: MessageHandler | ReactionHandler, 120 | ) { 121 | const id = generateRandomId(); 122 | handlers[id] = { id, handler, type }; 123 | logger.log("Add new handler: ", handlers[id]); 124 | handlerList = Object.values(handlers).filter((h) => !!h); 125 | return id; 126 | }, 127 | removeHandler: function (id: string) { 128 | if (handlers[id]) { 129 | logger.log("Removed handler: ", handlers[id]); 130 | handlers[id] = undefined; 131 | handlerList = Object.values(handlers).filter((h) => !!h); 132 | return true; 133 | } else { 134 | return false; 135 | } 136 | }, 137 | watchMessages: function (onStop: () => void) { 138 | const readyHandler = function () { 139 | logger.log("Bot is ready!"); 140 | logger.log("Watch for messages"); 141 | 142 | client.on(MessageEventTypes.CREATE, messageHandler); 143 | client.on(MessageEventTypes.UPDATE, messageUpdateHandler); 144 | 145 | client.on(MessageEventTypes.REACTION, messageReactionHandler); 146 | }; 147 | 148 | const errorHandler = function (error: Error) { 149 | logger.error(error, "BOT ERROR"); 150 | client.off("ready", readyHandler); 151 | 152 | client.off(MessageEventTypes.CREATE, messageHandler); 153 | client.off(MessageEventTypes.UPDATE, messageUpdateHandler); 154 | 155 | client.off(MessageEventTypes.REACTION, messageReactionHandler); 156 | 157 | onStop(); 158 | }; 159 | 160 | client.on("error", errorHandler); 161 | 162 | logger.log("Wait until bot is ready"); 163 | client.once("ready", readyHandler); 164 | return newListener(() => { 165 | logger.log("Stop watching for messages"); 166 | client.off("ready", readyHandler); 167 | 168 | client.off(MessageEventTypes.CREATE, messageHandler); 169 | client.off(MessageEventTypes.UPDATE, messageUpdateHandler); 170 | 171 | client.off(MessageEventTypes.REACTION, messageReactionHandler); 172 | 173 | onStop(); 174 | }); 175 | }, 176 | login: async function () { 177 | const { token } = config; 178 | return client 179 | .login(token) 180 | .then(() => { 181 | logger.log("Bot logged in!"); 182 | return true; 183 | }) 184 | .catch((e) => { 185 | logger.error(e, "Error logging in"); 186 | return false; 187 | }); 188 | }, 189 | }); 190 | }; 191 | -------------------------------------------------------------------------------- /src/bot/message/messages.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 pyamsoft 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { 18 | Message, 19 | MessageReaction, 20 | PartialMessage, 21 | PartialMessageReaction, 22 | PartialUser, 23 | User, 24 | } from "discord.js"; 25 | import { stringContentToParkCommand } from "../../commands/command"; 26 | import { BotConfig } from "../../config"; 27 | import { newLogger } from "../logger"; 28 | import { MessageEventType, MessageEventTypes } from "../model/MessageEventType"; 29 | import { 30 | createCommunicationMessage, 31 | createCommunicationResult, 32 | sendMessage, 33 | } from "./communicate"; 34 | import { MessageCache } from "./MessageCache"; 35 | import { KeyedMessageHandler, MessageHandlerOutput } from "./MessageHandler"; 36 | import { 37 | logMsg, 38 | Msg, 39 | msgFromMessage, 40 | SendChannel, 41 | sendChannelFromMessage, 42 | } from "./Msg"; 43 | import { validateMessage } from "./validate"; 44 | 45 | const logger = newLogger("messages"); 46 | 47 | const sendMessageAfterParsing = function ( 48 | results: ReadonlyArray, 49 | message: Msg, 50 | sendChannel: SendChannel, 51 | env: { 52 | handlers: ReadonlyArray; 53 | cache: MessageCache; 54 | }, 55 | ) { 56 | // None of our handlers have done this, if we continue, behavior is undefined 57 | if (results.length <= 0) { 58 | logger.warn("No results, unhandled message: ", logMsg(message)); 59 | return; 60 | } 61 | 62 | const combinedOutputs: Record = {}; 63 | for (const res of results) { 64 | // Any help outputs immediately stop the message sending 65 | if (!!res.helpOutput && !!res.helpOutput.trim()) { 66 | sendMessage( 67 | message.id, 68 | sendChannel, 69 | createCommunicationMessage(res.helpOutput), 70 | env, 71 | ).then((responded) => { 72 | logger.log("Responded with help text", !!responded); 73 | }); 74 | return; 75 | } else { 76 | for (const key of Object.keys(res.messages)) { 77 | combinedOutputs[key] = res.messages[key]; 78 | } 79 | } 80 | } 81 | 82 | // Otherwise we've collected all of our output, so spit it out into a single message 83 | sendMessage( 84 | message.id, 85 | sendChannel, 86 | createCommunicationResult(combinedOutputs), 87 | env, 88 | ).then((responded) => { 89 | logger.log( 90 | "Responded with combined output for keys: ", 91 | Object.keys(combinedOutputs), 92 | !!responded, 93 | ); 94 | }); 95 | }; 96 | 97 | export const handleBotMessage = function ( 98 | config: BotConfig, 99 | eventType: MessageEventType, 100 | message: Message | PartialMessage, 101 | optionalOldMessage: Message | PartialMessage | undefined, 102 | env: { 103 | handlers: ReadonlyArray; 104 | cache: MessageCache; 105 | }, 106 | ) { 107 | // Reactions are handled by a different function 108 | if (eventType === MessageEventTypes.REACTION) { 109 | return; 110 | } 111 | 112 | const msg = msgFromMessage(message); 113 | 114 | // Normal message handling 115 | if (!validateMessage(config, msg)) { 116 | return; 117 | } 118 | 119 | const oldMsg = optionalOldMessage 120 | ? msgFromMessage(optionalOldMessage) 121 | : undefined; 122 | 123 | const sendChannel = sendChannelFromMessage(message); 124 | const current = stringContentToParkCommand(config, msg.content); 125 | const old = oldMsg 126 | ? stringContentToParkCommand(config, oldMsg.content) 127 | : undefined; 128 | 129 | const work: Promise[] = []; 130 | const { handlers } = env; 131 | for (const item of handlers) { 132 | // If it was removed, skip it 133 | if (!item) { 134 | continue; 135 | } 136 | 137 | const { handler, id, type } = item; 138 | if (type === eventType) { 139 | let output: Promise | undefined = undefined; 140 | if (handler.objectType === "MessageHandler") { 141 | output = handler.handle(config, { 142 | currentCommand: current, 143 | oldCommand: old, 144 | message: msg, 145 | }); 146 | } else { 147 | logger.warn( 148 | "Handler type cannot handle bot messages: ", 149 | handler.objectType, 150 | ); 151 | return; 152 | } 153 | 154 | if (output) { 155 | logger.log("Pass message to handler: ", { 156 | id, 157 | type, 158 | name: handler.tag, 159 | }); 160 | work.push(output); 161 | } 162 | } 163 | } 164 | 165 | Promise.all(work).then((outputs) => 166 | sendMessageAfterParsing(outputs, msg, sendChannel, env), 167 | ); 168 | }; 169 | 170 | export const handleBotMessageReaction = function ( 171 | config: BotConfig, 172 | eventType: MessageEventType, 173 | reaction: MessageReaction | PartialMessageReaction, 174 | user: User | PartialUser, 175 | env: { 176 | handlers: ReadonlyArray; 177 | cache: MessageCache; 178 | }, 179 | ) { 180 | // Messages are handled by a different function 181 | if (eventType !== MessageEventTypes.REACTION) { 182 | return; 183 | } 184 | 185 | const { handlers } = env; 186 | for (const item of handlers) { 187 | // If it was removed, skip it 188 | if (!item) { 189 | continue; 190 | } 191 | 192 | const { handler, type } = item; 193 | if (type === eventType) { 194 | if (handler.objectType === "ReactionHandler") { 195 | handler.handle(config, reaction, user); 196 | } else { 197 | logger.warn( 198 | "Handler type cannot handle bot reactions: ", 199 | handler.objectType, 200 | ); 201 | return; 202 | } 203 | } 204 | } 205 | }; 206 | -------------------------------------------------------------------------------- /src/commands/watch.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 pyamsoft 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { DateTime } from "luxon"; 18 | import { newLogger } from "../bot/logger"; 19 | import { 20 | messageHandlerOutput, 21 | newMessageHandler, 22 | } from "../bot/message/MessageHandler"; 23 | import { 24 | messageFromMsg, 25 | Msg, 26 | sendChannelFromMessage, 27 | } from "../bot/message/Msg"; 28 | import { BotConfig } from "../config"; 29 | import { ParkCalendarLookupLooper } from "../looper/ParkCalendarLookupLooper"; 30 | import { ParkCalendarLookupHandler } from "../looper/ParkCalendarLooupHandler"; 31 | import { ParkWatchCache } from "../looper/ParkWatchCache"; 32 | import { WatchAlertMessageCache } from "../looper/WatchAlertMessageCache"; 33 | import { ParkCommand, ParkCommandType, parseCommandDates } from "./command"; 34 | import { watchEntryFromMessage } from "./model/WatchEntry"; 35 | import { outputParkAvailability } from "./outputs/availability"; 36 | import { outputWatchStarted } from "./outputs/watch"; 37 | import { WatchResult } from "./model/WatchResult"; 38 | 39 | const TAG = "WatchHandler"; 40 | const logger = newLogger(TAG); 41 | 42 | const cancelOldWatches = function (message: Msg, oldCommand: ParkCommand) { 43 | if (oldCommand.isHelpCommand || oldCommand.type !== ParkCommandType.WATCH) { 44 | return; 45 | } 46 | 47 | const { magicKey } = oldCommand; 48 | const { dateList, error } = parseCommandDates(oldCommand); 49 | if (error) { 50 | return; 51 | } 52 | 53 | const { author } = message; 54 | const userId = author.id; 55 | for (const date of dateList) { 56 | if (ParkWatchCache.removeWatch(userId, magicKey, date)) { 57 | logger.log("Remove stale watch: ", { 58 | userId, 59 | magicKey, 60 | date, 61 | }); 62 | } 63 | } 64 | 65 | // If removing causes us no more watches, then stop the looper 66 | if (ParkWatchCache.targetCalendars().length <= 0) { 67 | ParkCalendarLookupLooper.stop(); 68 | } 69 | }; 70 | 71 | export const WatchHandler = newMessageHandler( 72 | TAG, 73 | function ( 74 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 75 | // @ts-ignore 76 | config: BotConfig, 77 | command: { 78 | currentCommand: ParkCommand; 79 | oldCommand?: ParkCommand; 80 | message: Msg; 81 | }, 82 | ) { 83 | const { currentCommand, oldCommand, message } = command; 84 | if ( 85 | currentCommand.isHelpCommand || 86 | currentCommand.type !== ParkCommandType.WATCH 87 | ) { 88 | return; 89 | } 90 | 91 | if (oldCommand) { 92 | // Cancel any old watches 93 | cancelOldWatches(message, oldCommand); 94 | } 95 | 96 | logger.log("Handle watch message", currentCommand); 97 | const { magicKey } = currentCommand; 98 | const { dateList, error } = parseCommandDates(currentCommand); 99 | 100 | if (error) { 101 | return error; 102 | } 103 | 104 | return ParkCalendarLookupHandler.lookup(magicKey, dateList).then( 105 | (results) => { 106 | const messages: Record = {}; 107 | const notFoundDates: DateTime[] = []; 108 | 109 | for (const d of dateList) { 110 | const res = results.find( 111 | (r) => d.valueOf() === r.targetDate.valueOf(), 112 | ); 113 | const key = d.toISO(); 114 | if (!key) { 115 | continue; 116 | } 117 | 118 | // If we have availability, don't watch just immediately output 119 | if (res && res.parkResponse.available) { 120 | messages[key] = outputParkAvailability(undefined, res); 121 | } else { 122 | messages[key] = outputWatchStarted(magicKey, d); 123 | notFoundDates.push(d); 124 | 125 | ParkWatchCache.addWatch( 126 | watchEntryFromMessage({ 127 | message, 128 | magicKey, 129 | targetDate: d, 130 | }), 131 | ); 132 | } 133 | } 134 | 135 | if (notFoundDates.length > 0) { 136 | sideEffectWatchLoop(message); 137 | } 138 | 139 | return messageHandlerOutput(messages); 140 | }, 141 | ); 142 | }, 143 | ); 144 | 145 | const alreadySeenResult = function (r1: WatchResult, r2: WatchResult): boolean { 146 | // If channels don't match, then maybe new 147 | if (r1.channelId !== r2.channelId) { 148 | return false; 149 | } 150 | 151 | // If messages don't match, then maybe new 152 | if (r1.messageId !== r2.messageId) { 153 | return false; 154 | } 155 | 156 | // If users don't match, then maybe new 157 | if (r1.userId !== r2.userId) { 158 | return false; 159 | } 160 | 161 | // If key types don't match, then maybe new 162 | if (r1.magicKey !== r2.magicKey) { 163 | return false; 164 | } 165 | 166 | // If we have seen the exact same date already, then we have seen this 167 | return r1.targetDate.valueOf() === r2.targetDate.valueOf(); 168 | }; 169 | 170 | const sideEffectWatchLoop = function (message: Msg) { 171 | const discordMsg = messageFromMsg(message); 172 | const sender = sendChannelFromMessage(discordMsg); 173 | 174 | ParkCalendarLookupLooper.loop((results) => { 175 | // For some reason, a single watch fires like 5 messages out. 176 | // We try to avoid this mass spam by de-duping at the sending point 177 | const avoidMassSpamBug: WatchResult[] = []; 178 | 179 | for (const res of results) { 180 | if (!res.parkResponse.available) { 181 | continue; 182 | } 183 | 184 | // If we have already sent this message before, do not send it again 185 | const alreadySent = avoidMassSpamBug.find((s) => 186 | alreadySeenResult(s, res), 187 | ); 188 | if (alreadySent) { 189 | logger.warn("[BUG]: We have already sent this Result!", res); 190 | continue; 191 | } else { 192 | avoidMassSpamBug.push(res); 193 | } 194 | 195 | const msg = outputParkAvailability(res.userId, res); 196 | 197 | // This alert message is uncached and thus uneditable by the robot. 198 | sender 199 | .send(msg) 200 | .then((newMessage) => { 201 | logger.log("WATCH: Fired alert message: ", { 202 | messageId: newMessage.id, 203 | result: res, 204 | }); 205 | 206 | // Cache into the AlertCache for later 207 | // 208 | // If a user responds to a message that we have a cached alert for, stop watching it. 209 | WatchAlertMessageCache.cacheAlert(newMessage.id, res); 210 | }) 211 | .catch((e) => { 212 | logger.error(e, "Error firing alert message: ", { 213 | result: res, 214 | }); 215 | }); 216 | } 217 | }); 218 | }; 219 | -------------------------------------------------------------------------------- /src/bot/message/communicate.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 pyamsoft 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { MessageCache } from "./MessageCache"; 18 | import { newLogger } from "../logger"; 19 | import { 20 | editorFromMessage, 21 | MessageEditor, 22 | messageFromMsg, 23 | Msg, 24 | removerFromMessage, 25 | SendChannel, 26 | } from "./Msg"; 27 | import { ensureArray } from "../../util/array"; 28 | 29 | const GLOBAL_CACHE_KEY = "global-cache-key"; 30 | const logger = newLogger("communicate"); 31 | 32 | export interface CommunicationResult { 33 | objectType: "CommunicationResult"; 34 | data: T; 35 | } 36 | 37 | export interface CommunicationMessage { 38 | objectType: "CommunicationMessage"; 39 | message: string; 40 | } 41 | 42 | export const createCommunicationMessage = function ( 43 | message: string, 44 | ): CommunicationMessage { 45 | return { 46 | objectType: "CommunicationMessage", 47 | message, 48 | }; 49 | }; 50 | 51 | export const createCommunicationResult = function ( 52 | data: T, 53 | ): CommunicationResult { 54 | return { 55 | objectType: "CommunicationResult", 56 | data, 57 | }; 58 | }; 59 | 60 | const deleteOldMessages = function ( 61 | receivedMessageId: string, 62 | keys: ReadonlyArray, 63 | env: { 64 | cache: MessageCache; 65 | }, 66 | ): Promise { 67 | const { cache } = env; 68 | const allOldData = cache.getAll(receivedMessageId); 69 | const oldContents = Object.keys(allOldData); 70 | if (oldContents.length <= 0) { 71 | logger.log("No old contents to delete, continue."); 72 | return Promise.resolve(); 73 | } 74 | 75 | const work = []; 76 | for (const key of oldContents) { 77 | // If the new message replacing this one does not include previous existing content, delete the old message 78 | // that holds the old content 79 | if (!keys.includes(key) && oldContents.includes(key)) { 80 | const oldMessage = allOldData[key]; 81 | 82 | // Double check 83 | if (!oldMessage) { 84 | continue; 85 | } 86 | 87 | logger.log("Key existed in old message but not new message, delete it", { 88 | oldContent: key, 89 | newContents: keys, 90 | }); 91 | 92 | // We know this to be true 93 | const remover = removerFromMessage(messageFromMsg(oldMessage)); 94 | const working = remover 95 | .remove() 96 | .then((id) => { 97 | logger.log("Deleted old message: ", { 98 | key, 99 | messageId: id, 100 | }); 101 | cache.remove(receivedMessageId, key); 102 | }) 103 | .catch((e) => { 104 | logger.error(e, "Failed to delete old message", { 105 | key, 106 | messageId: oldMessage.id, 107 | }); 108 | }); 109 | 110 | // Add to the list of jobs we are waiting for 111 | work.push(working); 112 | } 113 | } 114 | 115 | return Promise.all(work).then(() => { 116 | // blank 117 | }); 118 | }; 119 | 120 | const postNewMessages = async function ( 121 | messageId: string, 122 | channel: SendChannel, 123 | keys: ReadonlyArray, 124 | messages: Record, 125 | env: { 126 | cache: MessageCache; 127 | }, 128 | ): Promise> { 129 | const work = []; 130 | for (const key of keys) { 131 | const messageText = messages[key]; 132 | if (!!messageText && !!messageText.trim()) { 133 | const working = postMessageToPublicChannel( 134 | messageId, 135 | channel, 136 | messageText, 137 | { 138 | ...env, 139 | cacheKey: key, 140 | cacheResult: true, 141 | }, 142 | ); 143 | 144 | // Add to the list of jobs we are waiting for 145 | work.push(working); 146 | } 147 | } 148 | 149 | return Promise.all(work).then((results) => { 150 | const messagesOnly = []; 151 | for (const msg of results) { 152 | if (msg) { 153 | messagesOnly.push(msg); 154 | } 155 | } 156 | 157 | return messagesOnly; 158 | }); 159 | }; 160 | 161 | const editExistingMessage = function ( 162 | receivedMessageId: string, 163 | editor: MessageEditor, 164 | oldMessage: Msg, 165 | messageText: string, 166 | env: { 167 | cacheKey: string; 168 | cache: MessageCache; 169 | cacheResult: boolean; 170 | }, 171 | ): Promise { 172 | const { cache, cacheKey, cacheResult } = env; 173 | return new Promise((resolve) => { 174 | editor 175 | .edit(messageText) 176 | .then((newMessage) => { 177 | logger.log("Updated old message with new content: ", { 178 | key: cacheKey, 179 | oldMessageId: oldMessage.id, 180 | newMessageId: newMessage.id, 181 | receivedMessageId, 182 | }); 183 | if (cacheResult) { 184 | logger.log("Caching update result: ", { 185 | messageId: receivedMessageId, 186 | key: cacheKey, 187 | }); 188 | cache.insert(receivedMessageId, cacheKey, newMessage); 189 | resolve(newMessage); 190 | } 191 | }) 192 | .catch((e) => { 193 | logger.error(e, "Unable to update old message with new content: ", { 194 | key: cacheKey, 195 | oldMessageId: oldMessage.id, 196 | receivedMessageId, 197 | }); 198 | resolve(undefined); 199 | }); 200 | }); 201 | }; 202 | 203 | const sendNewMessageToChannel = function ( 204 | receivedMessageId: string, 205 | channel: SendChannel, 206 | messageText: string, 207 | env: { 208 | cacheKey: string; 209 | cache: MessageCache; 210 | cacheResult: boolean; 211 | }, 212 | ): Promise { 213 | const { cache, cacheKey, cacheResult } = env; 214 | return new Promise((resolve) => { 215 | channel 216 | .send(messageText) 217 | .then((newMessage) => { 218 | logger.log("Send new message: ", { 219 | messageId: newMessage.id, 220 | receivedMessageId, 221 | key: cacheKey, 222 | }); 223 | 224 | if (cacheResult) { 225 | logger.log("Caching new message result: ", { 226 | messageId: newMessage.id, 227 | receivedMessageId, 228 | key: cacheKey, 229 | }); 230 | cache.insert(receivedMessageId, cacheKey, newMessage); 231 | } 232 | resolve(newMessage); 233 | }) 234 | .catch((e) => { 235 | logger.error(e, "Unable to send message", { 236 | key: cacheKey, 237 | text: messageText, 238 | receivedMessageId, 239 | }); 240 | resolve(undefined); 241 | }); 242 | }); 243 | }; 244 | 245 | const postMessageToPublicChannel = function ( 246 | receivedMessageId: string, 247 | channel: SendChannel, 248 | messageText: string, 249 | env: { 250 | cacheKey: string; 251 | cache: MessageCache; 252 | cacheResult: boolean; 253 | }, 254 | ): Promise { 255 | const { cache, cacheKey } = env; 256 | const oldMessage = 257 | !!cacheKey && !!cacheKey.trim() 258 | ? cache.get(receivedMessageId, cacheKey) 259 | : undefined; 260 | 261 | if (oldMessage) { 262 | // We know this to be true 263 | const editor = editorFromMessage(messageFromMsg(oldMessage)); 264 | return editExistingMessage( 265 | receivedMessageId, 266 | editor, 267 | oldMessage, 268 | messageText, 269 | env, 270 | ); 271 | } else { 272 | return sendNewMessageToChannel( 273 | receivedMessageId, 274 | channel, 275 | messageText, 276 | env, 277 | ); 278 | } 279 | }; 280 | 281 | export const sendMessage = async function ( 282 | receivedMessageId: string, 283 | channel: SendChannel, 284 | content: CommunicationMessage | CommunicationResult>, 285 | env: { 286 | cache: MessageCache; 287 | }, 288 | ): Promise> { 289 | if (content.objectType === "CommunicationMessage") { 290 | // Plain text message 291 | return postMessageToPublicChannel( 292 | receivedMessageId, 293 | channel, 294 | content.message, 295 | { 296 | ...env, 297 | cacheKey: GLOBAL_CACHE_KEY, 298 | cacheResult: true, 299 | }, 300 | ).then((msg) => { 301 | if (msg) { 302 | return ensureArray(msg); 303 | } else { 304 | return []; 305 | } 306 | }); 307 | } else { 308 | const { data } = content; 309 | // Delete any old messages first 310 | const keys = Object.keys(data); 311 | return deleteOldMessages(receivedMessageId, keys, env).then(() => 312 | postNewMessages(receivedMessageId, channel, keys, data, env), 313 | ); 314 | } 315 | }; 316 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /pnpm-lock.yaml: -------------------------------------------------------------------------------- 1 | lockfileVersion: '9.0' 2 | 3 | settings: 4 | autoInstallPeers: true 5 | excludeLinksFromLockfile: false 6 | 7 | importers: 8 | 9 | .: 10 | dependencies: 11 | discord.js: 12 | specifier: 14.25.1 13 | version: 14.25.1 14 | dotenv: 15 | specifier: 17.2.3 16 | version: 17.2.3 17 | luxon: 18 | specifier: 3.7.2 19 | version: 3.7.2 20 | devDependencies: 21 | '@eslint/eslintrc': 22 | specifier: 3.3.1 23 | version: 3.3.1 24 | '@eslint/js': 25 | specifier: 9.39.1 26 | version: 9.39.1 27 | '@types/luxon': 28 | specifier: 3.7.1 29 | version: 3.7.1 30 | '@types/node': 31 | specifier: 24.10.1 32 | version: 24.10.1 33 | '@typescript-eslint/eslint-plugin': 34 | specifier: 8.47.0 35 | version: 8.47.0(@typescript-eslint/parser@8.47.0(eslint@9.39.1)(typescript@5.9.3))(eslint@9.39.1)(typescript@5.9.3) 36 | '@typescript-eslint/parser': 37 | specifier: 8.47.0 38 | version: 8.47.0(eslint@9.39.1)(typescript@5.9.3) 39 | eslint: 40 | specifier: 9.39.1 41 | version: 9.39.1 42 | globals: 43 | specifier: 16.5.0 44 | version: 16.5.0 45 | npm-check-updates: 46 | specifier: 19.1.2 47 | version: 19.1.2 48 | prettier: 49 | specifier: 3.6.2 50 | version: 3.6.2 51 | tslib: 52 | specifier: 2.8.1 53 | version: 2.8.1 54 | typescript: 55 | specifier: 5.9.3 56 | version: 5.9.3 57 | typescript-eslint: 58 | specifier: 8.47.0 59 | version: 8.47.0(eslint@9.39.1)(typescript@5.9.3) 60 | vite-node: 61 | specifier: 5.2.0 62 | version: 5.2.0(@types/node@24.10.1) 63 | 64 | packages: 65 | 66 | '@discordjs/builders@1.13.0': 67 | resolution: {integrity: sha512-COK0uU6ZaJI+LA67H/rp8IbEkYwlZf3mAoBI5wtPh5G5cbEQGNhVpzINg2f/6+q/YipnNIKy6fJDg6kMUKUw4Q==} 68 | engines: {node: '>=16.11.0'} 69 | 70 | '@discordjs/collection@1.5.3': 71 | resolution: {integrity: sha512-SVb428OMd3WO1paV3rm6tSjM4wC+Kecaa1EUGX7vc6/fddvw/6lg90z4QtCqm21zvVe92vMMDt9+DkIvjXImQQ==} 72 | engines: {node: '>=16.11.0'} 73 | 74 | '@discordjs/collection@2.1.1': 75 | resolution: {integrity: sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg==} 76 | engines: {node: '>=18'} 77 | 78 | '@discordjs/formatters@0.6.2': 79 | resolution: {integrity: sha512-y4UPwWhH6vChKRkGdMB4odasUbHOUwy7KL+OVwF86PvT6QVOwElx+TiI1/6kcmcEe+g5YRXJFiXSXUdabqZOvQ==} 80 | engines: {node: '>=16.11.0'} 81 | 82 | '@discordjs/rest@2.6.0': 83 | resolution: {integrity: sha512-RDYrhmpB7mTvmCKcpj+pc5k7POKszS4E2O9TYc+U+Y4iaCP+r910QdO43qmpOja8LRr1RJ0b3U+CqVsnPqzf4w==} 84 | engines: {node: '>=18'} 85 | 86 | '@discordjs/util@1.2.0': 87 | resolution: {integrity: sha512-3LKP7F2+atl9vJFhaBjn4nOaSWahZ/yWjOvA4e5pnXkt2qyXRCHLxoBQy81GFtLGCq7K9lPm9R517M1U+/90Qg==} 88 | engines: {node: '>=18'} 89 | 90 | '@discordjs/ws@1.2.3': 91 | resolution: {integrity: sha512-wPlQDxEmlDg5IxhJPuxXr3Vy9AjYq5xCvFWGJyD7w7Np8ZGu+Mc+97LCoEc/+AYCo2IDpKioiH0/c/mj5ZR9Uw==} 92 | engines: {node: '>=16.11.0'} 93 | 94 | '@esbuild/aix-ppc64@0.25.12': 95 | resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} 96 | engines: {node: '>=18'} 97 | cpu: [ppc64] 98 | os: [aix] 99 | 100 | '@esbuild/android-arm64@0.25.12': 101 | resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} 102 | engines: {node: '>=18'} 103 | cpu: [arm64] 104 | os: [android] 105 | 106 | '@esbuild/android-arm@0.25.12': 107 | resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} 108 | engines: {node: '>=18'} 109 | cpu: [arm] 110 | os: [android] 111 | 112 | '@esbuild/android-x64@0.25.12': 113 | resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} 114 | engines: {node: '>=18'} 115 | cpu: [x64] 116 | os: [android] 117 | 118 | '@esbuild/darwin-arm64@0.25.12': 119 | resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} 120 | engines: {node: '>=18'} 121 | cpu: [arm64] 122 | os: [darwin] 123 | 124 | '@esbuild/darwin-x64@0.25.12': 125 | resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} 126 | engines: {node: '>=18'} 127 | cpu: [x64] 128 | os: [darwin] 129 | 130 | '@esbuild/freebsd-arm64@0.25.12': 131 | resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} 132 | engines: {node: '>=18'} 133 | cpu: [arm64] 134 | os: [freebsd] 135 | 136 | '@esbuild/freebsd-x64@0.25.12': 137 | resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} 138 | engines: {node: '>=18'} 139 | cpu: [x64] 140 | os: [freebsd] 141 | 142 | '@esbuild/linux-arm64@0.25.12': 143 | resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} 144 | engines: {node: '>=18'} 145 | cpu: [arm64] 146 | os: [linux] 147 | 148 | '@esbuild/linux-arm@0.25.12': 149 | resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} 150 | engines: {node: '>=18'} 151 | cpu: [arm] 152 | os: [linux] 153 | 154 | '@esbuild/linux-ia32@0.25.12': 155 | resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} 156 | engines: {node: '>=18'} 157 | cpu: [ia32] 158 | os: [linux] 159 | 160 | '@esbuild/linux-loong64@0.25.12': 161 | resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} 162 | engines: {node: '>=18'} 163 | cpu: [loong64] 164 | os: [linux] 165 | 166 | '@esbuild/linux-mips64el@0.25.12': 167 | resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} 168 | engines: {node: '>=18'} 169 | cpu: [mips64el] 170 | os: [linux] 171 | 172 | '@esbuild/linux-ppc64@0.25.12': 173 | resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} 174 | engines: {node: '>=18'} 175 | cpu: [ppc64] 176 | os: [linux] 177 | 178 | '@esbuild/linux-riscv64@0.25.12': 179 | resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} 180 | engines: {node: '>=18'} 181 | cpu: [riscv64] 182 | os: [linux] 183 | 184 | '@esbuild/linux-s390x@0.25.12': 185 | resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} 186 | engines: {node: '>=18'} 187 | cpu: [s390x] 188 | os: [linux] 189 | 190 | '@esbuild/linux-x64@0.25.12': 191 | resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} 192 | engines: {node: '>=18'} 193 | cpu: [x64] 194 | os: [linux] 195 | 196 | '@esbuild/netbsd-arm64@0.25.12': 197 | resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==} 198 | engines: {node: '>=18'} 199 | cpu: [arm64] 200 | os: [netbsd] 201 | 202 | '@esbuild/netbsd-x64@0.25.12': 203 | resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} 204 | engines: {node: '>=18'} 205 | cpu: [x64] 206 | os: [netbsd] 207 | 208 | '@esbuild/openbsd-arm64@0.25.12': 209 | resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==} 210 | engines: {node: '>=18'} 211 | cpu: [arm64] 212 | os: [openbsd] 213 | 214 | '@esbuild/openbsd-x64@0.25.12': 215 | resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} 216 | engines: {node: '>=18'} 217 | cpu: [x64] 218 | os: [openbsd] 219 | 220 | '@esbuild/openharmony-arm64@0.25.12': 221 | resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==} 222 | engines: {node: '>=18'} 223 | cpu: [arm64] 224 | os: [openharmony] 225 | 226 | '@esbuild/sunos-x64@0.25.12': 227 | resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} 228 | engines: {node: '>=18'} 229 | cpu: [x64] 230 | os: [sunos] 231 | 232 | '@esbuild/win32-arm64@0.25.12': 233 | resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} 234 | engines: {node: '>=18'} 235 | cpu: [arm64] 236 | os: [win32] 237 | 238 | '@esbuild/win32-ia32@0.25.12': 239 | resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} 240 | engines: {node: '>=18'} 241 | cpu: [ia32] 242 | os: [win32] 243 | 244 | '@esbuild/win32-x64@0.25.12': 245 | resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} 246 | engines: {node: '>=18'} 247 | cpu: [x64] 248 | os: [win32] 249 | 250 | '@eslint-community/eslint-utils@4.9.0': 251 | resolution: {integrity: sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==} 252 | engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} 253 | peerDependencies: 254 | eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 255 | 256 | '@eslint-community/regexpp@4.12.2': 257 | resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} 258 | engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} 259 | 260 | '@eslint/config-array@0.21.1': 261 | resolution: {integrity: sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==} 262 | engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 263 | 264 | '@eslint/config-helpers@0.4.2': 265 | resolution: {integrity: sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==} 266 | engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 267 | 268 | '@eslint/core@0.17.0': 269 | resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==} 270 | engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 271 | 272 | '@eslint/eslintrc@3.3.1': 273 | resolution: {integrity: sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==} 274 | engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 275 | 276 | '@eslint/js@9.39.1': 277 | resolution: {integrity: sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw==} 278 | engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 279 | 280 | '@eslint/object-schema@2.1.7': 281 | resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==} 282 | engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 283 | 284 | '@eslint/plugin-kit@0.4.1': 285 | resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} 286 | engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 287 | 288 | '@humanfs/core@0.19.1': 289 | resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} 290 | engines: {node: '>=18.18.0'} 291 | 292 | '@humanfs/node@0.16.7': 293 | resolution: {integrity: sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==} 294 | engines: {node: '>=18.18.0'} 295 | 296 | '@humanwhocodes/module-importer@1.0.1': 297 | resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} 298 | engines: {node: '>=12.22'} 299 | 300 | '@humanwhocodes/retry@0.4.3': 301 | resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} 302 | engines: {node: '>=18.18'} 303 | 304 | '@nodelib/fs.scandir@2.1.5': 305 | resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} 306 | engines: {node: '>= 8'} 307 | 308 | '@nodelib/fs.stat@2.0.5': 309 | resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} 310 | engines: {node: '>= 8'} 311 | 312 | '@nodelib/fs.walk@1.2.8': 313 | resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} 314 | engines: {node: '>= 8'} 315 | 316 | '@rollup/rollup-android-arm-eabi@4.53.3': 317 | resolution: {integrity: sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w==} 318 | cpu: [arm] 319 | os: [android] 320 | 321 | '@rollup/rollup-android-arm64@4.53.3': 322 | resolution: {integrity: sha512-CbDGaMpdE9sh7sCmTrTUyllhrg65t6SwhjlMJsLr+J8YjFuPmCEjbBSx4Z/e4SmDyH3aB5hGaJUP2ltV/vcs4w==} 323 | cpu: [arm64] 324 | os: [android] 325 | 326 | '@rollup/rollup-darwin-arm64@4.53.3': 327 | resolution: {integrity: sha512-Nr7SlQeqIBpOV6BHHGZgYBuSdanCXuw09hon14MGOLGmXAFYjx1wNvquVPmpZnl0tLjg25dEdr4IQ6GgyToCUA==} 328 | cpu: [arm64] 329 | os: [darwin] 330 | 331 | '@rollup/rollup-darwin-x64@4.53.3': 332 | resolution: {integrity: sha512-DZ8N4CSNfl965CmPktJ8oBnfYr3F8dTTNBQkRlffnUarJ2ohudQD17sZBa097J8xhQ26AwhHJ5mvUyQW8ddTsQ==} 333 | cpu: [x64] 334 | os: [darwin] 335 | 336 | '@rollup/rollup-freebsd-arm64@4.53.3': 337 | resolution: {integrity: sha512-yMTrCrK92aGyi7GuDNtGn2sNW+Gdb4vErx4t3Gv/Tr+1zRb8ax4z8GWVRfr3Jw8zJWvpGHNpss3vVlbF58DZ4w==} 338 | cpu: [arm64] 339 | os: [freebsd] 340 | 341 | '@rollup/rollup-freebsd-x64@4.53.3': 342 | resolution: {integrity: sha512-lMfF8X7QhdQzseM6XaX0vbno2m3hlyZFhwcndRMw8fbAGUGL3WFMBdK0hbUBIUYcEcMhVLr1SIamDeuLBnXS+Q==} 343 | cpu: [x64] 344 | os: [freebsd] 345 | 346 | '@rollup/rollup-linux-arm-gnueabihf@4.53.3': 347 | resolution: {integrity: sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw==} 348 | cpu: [arm] 349 | os: [linux] 350 | 351 | '@rollup/rollup-linux-arm-musleabihf@4.53.3': 352 | resolution: {integrity: sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg==} 353 | cpu: [arm] 354 | os: [linux] 355 | 356 | '@rollup/rollup-linux-arm64-gnu@4.53.3': 357 | resolution: {integrity: sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w==} 358 | cpu: [arm64] 359 | os: [linux] 360 | 361 | '@rollup/rollup-linux-arm64-musl@4.53.3': 362 | resolution: {integrity: sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A==} 363 | cpu: [arm64] 364 | os: [linux] 365 | 366 | '@rollup/rollup-linux-loong64-gnu@4.53.3': 367 | resolution: {integrity: sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g==} 368 | cpu: [loong64] 369 | os: [linux] 370 | 371 | '@rollup/rollup-linux-ppc64-gnu@4.53.3': 372 | resolution: {integrity: sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw==} 373 | cpu: [ppc64] 374 | os: [linux] 375 | 376 | '@rollup/rollup-linux-riscv64-gnu@4.53.3': 377 | resolution: {integrity: sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g==} 378 | cpu: [riscv64] 379 | os: [linux] 380 | 381 | '@rollup/rollup-linux-riscv64-musl@4.53.3': 382 | resolution: {integrity: sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A==} 383 | cpu: [riscv64] 384 | os: [linux] 385 | 386 | '@rollup/rollup-linux-s390x-gnu@4.53.3': 387 | resolution: {integrity: sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg==} 388 | cpu: [s390x] 389 | os: [linux] 390 | 391 | '@rollup/rollup-linux-x64-gnu@4.53.3': 392 | resolution: {integrity: sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w==} 393 | cpu: [x64] 394 | os: [linux] 395 | 396 | '@rollup/rollup-linux-x64-musl@4.53.3': 397 | resolution: {integrity: sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q==} 398 | cpu: [x64] 399 | os: [linux] 400 | 401 | '@rollup/rollup-openharmony-arm64@4.53.3': 402 | resolution: {integrity: sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw==} 403 | cpu: [arm64] 404 | os: [openharmony] 405 | 406 | '@rollup/rollup-win32-arm64-msvc@4.53.3': 407 | resolution: {integrity: sha512-GOFuKpsxR/whszbF/bzydebLiXIHSgsEUp6M0JI8dWvi+fFa1TD6YQa4aSZHtpmh2/uAlj/Dy+nmby3TJ3pkTw==} 408 | cpu: [arm64] 409 | os: [win32] 410 | 411 | '@rollup/rollup-win32-ia32-msvc@4.53.3': 412 | resolution: {integrity: sha512-iah+THLcBJdpfZ1TstDFbKNznlzoxa8fmnFYK4V67HvmuNYkVdAywJSoteUszvBQ9/HqN2+9AZghbajMsFT+oA==} 413 | cpu: [ia32] 414 | os: [win32] 415 | 416 | '@rollup/rollup-win32-x64-gnu@4.53.3': 417 | resolution: {integrity: sha512-J9QDiOIZlZLdcot5NXEepDkstocktoVjkaKUtqzgzpt2yWjGlbYiKyp05rWwk4nypbYUNoFAztEgixoLaSETkg==} 418 | cpu: [x64] 419 | os: [win32] 420 | 421 | '@rollup/rollup-win32-x64-msvc@4.53.3': 422 | resolution: {integrity: sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ==} 423 | cpu: [x64] 424 | os: [win32] 425 | 426 | '@sapphire/async-queue@1.5.5': 427 | resolution: {integrity: sha512-cvGzxbba6sav2zZkH8GPf2oGk9yYoD5qrNWdu9fRehifgnFZJMV+nuy2nON2roRO4yQQ+v7MK/Pktl/HgfsUXg==} 428 | engines: {node: '>=v14.0.0', npm: '>=7.0.0'} 429 | 430 | '@sapphire/shapeshift@4.0.0': 431 | resolution: {integrity: sha512-d9dUmWVA7MMiKobL3VpLF8P2aeanRTu6ypG2OIaEv/ZHH/SUQ2iHOVyi5wAPjQ+HmnMuL0whK9ez8I/raWbtIg==} 432 | engines: {node: '>=v16'} 433 | 434 | '@sapphire/snowflake@3.5.3': 435 | resolution: {integrity: sha512-jjmJywLAFoWeBi1W7994zZyiNWPIiqRRNAmSERxyg93xRGzNYvGjlZ0gR6x0F4gPRi2+0O6S71kOZYyr3cxaIQ==} 436 | engines: {node: '>=v14.0.0', npm: '>=7.0.0'} 437 | 438 | '@types/estree@1.0.8': 439 | resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} 440 | 441 | '@types/json-schema@7.0.15': 442 | resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} 443 | 444 | '@types/luxon@3.7.1': 445 | resolution: {integrity: sha512-H3iskjFIAn5SlJU7OuxUmTEpebK6TKB8rxZShDslBMZJ5u9S//KM1sbdAisiSrqwLQncVjnpi2OK2J51h+4lsg==} 446 | 447 | '@types/node@24.10.1': 448 | resolution: {integrity: sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==} 449 | 450 | '@types/ws@8.18.1': 451 | resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} 452 | 453 | '@typescript-eslint/eslint-plugin@8.47.0': 454 | resolution: {integrity: sha512-fe0rz9WJQ5t2iaLfdbDc9T80GJy0AeO453q8C3YCilnGozvOyCG5t+EZtg7j7D88+c3FipfP/x+wzGnh1xp8ZA==} 455 | engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 456 | peerDependencies: 457 | '@typescript-eslint/parser': ^8.47.0 458 | eslint: ^8.57.0 || ^9.0.0 459 | typescript: '>=4.8.4 <6.0.0' 460 | 461 | '@typescript-eslint/parser@8.47.0': 462 | resolution: {integrity: sha512-lJi3PfxVmo0AkEY93ecfN+r8SofEqZNGByvHAI3GBLrvt1Cw6H5k1IM02nSzu0RfUafr2EvFSw0wAsZgubNplQ==} 463 | engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 464 | peerDependencies: 465 | eslint: ^8.57.0 || ^9.0.0 466 | typescript: '>=4.8.4 <6.0.0' 467 | 468 | '@typescript-eslint/project-service@8.47.0': 469 | resolution: {integrity: sha512-2X4BX8hUeB5JcA1TQJ7GjcgulXQ+5UkNb0DL8gHsHUHdFoiCTJoYLTpib3LtSDPZsRET5ygN4qqIWrHyYIKERA==} 470 | engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 471 | peerDependencies: 472 | typescript: '>=4.8.4 <6.0.0' 473 | 474 | '@typescript-eslint/scope-manager@8.47.0': 475 | resolution: {integrity: sha512-a0TTJk4HXMkfpFkL9/WaGTNuv7JWfFTQFJd6zS9dVAjKsojmv9HT55xzbEpnZoY+VUb+YXLMp+ihMLz/UlZfDg==} 476 | engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 477 | 478 | '@typescript-eslint/tsconfig-utils@8.47.0': 479 | resolution: {integrity: sha512-ybUAvjy4ZCL11uryalkKxuT3w3sXJAuWhOoGS3T/Wu+iUu1tGJmk5ytSY8gbdACNARmcYEB0COksD2j6hfGK2g==} 480 | engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 481 | peerDependencies: 482 | typescript: '>=4.8.4 <6.0.0' 483 | 484 | '@typescript-eslint/type-utils@8.47.0': 485 | resolution: {integrity: sha512-QC9RiCmZ2HmIdCEvhd1aJELBlD93ErziOXXlHEZyuBo3tBiAZieya0HLIxp+DoDWlsQqDawyKuNEhORyku+P8A==} 486 | engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 487 | peerDependencies: 488 | eslint: ^8.57.0 || ^9.0.0 489 | typescript: '>=4.8.4 <6.0.0' 490 | 491 | '@typescript-eslint/types@8.47.0': 492 | resolution: {integrity: sha512-nHAE6bMKsizhA2uuYZbEbmp5z2UpffNrPEqiKIeN7VsV6UY/roxanWfoRrf6x/k9+Obf+GQdkm0nPU+vnMXo9A==} 493 | engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 494 | 495 | '@typescript-eslint/typescript-estree@8.47.0': 496 | resolution: {integrity: sha512-k6ti9UepJf5NpzCjH31hQNLHQWupTRPhZ+KFF8WtTuTpy7uHPfeg2NM7cP27aCGajoEplxJDFVCEm9TGPYyiVg==} 497 | engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 498 | peerDependencies: 499 | typescript: '>=4.8.4 <6.0.0' 500 | 501 | '@typescript-eslint/utils@8.47.0': 502 | resolution: {integrity: sha512-g7XrNf25iL4TJOiPqatNuaChyqt49a/onq5YsJ9+hXeugK+41LVg7AxikMfM02PC6jbNtZLCJj6AUcQXJS/jGQ==} 503 | engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 504 | peerDependencies: 505 | eslint: ^8.57.0 || ^9.0.0 506 | typescript: '>=4.8.4 <6.0.0' 507 | 508 | '@typescript-eslint/visitor-keys@8.47.0': 509 | resolution: {integrity: sha512-SIV3/6eftCy1bNzCQoPmbWsRLujS8t5iDIZ4spZOBHqrM+yfX2ogg8Tt3PDTAVKw3sSCiUgg30uOAvK2r9zGjQ==} 510 | engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 511 | 512 | '@vladfrangu/async_event_emitter@2.4.7': 513 | resolution: {integrity: sha512-Xfe6rpCTxSxfbswi/W/Pz7zp1WWSNn4A0eW4mLkQUewCrXXtMj31lCg+iQyTkh/CkusZSq9eDflu7tjEDXUY6g==} 514 | engines: {node: '>=v14.0.0', npm: '>=7.0.0'} 515 | 516 | acorn-jsx@5.3.2: 517 | resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} 518 | peerDependencies: 519 | acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 520 | 521 | acorn@8.15.0: 522 | resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} 523 | engines: {node: '>=0.4.0'} 524 | hasBin: true 525 | 526 | ajv@6.12.6: 527 | resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} 528 | 529 | ansi-styles@4.3.0: 530 | resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} 531 | engines: {node: '>=8'} 532 | 533 | argparse@2.0.1: 534 | resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} 535 | 536 | balanced-match@1.0.2: 537 | resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} 538 | 539 | brace-expansion@1.1.12: 540 | resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} 541 | 542 | brace-expansion@2.0.2: 543 | resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} 544 | 545 | braces@3.0.3: 546 | resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} 547 | engines: {node: '>=8'} 548 | 549 | cac@6.7.14: 550 | resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} 551 | engines: {node: '>=8'} 552 | 553 | callsites@3.1.0: 554 | resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} 555 | engines: {node: '>=6'} 556 | 557 | chalk@4.1.2: 558 | resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} 559 | engines: {node: '>=10'} 560 | 561 | color-convert@2.0.1: 562 | resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} 563 | engines: {node: '>=7.0.0'} 564 | 565 | color-name@1.1.4: 566 | resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} 567 | 568 | concat-map@0.0.1: 569 | resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} 570 | 571 | cross-spawn@7.0.6: 572 | resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} 573 | engines: {node: '>= 8'} 574 | 575 | debug@4.4.3: 576 | resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} 577 | engines: {node: '>=6.0'} 578 | peerDependencies: 579 | supports-color: '*' 580 | peerDependenciesMeta: 581 | supports-color: 582 | optional: true 583 | 584 | deep-is@0.1.4: 585 | resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} 586 | 587 | discord-api-types@0.38.34: 588 | resolution: {integrity: sha512-muq7xKGznj5MSFCzuIm/2TO7DpttuomUTemVM82fRqgnMl70YRzEyY24jlbiV6R9tzOTq6A6UnZ0bsfZeKD38Q==} 589 | 590 | discord.js@14.25.1: 591 | resolution: {integrity: sha512-2l0gsPOLPs5t6GFZfQZKnL1OJNYFcuC/ETWsW4VtKVD/tg4ICa9x+jb9bkPffkMdRpRpuUaO/fKkHCBeiCKh8g==} 592 | engines: {node: '>=18'} 593 | 594 | dotenv@17.2.3: 595 | resolution: {integrity: sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==} 596 | engines: {node: '>=12'} 597 | 598 | es-module-lexer@1.7.0: 599 | resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} 600 | 601 | esbuild@0.25.12: 602 | resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} 603 | engines: {node: '>=18'} 604 | hasBin: true 605 | 606 | escape-string-regexp@4.0.0: 607 | resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} 608 | engines: {node: '>=10'} 609 | 610 | eslint-scope@8.4.0: 611 | resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} 612 | engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 613 | 614 | eslint-visitor-keys@3.4.3: 615 | resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} 616 | engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} 617 | 618 | eslint-visitor-keys@4.2.1: 619 | resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} 620 | engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 621 | 622 | eslint@9.39.1: 623 | resolution: {integrity: sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==} 624 | engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 625 | hasBin: true 626 | peerDependencies: 627 | jiti: '*' 628 | peerDependenciesMeta: 629 | jiti: 630 | optional: true 631 | 632 | espree@10.4.0: 633 | resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} 634 | engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 635 | 636 | esquery@1.6.0: 637 | resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} 638 | engines: {node: '>=0.10'} 639 | 640 | esrecurse@4.3.0: 641 | resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} 642 | engines: {node: '>=4.0'} 643 | 644 | estraverse@5.3.0: 645 | resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} 646 | engines: {node: '>=4.0'} 647 | 648 | esutils@2.0.3: 649 | resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} 650 | engines: {node: '>=0.10.0'} 651 | 652 | fast-deep-equal@3.1.3: 653 | resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} 654 | 655 | fast-glob@3.3.3: 656 | resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} 657 | engines: {node: '>=8.6.0'} 658 | 659 | fast-json-stable-stringify@2.1.0: 660 | resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} 661 | 662 | fast-levenshtein@2.0.6: 663 | resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} 664 | 665 | fastq@1.19.1: 666 | resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} 667 | 668 | fdir@6.5.0: 669 | resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} 670 | engines: {node: '>=12.0.0'} 671 | peerDependencies: 672 | picomatch: ^3 || ^4 673 | peerDependenciesMeta: 674 | picomatch: 675 | optional: true 676 | 677 | file-entry-cache@8.0.0: 678 | resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} 679 | engines: {node: '>=16.0.0'} 680 | 681 | fill-range@7.1.1: 682 | resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} 683 | engines: {node: '>=8'} 684 | 685 | find-up@5.0.0: 686 | resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} 687 | engines: {node: '>=10'} 688 | 689 | flat-cache@4.0.1: 690 | resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} 691 | engines: {node: '>=16'} 692 | 693 | flatted@3.3.3: 694 | resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} 695 | 696 | fsevents@2.3.3: 697 | resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} 698 | engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} 699 | os: [darwin] 700 | 701 | glob-parent@5.1.2: 702 | resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} 703 | engines: {node: '>= 6'} 704 | 705 | glob-parent@6.0.2: 706 | resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} 707 | engines: {node: '>=10.13.0'} 708 | 709 | globals@14.0.0: 710 | resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} 711 | engines: {node: '>=18'} 712 | 713 | globals@16.5.0: 714 | resolution: {integrity: sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==} 715 | engines: {node: '>=18'} 716 | 717 | graphemer@1.4.0: 718 | resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} 719 | 720 | has-flag@4.0.0: 721 | resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} 722 | engines: {node: '>=8'} 723 | 724 | ignore@5.3.2: 725 | resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} 726 | engines: {node: '>= 4'} 727 | 728 | ignore@7.0.5: 729 | resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} 730 | engines: {node: '>= 4'} 731 | 732 | import-fresh@3.3.1: 733 | resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} 734 | engines: {node: '>=6'} 735 | 736 | imurmurhash@0.1.4: 737 | resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} 738 | engines: {node: '>=0.8.19'} 739 | 740 | is-extglob@2.1.1: 741 | resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} 742 | engines: {node: '>=0.10.0'} 743 | 744 | is-glob@4.0.3: 745 | resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} 746 | engines: {node: '>=0.10.0'} 747 | 748 | is-number@7.0.0: 749 | resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} 750 | engines: {node: '>=0.12.0'} 751 | 752 | isexe@2.0.0: 753 | resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} 754 | 755 | js-yaml@4.1.1: 756 | resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} 757 | hasBin: true 758 | 759 | json-buffer@3.0.1: 760 | resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} 761 | 762 | json-schema-traverse@0.4.1: 763 | resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} 764 | 765 | json-stable-stringify-without-jsonify@1.0.1: 766 | resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} 767 | 768 | keyv@4.5.4: 769 | resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} 770 | 771 | levn@0.4.1: 772 | resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} 773 | engines: {node: '>= 0.8.0'} 774 | 775 | locate-path@6.0.0: 776 | resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} 777 | engines: {node: '>=10'} 778 | 779 | lodash.merge@4.6.2: 780 | resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} 781 | 782 | lodash.snakecase@4.1.1: 783 | resolution: {integrity: sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==} 784 | 785 | lodash@4.17.21: 786 | resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} 787 | 788 | luxon@3.7.2: 789 | resolution: {integrity: sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==} 790 | engines: {node: '>=12'} 791 | 792 | magic-bytes.js@1.12.1: 793 | resolution: {integrity: sha512-ThQLOhN86ZkJ7qemtVRGYM+gRgR8GEXNli9H/PMvpnZsE44Xfh3wx9kGJaldg314v85m+bFW6WBMaVHJc/c3zA==} 794 | 795 | merge2@1.4.1: 796 | resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} 797 | engines: {node: '>= 8'} 798 | 799 | micromatch@4.0.8: 800 | resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} 801 | engines: {node: '>=8.6'} 802 | 803 | minimatch@3.1.2: 804 | resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} 805 | 806 | minimatch@9.0.5: 807 | resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} 808 | engines: {node: '>=16 || 14 >=14.17'} 809 | 810 | ms@2.1.3: 811 | resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} 812 | 813 | nanoid@3.3.11: 814 | resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} 815 | engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} 816 | hasBin: true 817 | 818 | natural-compare@1.4.0: 819 | resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} 820 | 821 | npm-check-updates@19.1.2: 822 | resolution: {integrity: sha512-FNeFCVgPOj0fz89hOpGtxP2rnnRHR7hD2E8qNU8SMWfkyDZXA/xpgjsL3UMLSo3F/K13QvJDnbxPngulNDDo/g==} 823 | engines: {node: '>=20.0.0', npm: '>=8.12.1'} 824 | hasBin: true 825 | 826 | obug@2.1.0: 827 | resolution: {integrity: sha512-uu/tgLPoa75CFA7UDkmqspKbefvZh1WMPwkU3bNr0PY746a/+xwXVgbw5co5C3GvJj3h5u8g/pbxXzI0gd1QFg==} 828 | 829 | optionator@0.9.4: 830 | resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} 831 | engines: {node: '>= 0.8.0'} 832 | 833 | p-limit@3.1.0: 834 | resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} 835 | engines: {node: '>=10'} 836 | 837 | p-locate@5.0.0: 838 | resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} 839 | engines: {node: '>=10'} 840 | 841 | parent-module@1.0.1: 842 | resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} 843 | engines: {node: '>=6'} 844 | 845 | path-exists@4.0.0: 846 | resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} 847 | engines: {node: '>=8'} 848 | 849 | path-key@3.1.1: 850 | resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} 851 | engines: {node: '>=8'} 852 | 853 | pathe@2.0.3: 854 | resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} 855 | 856 | picocolors@1.1.1: 857 | resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} 858 | 859 | picomatch@2.3.1: 860 | resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} 861 | engines: {node: '>=8.6'} 862 | 863 | picomatch@4.0.3: 864 | resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} 865 | engines: {node: '>=12'} 866 | 867 | postcss@8.5.6: 868 | resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} 869 | engines: {node: ^10 || ^12 || >=14} 870 | 871 | prelude-ls@1.2.1: 872 | resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} 873 | engines: {node: '>= 0.8.0'} 874 | 875 | prettier@3.6.2: 876 | resolution: {integrity: sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==} 877 | engines: {node: '>=14'} 878 | hasBin: true 879 | 880 | punycode@2.3.1: 881 | resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} 882 | engines: {node: '>=6'} 883 | 884 | queue-microtask@1.2.3: 885 | resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} 886 | 887 | resolve-from@4.0.0: 888 | resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} 889 | engines: {node: '>=4'} 890 | 891 | reusify@1.1.0: 892 | resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} 893 | engines: {iojs: '>=1.0.0', node: '>=0.10.0'} 894 | 895 | rollup@4.53.3: 896 | resolution: {integrity: sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==} 897 | engines: {node: '>=18.0.0', npm: '>=8.0.0'} 898 | hasBin: true 899 | 900 | run-parallel@1.2.0: 901 | resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} 902 | 903 | semver@7.7.3: 904 | resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==} 905 | engines: {node: '>=10'} 906 | hasBin: true 907 | 908 | shebang-command@2.0.0: 909 | resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} 910 | engines: {node: '>=8'} 911 | 912 | shebang-regex@3.0.0: 913 | resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} 914 | engines: {node: '>=8'} 915 | 916 | source-map-js@1.2.1: 917 | resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} 918 | engines: {node: '>=0.10.0'} 919 | 920 | strip-json-comments@3.1.1: 921 | resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} 922 | engines: {node: '>=8'} 923 | 924 | supports-color@7.2.0: 925 | resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} 926 | engines: {node: '>=8'} 927 | 928 | tinyglobby@0.2.15: 929 | resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} 930 | engines: {node: '>=12.0.0'} 931 | 932 | to-regex-range@5.0.1: 933 | resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} 934 | engines: {node: '>=8.0'} 935 | 936 | ts-api-utils@2.1.0: 937 | resolution: {integrity: sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==} 938 | engines: {node: '>=18.12'} 939 | peerDependencies: 940 | typescript: '>=4.8.4' 941 | 942 | ts-mixer@6.0.4: 943 | resolution: {integrity: sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA==} 944 | 945 | tslib@2.8.1: 946 | resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} 947 | 948 | type-check@0.4.0: 949 | resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} 950 | engines: {node: '>= 0.8.0'} 951 | 952 | typescript-eslint@8.47.0: 953 | resolution: {integrity: sha512-Lwe8i2XQ3WoMjua/r1PHrCTpkubPYJCAfOurtn+mtTzqB6jNd+14n9UN1bJ4s3F49x9ixAm0FLflB/JzQ57M8Q==} 954 | engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 955 | peerDependencies: 956 | eslint: ^8.57.0 || ^9.0.0 957 | typescript: '>=4.8.4 <6.0.0' 958 | 959 | typescript@5.9.3: 960 | resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} 961 | engines: {node: '>=14.17'} 962 | hasBin: true 963 | 964 | undici-types@7.16.0: 965 | resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} 966 | 967 | undici@6.21.3: 968 | resolution: {integrity: sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw==} 969 | engines: {node: '>=18.17'} 970 | 971 | uri-js@4.4.1: 972 | resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} 973 | 974 | vite-node@5.2.0: 975 | resolution: {integrity: sha512-7UT39YxUukIA97zWPXUGb0SGSiLexEGlavMwU3HDE6+d/HJhKLjLqu4eX2qv6SQiocdhKLRcusroDwXHQ6CnRQ==} 976 | engines: {node: ^20.19.0 || >=22.12.0} 977 | hasBin: true 978 | 979 | vite@7.2.4: 980 | resolution: {integrity: sha512-NL8jTlbo0Tn4dUEXEsUg8KeyG/Lkmc4Fnzb8JXN/Ykm9G4HNImjtABMJgkQoVjOBN/j2WAwDTRytdqJbZsah7w==} 981 | engines: {node: ^20.19.0 || >=22.12.0} 982 | hasBin: true 983 | peerDependencies: 984 | '@types/node': ^20.19.0 || >=22.12.0 985 | jiti: '>=1.21.0' 986 | less: ^4.0.0 987 | lightningcss: ^1.21.0 988 | sass: ^1.70.0 989 | sass-embedded: ^1.70.0 990 | stylus: '>=0.54.8' 991 | sugarss: ^5.0.0 992 | terser: ^5.16.0 993 | tsx: ^4.8.1 994 | yaml: ^2.4.2 995 | peerDependenciesMeta: 996 | '@types/node': 997 | optional: true 998 | jiti: 999 | optional: true 1000 | less: 1001 | optional: true 1002 | lightningcss: 1003 | optional: true 1004 | sass: 1005 | optional: true 1006 | sass-embedded: 1007 | optional: true 1008 | stylus: 1009 | optional: true 1010 | sugarss: 1011 | optional: true 1012 | terser: 1013 | optional: true 1014 | tsx: 1015 | optional: true 1016 | yaml: 1017 | optional: true 1018 | 1019 | which@2.0.2: 1020 | resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} 1021 | engines: {node: '>= 8'} 1022 | hasBin: true 1023 | 1024 | word-wrap@1.2.5: 1025 | resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} 1026 | engines: {node: '>=0.10.0'} 1027 | 1028 | ws@8.18.3: 1029 | resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==} 1030 | engines: {node: '>=10.0.0'} 1031 | peerDependencies: 1032 | bufferutil: ^4.0.1 1033 | utf-8-validate: '>=5.0.2' 1034 | peerDependenciesMeta: 1035 | bufferutil: 1036 | optional: true 1037 | utf-8-validate: 1038 | optional: true 1039 | 1040 | yocto-queue@0.1.0: 1041 | resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} 1042 | engines: {node: '>=10'} 1043 | 1044 | snapshots: 1045 | 1046 | '@discordjs/builders@1.13.0': 1047 | dependencies: 1048 | '@discordjs/formatters': 0.6.2 1049 | '@discordjs/util': 1.2.0 1050 | '@sapphire/shapeshift': 4.0.0 1051 | discord-api-types: 0.38.34 1052 | fast-deep-equal: 3.1.3 1053 | ts-mixer: 6.0.4 1054 | tslib: 2.8.1 1055 | 1056 | '@discordjs/collection@1.5.3': {} 1057 | 1058 | '@discordjs/collection@2.1.1': {} 1059 | 1060 | '@discordjs/formatters@0.6.2': 1061 | dependencies: 1062 | discord-api-types: 0.38.34 1063 | 1064 | '@discordjs/rest@2.6.0': 1065 | dependencies: 1066 | '@discordjs/collection': 2.1.1 1067 | '@discordjs/util': 1.2.0 1068 | '@sapphire/async-queue': 1.5.5 1069 | '@sapphire/snowflake': 3.5.3 1070 | '@vladfrangu/async_event_emitter': 2.4.7 1071 | discord-api-types: 0.38.34 1072 | magic-bytes.js: 1.12.1 1073 | tslib: 2.8.1 1074 | undici: 6.21.3 1075 | 1076 | '@discordjs/util@1.2.0': 1077 | dependencies: 1078 | discord-api-types: 0.38.34 1079 | 1080 | '@discordjs/ws@1.2.3': 1081 | dependencies: 1082 | '@discordjs/collection': 2.1.1 1083 | '@discordjs/rest': 2.6.0 1084 | '@discordjs/util': 1.2.0 1085 | '@sapphire/async-queue': 1.5.5 1086 | '@types/ws': 8.18.1 1087 | '@vladfrangu/async_event_emitter': 2.4.7 1088 | discord-api-types: 0.38.34 1089 | tslib: 2.8.1 1090 | ws: 8.18.3 1091 | transitivePeerDependencies: 1092 | - bufferutil 1093 | - utf-8-validate 1094 | 1095 | '@esbuild/aix-ppc64@0.25.12': 1096 | optional: true 1097 | 1098 | '@esbuild/android-arm64@0.25.12': 1099 | optional: true 1100 | 1101 | '@esbuild/android-arm@0.25.12': 1102 | optional: true 1103 | 1104 | '@esbuild/android-x64@0.25.12': 1105 | optional: true 1106 | 1107 | '@esbuild/darwin-arm64@0.25.12': 1108 | optional: true 1109 | 1110 | '@esbuild/darwin-x64@0.25.12': 1111 | optional: true 1112 | 1113 | '@esbuild/freebsd-arm64@0.25.12': 1114 | optional: true 1115 | 1116 | '@esbuild/freebsd-x64@0.25.12': 1117 | optional: true 1118 | 1119 | '@esbuild/linux-arm64@0.25.12': 1120 | optional: true 1121 | 1122 | '@esbuild/linux-arm@0.25.12': 1123 | optional: true 1124 | 1125 | '@esbuild/linux-ia32@0.25.12': 1126 | optional: true 1127 | 1128 | '@esbuild/linux-loong64@0.25.12': 1129 | optional: true 1130 | 1131 | '@esbuild/linux-mips64el@0.25.12': 1132 | optional: true 1133 | 1134 | '@esbuild/linux-ppc64@0.25.12': 1135 | optional: true 1136 | 1137 | '@esbuild/linux-riscv64@0.25.12': 1138 | optional: true 1139 | 1140 | '@esbuild/linux-s390x@0.25.12': 1141 | optional: true 1142 | 1143 | '@esbuild/linux-x64@0.25.12': 1144 | optional: true 1145 | 1146 | '@esbuild/netbsd-arm64@0.25.12': 1147 | optional: true 1148 | 1149 | '@esbuild/netbsd-x64@0.25.12': 1150 | optional: true 1151 | 1152 | '@esbuild/openbsd-arm64@0.25.12': 1153 | optional: true 1154 | 1155 | '@esbuild/openbsd-x64@0.25.12': 1156 | optional: true 1157 | 1158 | '@esbuild/openharmony-arm64@0.25.12': 1159 | optional: true 1160 | 1161 | '@esbuild/sunos-x64@0.25.12': 1162 | optional: true 1163 | 1164 | '@esbuild/win32-arm64@0.25.12': 1165 | optional: true 1166 | 1167 | '@esbuild/win32-ia32@0.25.12': 1168 | optional: true 1169 | 1170 | '@esbuild/win32-x64@0.25.12': 1171 | optional: true 1172 | 1173 | '@eslint-community/eslint-utils@4.9.0(eslint@9.39.1)': 1174 | dependencies: 1175 | eslint: 9.39.1 1176 | eslint-visitor-keys: 3.4.3 1177 | 1178 | '@eslint-community/regexpp@4.12.2': {} 1179 | 1180 | '@eslint/config-array@0.21.1': 1181 | dependencies: 1182 | '@eslint/object-schema': 2.1.7 1183 | debug: 4.4.3 1184 | minimatch: 3.1.2 1185 | transitivePeerDependencies: 1186 | - supports-color 1187 | 1188 | '@eslint/config-helpers@0.4.2': 1189 | dependencies: 1190 | '@eslint/core': 0.17.0 1191 | 1192 | '@eslint/core@0.17.0': 1193 | dependencies: 1194 | '@types/json-schema': 7.0.15 1195 | 1196 | '@eslint/eslintrc@3.3.1': 1197 | dependencies: 1198 | ajv: 6.12.6 1199 | debug: 4.4.3 1200 | espree: 10.4.0 1201 | globals: 14.0.0 1202 | ignore: 5.3.2 1203 | import-fresh: 3.3.1 1204 | js-yaml: 4.1.1 1205 | minimatch: 3.1.2 1206 | strip-json-comments: 3.1.1 1207 | transitivePeerDependencies: 1208 | - supports-color 1209 | 1210 | '@eslint/js@9.39.1': {} 1211 | 1212 | '@eslint/object-schema@2.1.7': {} 1213 | 1214 | '@eslint/plugin-kit@0.4.1': 1215 | dependencies: 1216 | '@eslint/core': 0.17.0 1217 | levn: 0.4.1 1218 | 1219 | '@humanfs/core@0.19.1': {} 1220 | 1221 | '@humanfs/node@0.16.7': 1222 | dependencies: 1223 | '@humanfs/core': 0.19.1 1224 | '@humanwhocodes/retry': 0.4.3 1225 | 1226 | '@humanwhocodes/module-importer@1.0.1': {} 1227 | 1228 | '@humanwhocodes/retry@0.4.3': {} 1229 | 1230 | '@nodelib/fs.scandir@2.1.5': 1231 | dependencies: 1232 | '@nodelib/fs.stat': 2.0.5 1233 | run-parallel: 1.2.0 1234 | 1235 | '@nodelib/fs.stat@2.0.5': {} 1236 | 1237 | '@nodelib/fs.walk@1.2.8': 1238 | dependencies: 1239 | '@nodelib/fs.scandir': 2.1.5 1240 | fastq: 1.19.1 1241 | 1242 | '@rollup/rollup-android-arm-eabi@4.53.3': 1243 | optional: true 1244 | 1245 | '@rollup/rollup-android-arm64@4.53.3': 1246 | optional: true 1247 | 1248 | '@rollup/rollup-darwin-arm64@4.53.3': 1249 | optional: true 1250 | 1251 | '@rollup/rollup-darwin-x64@4.53.3': 1252 | optional: true 1253 | 1254 | '@rollup/rollup-freebsd-arm64@4.53.3': 1255 | optional: true 1256 | 1257 | '@rollup/rollup-freebsd-x64@4.53.3': 1258 | optional: true 1259 | 1260 | '@rollup/rollup-linux-arm-gnueabihf@4.53.3': 1261 | optional: true 1262 | 1263 | '@rollup/rollup-linux-arm-musleabihf@4.53.3': 1264 | optional: true 1265 | 1266 | '@rollup/rollup-linux-arm64-gnu@4.53.3': 1267 | optional: true 1268 | 1269 | '@rollup/rollup-linux-arm64-musl@4.53.3': 1270 | optional: true 1271 | 1272 | '@rollup/rollup-linux-loong64-gnu@4.53.3': 1273 | optional: true 1274 | 1275 | '@rollup/rollup-linux-ppc64-gnu@4.53.3': 1276 | optional: true 1277 | 1278 | '@rollup/rollup-linux-riscv64-gnu@4.53.3': 1279 | optional: true 1280 | 1281 | '@rollup/rollup-linux-riscv64-musl@4.53.3': 1282 | optional: true 1283 | 1284 | '@rollup/rollup-linux-s390x-gnu@4.53.3': 1285 | optional: true 1286 | 1287 | '@rollup/rollup-linux-x64-gnu@4.53.3': 1288 | optional: true 1289 | 1290 | '@rollup/rollup-linux-x64-musl@4.53.3': 1291 | optional: true 1292 | 1293 | '@rollup/rollup-openharmony-arm64@4.53.3': 1294 | optional: true 1295 | 1296 | '@rollup/rollup-win32-arm64-msvc@4.53.3': 1297 | optional: true 1298 | 1299 | '@rollup/rollup-win32-ia32-msvc@4.53.3': 1300 | optional: true 1301 | 1302 | '@rollup/rollup-win32-x64-gnu@4.53.3': 1303 | optional: true 1304 | 1305 | '@rollup/rollup-win32-x64-msvc@4.53.3': 1306 | optional: true 1307 | 1308 | '@sapphire/async-queue@1.5.5': {} 1309 | 1310 | '@sapphire/shapeshift@4.0.0': 1311 | dependencies: 1312 | fast-deep-equal: 3.1.3 1313 | lodash: 4.17.21 1314 | 1315 | '@sapphire/snowflake@3.5.3': {} 1316 | 1317 | '@types/estree@1.0.8': {} 1318 | 1319 | '@types/json-schema@7.0.15': {} 1320 | 1321 | '@types/luxon@3.7.1': {} 1322 | 1323 | '@types/node@24.10.1': 1324 | dependencies: 1325 | undici-types: 7.16.0 1326 | 1327 | '@types/ws@8.18.1': 1328 | dependencies: 1329 | '@types/node': 24.10.1 1330 | 1331 | '@typescript-eslint/eslint-plugin@8.47.0(@typescript-eslint/parser@8.47.0(eslint@9.39.1)(typescript@5.9.3))(eslint@9.39.1)(typescript@5.9.3)': 1332 | dependencies: 1333 | '@eslint-community/regexpp': 4.12.2 1334 | '@typescript-eslint/parser': 8.47.0(eslint@9.39.1)(typescript@5.9.3) 1335 | '@typescript-eslint/scope-manager': 8.47.0 1336 | '@typescript-eslint/type-utils': 8.47.0(eslint@9.39.1)(typescript@5.9.3) 1337 | '@typescript-eslint/utils': 8.47.0(eslint@9.39.1)(typescript@5.9.3) 1338 | '@typescript-eslint/visitor-keys': 8.47.0 1339 | eslint: 9.39.1 1340 | graphemer: 1.4.0 1341 | ignore: 7.0.5 1342 | natural-compare: 1.4.0 1343 | ts-api-utils: 2.1.0(typescript@5.9.3) 1344 | typescript: 5.9.3 1345 | transitivePeerDependencies: 1346 | - supports-color 1347 | 1348 | '@typescript-eslint/parser@8.47.0(eslint@9.39.1)(typescript@5.9.3)': 1349 | dependencies: 1350 | '@typescript-eslint/scope-manager': 8.47.0 1351 | '@typescript-eslint/types': 8.47.0 1352 | '@typescript-eslint/typescript-estree': 8.47.0(typescript@5.9.3) 1353 | '@typescript-eslint/visitor-keys': 8.47.0 1354 | debug: 4.4.3 1355 | eslint: 9.39.1 1356 | typescript: 5.9.3 1357 | transitivePeerDependencies: 1358 | - supports-color 1359 | 1360 | '@typescript-eslint/project-service@8.47.0(typescript@5.9.3)': 1361 | dependencies: 1362 | '@typescript-eslint/tsconfig-utils': 8.47.0(typescript@5.9.3) 1363 | '@typescript-eslint/types': 8.47.0 1364 | debug: 4.4.3 1365 | typescript: 5.9.3 1366 | transitivePeerDependencies: 1367 | - supports-color 1368 | 1369 | '@typescript-eslint/scope-manager@8.47.0': 1370 | dependencies: 1371 | '@typescript-eslint/types': 8.47.0 1372 | '@typescript-eslint/visitor-keys': 8.47.0 1373 | 1374 | '@typescript-eslint/tsconfig-utils@8.47.0(typescript@5.9.3)': 1375 | dependencies: 1376 | typescript: 5.9.3 1377 | 1378 | '@typescript-eslint/type-utils@8.47.0(eslint@9.39.1)(typescript@5.9.3)': 1379 | dependencies: 1380 | '@typescript-eslint/types': 8.47.0 1381 | '@typescript-eslint/typescript-estree': 8.47.0(typescript@5.9.3) 1382 | '@typescript-eslint/utils': 8.47.0(eslint@9.39.1)(typescript@5.9.3) 1383 | debug: 4.4.3 1384 | eslint: 9.39.1 1385 | ts-api-utils: 2.1.0(typescript@5.9.3) 1386 | typescript: 5.9.3 1387 | transitivePeerDependencies: 1388 | - supports-color 1389 | 1390 | '@typescript-eslint/types@8.47.0': {} 1391 | 1392 | '@typescript-eslint/typescript-estree@8.47.0(typescript@5.9.3)': 1393 | dependencies: 1394 | '@typescript-eslint/project-service': 8.47.0(typescript@5.9.3) 1395 | '@typescript-eslint/tsconfig-utils': 8.47.0(typescript@5.9.3) 1396 | '@typescript-eslint/types': 8.47.0 1397 | '@typescript-eslint/visitor-keys': 8.47.0 1398 | debug: 4.4.3 1399 | fast-glob: 3.3.3 1400 | is-glob: 4.0.3 1401 | minimatch: 9.0.5 1402 | semver: 7.7.3 1403 | ts-api-utils: 2.1.0(typescript@5.9.3) 1404 | typescript: 5.9.3 1405 | transitivePeerDependencies: 1406 | - supports-color 1407 | 1408 | '@typescript-eslint/utils@8.47.0(eslint@9.39.1)(typescript@5.9.3)': 1409 | dependencies: 1410 | '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1) 1411 | '@typescript-eslint/scope-manager': 8.47.0 1412 | '@typescript-eslint/types': 8.47.0 1413 | '@typescript-eslint/typescript-estree': 8.47.0(typescript@5.9.3) 1414 | eslint: 9.39.1 1415 | typescript: 5.9.3 1416 | transitivePeerDependencies: 1417 | - supports-color 1418 | 1419 | '@typescript-eslint/visitor-keys@8.47.0': 1420 | dependencies: 1421 | '@typescript-eslint/types': 8.47.0 1422 | eslint-visitor-keys: 4.2.1 1423 | 1424 | '@vladfrangu/async_event_emitter@2.4.7': {} 1425 | 1426 | acorn-jsx@5.3.2(acorn@8.15.0): 1427 | dependencies: 1428 | acorn: 8.15.0 1429 | 1430 | acorn@8.15.0: {} 1431 | 1432 | ajv@6.12.6: 1433 | dependencies: 1434 | fast-deep-equal: 3.1.3 1435 | fast-json-stable-stringify: 2.1.0 1436 | json-schema-traverse: 0.4.1 1437 | uri-js: 4.4.1 1438 | 1439 | ansi-styles@4.3.0: 1440 | dependencies: 1441 | color-convert: 2.0.1 1442 | 1443 | argparse@2.0.1: {} 1444 | 1445 | balanced-match@1.0.2: {} 1446 | 1447 | brace-expansion@1.1.12: 1448 | dependencies: 1449 | balanced-match: 1.0.2 1450 | concat-map: 0.0.1 1451 | 1452 | brace-expansion@2.0.2: 1453 | dependencies: 1454 | balanced-match: 1.0.2 1455 | 1456 | braces@3.0.3: 1457 | dependencies: 1458 | fill-range: 7.1.1 1459 | 1460 | cac@6.7.14: {} 1461 | 1462 | callsites@3.1.0: {} 1463 | 1464 | chalk@4.1.2: 1465 | dependencies: 1466 | ansi-styles: 4.3.0 1467 | supports-color: 7.2.0 1468 | 1469 | color-convert@2.0.1: 1470 | dependencies: 1471 | color-name: 1.1.4 1472 | 1473 | color-name@1.1.4: {} 1474 | 1475 | concat-map@0.0.1: {} 1476 | 1477 | cross-spawn@7.0.6: 1478 | dependencies: 1479 | path-key: 3.1.1 1480 | shebang-command: 2.0.0 1481 | which: 2.0.2 1482 | 1483 | debug@4.4.3: 1484 | dependencies: 1485 | ms: 2.1.3 1486 | 1487 | deep-is@0.1.4: {} 1488 | 1489 | discord-api-types@0.38.34: {} 1490 | 1491 | discord.js@14.25.1: 1492 | dependencies: 1493 | '@discordjs/builders': 1.13.0 1494 | '@discordjs/collection': 1.5.3 1495 | '@discordjs/formatters': 0.6.2 1496 | '@discordjs/rest': 2.6.0 1497 | '@discordjs/util': 1.2.0 1498 | '@discordjs/ws': 1.2.3 1499 | '@sapphire/snowflake': 3.5.3 1500 | discord-api-types: 0.38.34 1501 | fast-deep-equal: 3.1.3 1502 | lodash.snakecase: 4.1.1 1503 | magic-bytes.js: 1.12.1 1504 | tslib: 2.8.1 1505 | undici: 6.21.3 1506 | transitivePeerDependencies: 1507 | - bufferutil 1508 | - utf-8-validate 1509 | 1510 | dotenv@17.2.3: {} 1511 | 1512 | es-module-lexer@1.7.0: {} 1513 | 1514 | esbuild@0.25.12: 1515 | optionalDependencies: 1516 | '@esbuild/aix-ppc64': 0.25.12 1517 | '@esbuild/android-arm': 0.25.12 1518 | '@esbuild/android-arm64': 0.25.12 1519 | '@esbuild/android-x64': 0.25.12 1520 | '@esbuild/darwin-arm64': 0.25.12 1521 | '@esbuild/darwin-x64': 0.25.12 1522 | '@esbuild/freebsd-arm64': 0.25.12 1523 | '@esbuild/freebsd-x64': 0.25.12 1524 | '@esbuild/linux-arm': 0.25.12 1525 | '@esbuild/linux-arm64': 0.25.12 1526 | '@esbuild/linux-ia32': 0.25.12 1527 | '@esbuild/linux-loong64': 0.25.12 1528 | '@esbuild/linux-mips64el': 0.25.12 1529 | '@esbuild/linux-ppc64': 0.25.12 1530 | '@esbuild/linux-riscv64': 0.25.12 1531 | '@esbuild/linux-s390x': 0.25.12 1532 | '@esbuild/linux-x64': 0.25.12 1533 | '@esbuild/netbsd-arm64': 0.25.12 1534 | '@esbuild/netbsd-x64': 0.25.12 1535 | '@esbuild/openbsd-arm64': 0.25.12 1536 | '@esbuild/openbsd-x64': 0.25.12 1537 | '@esbuild/openharmony-arm64': 0.25.12 1538 | '@esbuild/sunos-x64': 0.25.12 1539 | '@esbuild/win32-arm64': 0.25.12 1540 | '@esbuild/win32-ia32': 0.25.12 1541 | '@esbuild/win32-x64': 0.25.12 1542 | 1543 | escape-string-regexp@4.0.0: {} 1544 | 1545 | eslint-scope@8.4.0: 1546 | dependencies: 1547 | esrecurse: 4.3.0 1548 | estraverse: 5.3.0 1549 | 1550 | eslint-visitor-keys@3.4.3: {} 1551 | 1552 | eslint-visitor-keys@4.2.1: {} 1553 | 1554 | eslint@9.39.1: 1555 | dependencies: 1556 | '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1) 1557 | '@eslint-community/regexpp': 4.12.2 1558 | '@eslint/config-array': 0.21.1 1559 | '@eslint/config-helpers': 0.4.2 1560 | '@eslint/core': 0.17.0 1561 | '@eslint/eslintrc': 3.3.1 1562 | '@eslint/js': 9.39.1 1563 | '@eslint/plugin-kit': 0.4.1 1564 | '@humanfs/node': 0.16.7 1565 | '@humanwhocodes/module-importer': 1.0.1 1566 | '@humanwhocodes/retry': 0.4.3 1567 | '@types/estree': 1.0.8 1568 | ajv: 6.12.6 1569 | chalk: 4.1.2 1570 | cross-spawn: 7.0.6 1571 | debug: 4.4.3 1572 | escape-string-regexp: 4.0.0 1573 | eslint-scope: 8.4.0 1574 | eslint-visitor-keys: 4.2.1 1575 | espree: 10.4.0 1576 | esquery: 1.6.0 1577 | esutils: 2.0.3 1578 | fast-deep-equal: 3.1.3 1579 | file-entry-cache: 8.0.0 1580 | find-up: 5.0.0 1581 | glob-parent: 6.0.2 1582 | ignore: 5.3.2 1583 | imurmurhash: 0.1.4 1584 | is-glob: 4.0.3 1585 | json-stable-stringify-without-jsonify: 1.0.1 1586 | lodash.merge: 4.6.2 1587 | minimatch: 3.1.2 1588 | natural-compare: 1.4.0 1589 | optionator: 0.9.4 1590 | transitivePeerDependencies: 1591 | - supports-color 1592 | 1593 | espree@10.4.0: 1594 | dependencies: 1595 | acorn: 8.15.0 1596 | acorn-jsx: 5.3.2(acorn@8.15.0) 1597 | eslint-visitor-keys: 4.2.1 1598 | 1599 | esquery@1.6.0: 1600 | dependencies: 1601 | estraverse: 5.3.0 1602 | 1603 | esrecurse@4.3.0: 1604 | dependencies: 1605 | estraverse: 5.3.0 1606 | 1607 | estraverse@5.3.0: {} 1608 | 1609 | esutils@2.0.3: {} 1610 | 1611 | fast-deep-equal@3.1.3: {} 1612 | 1613 | fast-glob@3.3.3: 1614 | dependencies: 1615 | '@nodelib/fs.stat': 2.0.5 1616 | '@nodelib/fs.walk': 1.2.8 1617 | glob-parent: 5.1.2 1618 | merge2: 1.4.1 1619 | micromatch: 4.0.8 1620 | 1621 | fast-json-stable-stringify@2.1.0: {} 1622 | 1623 | fast-levenshtein@2.0.6: {} 1624 | 1625 | fastq@1.19.1: 1626 | dependencies: 1627 | reusify: 1.1.0 1628 | 1629 | fdir@6.5.0(picomatch@4.0.3): 1630 | optionalDependencies: 1631 | picomatch: 4.0.3 1632 | 1633 | file-entry-cache@8.0.0: 1634 | dependencies: 1635 | flat-cache: 4.0.1 1636 | 1637 | fill-range@7.1.1: 1638 | dependencies: 1639 | to-regex-range: 5.0.1 1640 | 1641 | find-up@5.0.0: 1642 | dependencies: 1643 | locate-path: 6.0.0 1644 | path-exists: 4.0.0 1645 | 1646 | flat-cache@4.0.1: 1647 | dependencies: 1648 | flatted: 3.3.3 1649 | keyv: 4.5.4 1650 | 1651 | flatted@3.3.3: {} 1652 | 1653 | fsevents@2.3.3: 1654 | optional: true 1655 | 1656 | glob-parent@5.1.2: 1657 | dependencies: 1658 | is-glob: 4.0.3 1659 | 1660 | glob-parent@6.0.2: 1661 | dependencies: 1662 | is-glob: 4.0.3 1663 | 1664 | globals@14.0.0: {} 1665 | 1666 | globals@16.5.0: {} 1667 | 1668 | graphemer@1.4.0: {} 1669 | 1670 | has-flag@4.0.0: {} 1671 | 1672 | ignore@5.3.2: {} 1673 | 1674 | ignore@7.0.5: {} 1675 | 1676 | import-fresh@3.3.1: 1677 | dependencies: 1678 | parent-module: 1.0.1 1679 | resolve-from: 4.0.0 1680 | 1681 | imurmurhash@0.1.4: {} 1682 | 1683 | is-extglob@2.1.1: {} 1684 | 1685 | is-glob@4.0.3: 1686 | dependencies: 1687 | is-extglob: 2.1.1 1688 | 1689 | is-number@7.0.0: {} 1690 | 1691 | isexe@2.0.0: {} 1692 | 1693 | js-yaml@4.1.1: 1694 | dependencies: 1695 | argparse: 2.0.1 1696 | 1697 | json-buffer@3.0.1: {} 1698 | 1699 | json-schema-traverse@0.4.1: {} 1700 | 1701 | json-stable-stringify-without-jsonify@1.0.1: {} 1702 | 1703 | keyv@4.5.4: 1704 | dependencies: 1705 | json-buffer: 3.0.1 1706 | 1707 | levn@0.4.1: 1708 | dependencies: 1709 | prelude-ls: 1.2.1 1710 | type-check: 0.4.0 1711 | 1712 | locate-path@6.0.0: 1713 | dependencies: 1714 | p-locate: 5.0.0 1715 | 1716 | lodash.merge@4.6.2: {} 1717 | 1718 | lodash.snakecase@4.1.1: {} 1719 | 1720 | lodash@4.17.21: {} 1721 | 1722 | luxon@3.7.2: {} 1723 | 1724 | magic-bytes.js@1.12.1: {} 1725 | 1726 | merge2@1.4.1: {} 1727 | 1728 | micromatch@4.0.8: 1729 | dependencies: 1730 | braces: 3.0.3 1731 | picomatch: 2.3.1 1732 | 1733 | minimatch@3.1.2: 1734 | dependencies: 1735 | brace-expansion: 1.1.12 1736 | 1737 | minimatch@9.0.5: 1738 | dependencies: 1739 | brace-expansion: 2.0.2 1740 | 1741 | ms@2.1.3: {} 1742 | 1743 | nanoid@3.3.11: {} 1744 | 1745 | natural-compare@1.4.0: {} 1746 | 1747 | npm-check-updates@19.1.2: {} 1748 | 1749 | obug@2.1.0: {} 1750 | 1751 | optionator@0.9.4: 1752 | dependencies: 1753 | deep-is: 0.1.4 1754 | fast-levenshtein: 2.0.6 1755 | levn: 0.4.1 1756 | prelude-ls: 1.2.1 1757 | type-check: 0.4.0 1758 | word-wrap: 1.2.5 1759 | 1760 | p-limit@3.1.0: 1761 | dependencies: 1762 | yocto-queue: 0.1.0 1763 | 1764 | p-locate@5.0.0: 1765 | dependencies: 1766 | p-limit: 3.1.0 1767 | 1768 | parent-module@1.0.1: 1769 | dependencies: 1770 | callsites: 3.1.0 1771 | 1772 | path-exists@4.0.0: {} 1773 | 1774 | path-key@3.1.1: {} 1775 | 1776 | pathe@2.0.3: {} 1777 | 1778 | picocolors@1.1.1: {} 1779 | 1780 | picomatch@2.3.1: {} 1781 | 1782 | picomatch@4.0.3: {} 1783 | 1784 | postcss@8.5.6: 1785 | dependencies: 1786 | nanoid: 3.3.11 1787 | picocolors: 1.1.1 1788 | source-map-js: 1.2.1 1789 | 1790 | prelude-ls@1.2.1: {} 1791 | 1792 | prettier@3.6.2: {} 1793 | 1794 | punycode@2.3.1: {} 1795 | 1796 | queue-microtask@1.2.3: {} 1797 | 1798 | resolve-from@4.0.0: {} 1799 | 1800 | reusify@1.1.0: {} 1801 | 1802 | rollup@4.53.3: 1803 | dependencies: 1804 | '@types/estree': 1.0.8 1805 | optionalDependencies: 1806 | '@rollup/rollup-android-arm-eabi': 4.53.3 1807 | '@rollup/rollup-android-arm64': 4.53.3 1808 | '@rollup/rollup-darwin-arm64': 4.53.3 1809 | '@rollup/rollup-darwin-x64': 4.53.3 1810 | '@rollup/rollup-freebsd-arm64': 4.53.3 1811 | '@rollup/rollup-freebsd-x64': 4.53.3 1812 | '@rollup/rollup-linux-arm-gnueabihf': 4.53.3 1813 | '@rollup/rollup-linux-arm-musleabihf': 4.53.3 1814 | '@rollup/rollup-linux-arm64-gnu': 4.53.3 1815 | '@rollup/rollup-linux-arm64-musl': 4.53.3 1816 | '@rollup/rollup-linux-loong64-gnu': 4.53.3 1817 | '@rollup/rollup-linux-ppc64-gnu': 4.53.3 1818 | '@rollup/rollup-linux-riscv64-gnu': 4.53.3 1819 | '@rollup/rollup-linux-riscv64-musl': 4.53.3 1820 | '@rollup/rollup-linux-s390x-gnu': 4.53.3 1821 | '@rollup/rollup-linux-x64-gnu': 4.53.3 1822 | '@rollup/rollup-linux-x64-musl': 4.53.3 1823 | '@rollup/rollup-openharmony-arm64': 4.53.3 1824 | '@rollup/rollup-win32-arm64-msvc': 4.53.3 1825 | '@rollup/rollup-win32-ia32-msvc': 4.53.3 1826 | '@rollup/rollup-win32-x64-gnu': 4.53.3 1827 | '@rollup/rollup-win32-x64-msvc': 4.53.3 1828 | fsevents: 2.3.3 1829 | 1830 | run-parallel@1.2.0: 1831 | dependencies: 1832 | queue-microtask: 1.2.3 1833 | 1834 | semver@7.7.3: {} 1835 | 1836 | shebang-command@2.0.0: 1837 | dependencies: 1838 | shebang-regex: 3.0.0 1839 | 1840 | shebang-regex@3.0.0: {} 1841 | 1842 | source-map-js@1.2.1: {} 1843 | 1844 | strip-json-comments@3.1.1: {} 1845 | 1846 | supports-color@7.2.0: 1847 | dependencies: 1848 | has-flag: 4.0.0 1849 | 1850 | tinyglobby@0.2.15: 1851 | dependencies: 1852 | fdir: 6.5.0(picomatch@4.0.3) 1853 | picomatch: 4.0.3 1854 | 1855 | to-regex-range@5.0.1: 1856 | dependencies: 1857 | is-number: 7.0.0 1858 | 1859 | ts-api-utils@2.1.0(typescript@5.9.3): 1860 | dependencies: 1861 | typescript: 5.9.3 1862 | 1863 | ts-mixer@6.0.4: {} 1864 | 1865 | tslib@2.8.1: {} 1866 | 1867 | type-check@0.4.0: 1868 | dependencies: 1869 | prelude-ls: 1.2.1 1870 | 1871 | typescript-eslint@8.47.0(eslint@9.39.1)(typescript@5.9.3): 1872 | dependencies: 1873 | '@typescript-eslint/eslint-plugin': 8.47.0(@typescript-eslint/parser@8.47.0(eslint@9.39.1)(typescript@5.9.3))(eslint@9.39.1)(typescript@5.9.3) 1874 | '@typescript-eslint/parser': 8.47.0(eslint@9.39.1)(typescript@5.9.3) 1875 | '@typescript-eslint/typescript-estree': 8.47.0(typescript@5.9.3) 1876 | '@typescript-eslint/utils': 8.47.0(eslint@9.39.1)(typescript@5.9.3) 1877 | eslint: 9.39.1 1878 | typescript: 5.9.3 1879 | transitivePeerDependencies: 1880 | - supports-color 1881 | 1882 | typescript@5.9.3: {} 1883 | 1884 | undici-types@7.16.0: {} 1885 | 1886 | undici@6.21.3: {} 1887 | 1888 | uri-js@4.4.1: 1889 | dependencies: 1890 | punycode: 2.3.1 1891 | 1892 | vite-node@5.2.0(@types/node@24.10.1): 1893 | dependencies: 1894 | cac: 6.7.14 1895 | es-module-lexer: 1.7.0 1896 | obug: 2.1.0 1897 | pathe: 2.0.3 1898 | vite: 7.2.4(@types/node@24.10.1) 1899 | transitivePeerDependencies: 1900 | - '@types/node' 1901 | - jiti 1902 | - less 1903 | - lightningcss 1904 | - sass 1905 | - sass-embedded 1906 | - stylus 1907 | - sugarss 1908 | - terser 1909 | - tsx 1910 | - yaml 1911 | 1912 | vite@7.2.4(@types/node@24.10.1): 1913 | dependencies: 1914 | esbuild: 0.25.12 1915 | fdir: 6.5.0(picomatch@4.0.3) 1916 | picomatch: 4.0.3 1917 | postcss: 8.5.6 1918 | rollup: 4.53.3 1919 | tinyglobby: 0.2.15 1920 | optionalDependencies: 1921 | '@types/node': 24.10.1 1922 | fsevents: 2.3.3 1923 | 1924 | which@2.0.2: 1925 | dependencies: 1926 | isexe: 2.0.0 1927 | 1928 | word-wrap@1.2.5: {} 1929 | 1930 | ws@8.18.3: {} 1931 | 1932 | yocto-queue@0.1.0: {} 1933 | --------------------------------------------------------------------------------