From 9fcb06f97766cdb30fb39a1a9c87bf16b13a17e2 Mon Sep 17 00:00:00 2001 From: yurisasc Date: Mon, 29 Jan 2024 22:03:00 +0700 Subject: [PATCH] first commit --- .github/workflows/publish-discord-bot.yml | 48 + .github/workflows/publish-watchdog.yml | 28 + .gitignore | 2 + LICENSE | 201 ++ cdk/.env.sample | 27 + cdk/.gitignore | 13 + cdk/.npmignore | 6 + cdk/.prettierrc | 6 + cdk/bin/cdk.ts | 42 + cdk/cdk.json | 11 + cdk/lib/config.ts | 51 + cdk/lib/constants.ts | 12 + cdk/lib/cw-global-resource-policy.ts | 60 + cdk/lib/domain-stack.ts | 156 + cdk/lib/palworld-stack.ts | 425 +++ cdk/lib/ssm-parameter-reader.ts | 35 + cdk/lib/types.ts | 211 ++ cdk/lib/util.ts | 38 + cdk/minecraft-env.json | 21 + cdk/package-lock.json | 863 +++++ cdk/package.json | 28 + cdk/tsconfig.json | 30 + discord-bot/.dockerignore | 11 + discord-bot/.env.example | 5 + discord-bot/.gitattributes | 2 + discord-bot/.gitignore | 104 + discord-bot/Dockerfile | 23 + discord-bot/README.md | 43 + discord-bot/config/config.json | 30 + discord-bot/lang/lang.common.json | 25 + discord-bot/lang/lang.en-GB.json | 145 + discord-bot/lang/lang.en-US.json | 145 + discord-bot/lang/logs.json | 60 + discord-bot/package-lock.json | 3070 +++++++++++++++++ discord-bot/package.json | 42 + discord-bot/src/commands/args.ts | 49 + discord-bot/src/commands/chat/index.ts | 2 + .../src/commands/chat/start-server-command.ts | 57 + .../src/commands/chat/stop-server-command.ts | 58 + discord-bot/src/commands/command.ts | 28 + discord-bot/src/commands/index.ts | 3 + discord-bot/src/commands/metadata.ts | 51 + discord-bot/src/constants/discord-limits.ts | 3 + discord-bot/src/constants/index.ts | 1 + discord-bot/src/enums/help-option.ts | 4 + discord-bot/src/events/command-handler.ts | 192 ++ discord-bot/src/events/event-handler.ts | 3 + discord-bot/src/events/index.ts | 2 + discord-bot/src/extensions/custom-client.ts | 23 + discord-bot/src/models/bot.ts | 86 + .../src/models/enum-helpers/language.ts | 123 + discord-bot/src/models/internal-models.ts | 37 + discord-bot/src/services/aws-service.ts | 46 + .../services/command-registration-service.ts | 180 + .../src/services/event-data-service.ts | 42 + discord-bot/src/services/index.ts | 5 + discord-bot/src/services/lang.ts | 91 + discord-bot/src/services/logger.ts | 92 + .../src/services/multi-servers-service.ts | 37 + discord-bot/src/start-bot.ts | 95 + discord-bot/src/utils/client-utils.ts | 14 + discord-bot/src/utils/command-utils.ts | 63 + discord-bot/src/utils/format-utils.ts | 33 + discord-bot/src/utils/index.ts | 4 + discord-bot/src/utils/interaction-utils.ts | 185 + discord-bot/tsconfig.json | 19 + docs/diagrams/.gitkeep | 0 docs/diagrams/aws_architecture.drawio | 147 + docs/diagrams/aws_architecture.drawio.png | Bin 0 -> 68539 bytes lambda/lambda_function.py | 35 + palworld-ecsfargate-watchdog/Dockerfile | 15 + palworld-ecsfargate-watchdog/README.md | 15 + palworld-ecsfargate-watchdog/watchdog.sh | 170 + 73 files changed, 8029 insertions(+) create mode 100644 .github/workflows/publish-discord-bot.yml create mode 100644 .github/workflows/publish-watchdog.yml create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 cdk/.env.sample create mode 100644 cdk/.gitignore create mode 100644 cdk/.npmignore create mode 100644 cdk/.prettierrc create mode 100644 cdk/bin/cdk.ts create mode 100644 cdk/cdk.json create mode 100644 cdk/lib/config.ts create mode 100644 cdk/lib/constants.ts create mode 100644 cdk/lib/cw-global-resource-policy.ts create mode 100644 cdk/lib/domain-stack.ts create mode 100644 cdk/lib/palworld-stack.ts create mode 100644 cdk/lib/ssm-parameter-reader.ts create mode 100644 cdk/lib/types.ts create mode 100644 cdk/lib/util.ts create mode 100644 cdk/minecraft-env.json create mode 100644 cdk/package-lock.json create mode 100644 cdk/package.json create mode 100644 cdk/tsconfig.json create mode 100644 discord-bot/.dockerignore create mode 100644 discord-bot/.env.example create mode 100644 discord-bot/.gitattributes create mode 100644 discord-bot/.gitignore create mode 100644 discord-bot/Dockerfile create mode 100644 discord-bot/README.md create mode 100644 discord-bot/config/config.json create mode 100644 discord-bot/lang/lang.common.json create mode 100644 discord-bot/lang/lang.en-GB.json create mode 100644 discord-bot/lang/lang.en-US.json create mode 100644 discord-bot/lang/logs.json create mode 100644 discord-bot/package-lock.json create mode 100644 discord-bot/package.json create mode 100644 discord-bot/src/commands/args.ts create mode 100644 discord-bot/src/commands/chat/index.ts create mode 100644 discord-bot/src/commands/chat/start-server-command.ts create mode 100644 discord-bot/src/commands/chat/stop-server-command.ts create mode 100644 discord-bot/src/commands/command.ts create mode 100644 discord-bot/src/commands/index.ts create mode 100644 discord-bot/src/commands/metadata.ts create mode 100644 discord-bot/src/constants/discord-limits.ts create mode 100644 discord-bot/src/constants/index.ts create mode 100644 discord-bot/src/enums/help-option.ts create mode 100644 discord-bot/src/events/command-handler.ts create mode 100644 discord-bot/src/events/event-handler.ts create mode 100644 discord-bot/src/events/index.ts create mode 100644 discord-bot/src/extensions/custom-client.ts create mode 100644 discord-bot/src/models/bot.ts create mode 100644 discord-bot/src/models/enum-helpers/language.ts create mode 100644 discord-bot/src/models/internal-models.ts create mode 100644 discord-bot/src/services/aws-service.ts create mode 100644 discord-bot/src/services/command-registration-service.ts create mode 100644 discord-bot/src/services/event-data-service.ts create mode 100644 discord-bot/src/services/index.ts create mode 100644 discord-bot/src/services/lang.ts create mode 100644 discord-bot/src/services/logger.ts create mode 100644 discord-bot/src/services/multi-servers-service.ts create mode 100644 discord-bot/src/start-bot.ts create mode 100644 discord-bot/src/utils/client-utils.ts create mode 100644 discord-bot/src/utils/command-utils.ts create mode 100644 discord-bot/src/utils/format-utils.ts create mode 100644 discord-bot/src/utils/index.ts create mode 100644 discord-bot/src/utils/interaction-utils.ts create mode 100644 discord-bot/tsconfig.json create mode 100644 docs/diagrams/.gitkeep create mode 100644 docs/diagrams/aws_architecture.drawio create mode 100644 docs/diagrams/aws_architecture.drawio.png create mode 100644 lambda/lambda_function.py create mode 100644 palworld-ecsfargate-watchdog/Dockerfile create mode 100644 palworld-ecsfargate-watchdog/README.md create mode 100755 palworld-ecsfargate-watchdog/watchdog.sh diff --git a/.github/workflows/publish-discord-bot.yml b/.github/workflows/publish-discord-bot.yml new file mode 100644 index 0000000..baad561 --- /dev/null +++ b/.github/workflows/publish-discord-bot.yml @@ -0,0 +1,48 @@ +name: Publish Discord Bot +on: + push: + branches: + - main + paths: + - 'discord-bot/**' + workflow_dispatch: +jobs: + build-and-publish: + runs-on: ubuntu-latest + strategy: + matrix: + os: [ubuntu-latest] + arch: [arm64] + steps: + - name: Checkout code + uses: actions/checkout@v2 + - name: Re-register commands + working-directory: ./discord-bot + env: + CLIENT_ID: ${{ secrets.DISCORD_CLIENT_ID }} + CLIENT_TOKEN: ${{ secrets.DISCORD_CLIENT_TOKEN }} + run: | + npm install + npm run commands:clear + npm run commands:register + - name: Set up QEMU + uses: docker/setup-qemu-action@v1 + with: + platforms: ${{ matrix.arch }} + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v1 + - name: Login to Docker Hub + uses: docker/login-action@v1 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_PASSWORD }} + - name: Build and push Docker image + uses: docker/build-push-action@v2 + with: + context: ./discord-bot + push: true + platforms: linux/arm64 + tags: ${{ secrets.DOCKERHUB_USERNAME }}/mc-discord-bot:latest + - name: Logout of Docker Hub + run: | + docker logout \ No newline at end of file diff --git a/.github/workflows/publish-watchdog.yml b/.github/workflows/publish-watchdog.yml new file mode 100644 index 0000000..ccf9266 --- /dev/null +++ b/.github/workflows/publish-watchdog.yml @@ -0,0 +1,28 @@ +name: Publish Watchdog +on: + push: + branches: + - main + paths: + - 'palworld-ecsfargate-watchdog/watchdog.sh' + workflow_dispatch: +jobs: + build-and-publish: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v2 + - name: Login to Docker Hub + uses: docker/login-action@v1 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_PASSWORD }} + - name: Build and tag Docker image + uses: docker/build-push-action@v2 + with: + context: ./palworld-ecsfargate-watchdog + push: true + tags: ${{ secrets.DOCKERHUB_USERNAME }}/palworld-ecsfargate-watchdog:latest + - name: Logout of Docker Hub + run: | + docker logout diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d48c759 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.idea +.vscode \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/cdk/.env.sample b/cdk/.env.sample new file mode 100644 index 0000000..514e292 --- /dev/null +++ b/cdk/.env.sample @@ -0,0 +1,27 @@ +DOMAIN_NAME = example.com + +# Optional +CLUSTER_NAME = palworld +SERVICE_NAME = palworld-server +SUBDOMAIN_PART = palworld +SERVER_REGION = ap-northeast-1 +STARTUP_MINUTES = 10 +SHUTDOWN_MINUTES = 20 +USE_FARGATE_SPOT = true +TASK_MEMORY = 16384 +TASK_CPU = 4096 +VPC_ID = +PALWORLD_IMAGE_ENV_VARS_JSON = {"TZ":"Europe/Berlin","ALWAYS_UPDATE_ON_START":"true","MULTITHREAD_ENABLED":"true","COMMUNITY_SERVER":"true","BACKUP_ENABLED":"true","BACKUP_CRON_EXPRESSION":"0 * * * *","STEAMCMD_VALIDATE_FILES":"true","SERVER_SETTINGS_MODE":"auto","NETSERVERMAXTICKRATE":"120","DIFFICULTY":"None","DAYTIME_SPEEDRATE":"1.000000","NIGHTTIME_SPEEDRATE":"1.000000","EXP_RATE":"1.000000","PAL_CAPTURE_RATE":"1.000000","PAL_SPAWN_NUM_RATE":"1.000000","PAL_DAMAGE_RATE_ATTACK":"1.000000","PAL_DAMAGE_RATE_DEFENSE":"1.000000","PLAYER_DAMAGE_RATE_ATTACK":"1.000000","PLAYER_DAMAGE_RATE_DEFENSE":"1.000000","PLAYER_STOMACH_DECREASE_RATE":"1.000000","PLAYER_STAMINA_DECREACE_RATE":"1.000000","PLAYER_AUTO_HP_REGENE_RATE":"1.000000","PLAYER_AUTO_HP_REGENE_RATE_IN_SLEEP":"1.000000","PAL_STOMACH_DECREACE_RATE":"1.000000","PAL_STAMINA_DECREACE_RATE":"1.000000","PAL_AUTO_HP_REGENE_RATE":"1.000000","PAL_AUTO_HP_REGENE_RATE_IN_SLEEP":"1.000000","BUILD_OBJECT_DAMAGE_RATE":"1.000000","BUILD_OBJECT_DETERIORATION_DAMAGE_RATE":"1.000000","COLLECTION_DROP_RATE":"1.000000","COLLECTION_OBJECT_HP_RATE":"1.000000","COLLECTION_OBJECT_RESPAWN_SPEED_RATE":"1.000000","ENEMY_DROP_ITEM_RATE":"1.000000","DEATH_PENALTY":"All","ENABLE_PLAYER_TO_PLAYER_DAMAGE":"false","ENABLE_FRIENDLY_FIRE":"false","ENABLE_INVADER_ENEMY":"true","ACTIVE_UNKO":"false","ENABLE_AIM_ASSIST_PAD":"true","ENABLE_AIM_ASSIST_KEYBOARD":"false","DROP_ITEM_MAX_NUM":"3000","DROP_ITEM_MAX_NUM_UNKO":"100","BASE_CAMP_MAX_NUM":"128","BASE_CAMP_WORKER_MAXNUM":"15","DROP_ITEM_ALIVE_MAX_HOURS":"1.000000","AUTO_RESET_GUILD_NO_ONLINE_PLAYERS":"false","AUTO_RESET_GUILD_TIME_NO_ONLINE_PLAYERS":"72.000000","GUILD_PLAYER_MAX_NUM":"20","PAL_EGG_DEFAULT_HATCHING_TIME":"72.000000","WORK_SPEED_RATE":"1.000000","IS_MULTIPLAY":"false","IS_PVP":"false","CAN_PICKUP_OTHER_GUILD_DEATH_PENALTY_DROP":"false","ENABLE_NON_LOGIN_PENALTY":"true","ENABLE_FAST_TRAVEL":"true","IS_START_LOCATION_SELECT_BY_MAP":"true","EXIST_PLAYER_AFTER_LOGOUT":"false","ENABLE_DEFENSE_OTHER_GUILD_PLAYER":"false","COOP_PLAYER_MAX_NUM":"4","MAX_PLAYERS":"32","SERVER_NAME":"jammsen-docker-generated-###RANDOM###","SERVER_DESCRIPTION":"Palworld-Dedicated-Server running in Docker by jammsen","ADMIN_PASSWORD":"adminPasswordHere","SERVER_PASSWORD":"serverPasswordHere","PUBLIC_PORT":"8211","PUBLIC_IP":"","RCON_ENABLED":"false","RCON_PORT":"25575","REGION":"","USEAUTH":"true","BAN_LIST_URL":"https://api.palworldgame.com/api/banlist.txt"} +RCON_PASSWORD = +DISCORD_WEBHOOK_URLS = "https://discord.com/api/webhook1,https://discord.com/api/webhook2" +SNS_EMAIL_ADDRESS = +TWILIO_PHONE_FROM = +TWILIO_PHONE_TO = +TWILIO_ACCOUNT_ID = +TWILIO_AUTH_CODE = + +# Advanced +DEBUG = true +CDK_NEW_BOOTSTRAP = 1 +EXTRA_TCP_PORTS = [] +EXTRA_UDP_PORTS = [] \ No newline at end of file diff --git a/cdk/.gitignore b/cdk/.gitignore new file mode 100644 index 0000000..4f9a9a5 --- /dev/null +++ b/cdk/.gitignore @@ -0,0 +1,13 @@ +*.js +!jest.config.js +*.d.ts +node_modules + +# CDK asset staging directory +.cdk.staging +cdk.out + +# User context +cdk.context.json +# User options +.env diff --git a/cdk/.npmignore b/cdk/.npmignore new file mode 100644 index 0000000..c1d6d45 --- /dev/null +++ b/cdk/.npmignore @@ -0,0 +1,6 @@ +*.ts +!*.d.ts + +# CDK asset staging directory +.cdk.staging +cdk.out diff --git a/cdk/.prettierrc b/cdk/.prettierrc new file mode 100644 index 0000000..d598841 --- /dev/null +++ b/cdk/.prettierrc @@ -0,0 +1,6 @@ +{ + "singleQuote": true, + "trailingComma": "es5", + "arrowParens": "avoid", + "printWidth": 80 +} diff --git a/cdk/bin/cdk.ts b/cdk/bin/cdk.ts new file mode 100644 index 0000000..62d5727 --- /dev/null +++ b/cdk/bin/cdk.ts @@ -0,0 +1,42 @@ +#!/usr/bin/env node +import 'source-map-support/register'; +import * as cdk from 'aws-cdk-lib'; +import { PalworldStack } from '../lib/palworld-stack'; +import { DomainStack } from '../lib/domain-stack'; +import { constants } from '../lib/constants'; +import { resolveConfig } from '../lib/config'; + +const app = new cdk.App(); + +const config = resolveConfig(); + +if (!config.domainName) { + throw new Error( + 'Missing required `DOMAIN_NAME` in .env file, please rename\ + `.env.sample` to `.env` and add your domain name.' + ); +} + +const domainStack = new DomainStack(app, 'palworld-domain-stack', { + env: { + /** + * Because we are relying on Route 53+CloudWatch to invoke the Lambda function, + * it _must_ reside in the N. Virginia (us-east-1) region. + */ + region: constants.DOMAIN_STACK_REGION, + /* Account must be specified to allow for hosted zone lookup */ + account: process.env.CDK_DEFAULT_ACCOUNT, + }, + config, +}); + +const palworld = new PalworldStack(app, 'palworld-server-stack', { + env: { + region: config.serverRegion, + /* Account must be specified to allow for VPC lookup */ + account: process.env.CDK_DEFAULT_ACCOUNT, + }, + config, +}); + +palworld.addDependency(domainStack); diff --git a/cdk/cdk.json b/cdk/cdk.json new file mode 100644 index 0000000..b241f6a --- /dev/null +++ b/cdk/cdk.json @@ -0,0 +1,11 @@ +{ + "app": "npx ts-node --prefer-ts-exts bin/cdk.ts", + "context": { + "@aws-cdk/aws-apigateway:usagePlanKeyOrderInsensitiveId": true, + "@aws-cdk/core:stackRelativeExports": "true", + "@aws-cdk/aws-rds:lowercaseDbIdentifier": true, + "@aws-cdk/aws-lambda:recognizeVersionProps": true, + "@aws-cdk/customresources:installLatestAwsSdkDefault": false + }, + "requireApproval": "never" +} diff --git a/cdk/lib/config.ts b/cdk/lib/config.ts new file mode 100644 index 0000000..db7af1c --- /dev/null +++ b/cdk/lib/config.ts @@ -0,0 +1,51 @@ +import * as dotenv from 'dotenv'; +import * as path from 'path'; +import { PalworldImageEnv, StackConfig } from './types'; +import { stringAsBoolean, stringAsNumberArray } from './util'; + +dotenv.config({ path: path.resolve(__dirname, '../.env') }); + +const resolvePalworldEnvVars = (json = ''): PalworldImageEnv => { + const defaults = { EULA: 'TRUE' }; + try { + return { + ...defaults, + ...JSON.parse(json), + }; + } catch (e) { + console.error( + 'Unable to resolve .env value for PALWORLD_IMAGE_ENV_VARS_JSON.\ + Defaults will be used' + ); + return defaults; + } +}; + +export const resolveConfig = (): StackConfig => ({ + domainName: process.env.DOMAIN_NAME || '', + subdomainPart: process.env.SUBDOMAIN_PART || 'palworld', + serverRegion: process.env.SERVER_REGION || 'us-east-1', + shutdownMinutes: process.env.SHUTDOWN_MINUTES || '20', + startupMinutes: process.env.STARTUP_MINUTES || '10', + useFargateSpot: stringAsBoolean(process.env.USE_FARGATE_SPOT) || false, + taskCpu: +(process.env.TASK_CPU || 1024), + taskMemory: +(process.env.TASK_MEMORY || 2048), + vpcId: process.env.VPC_ID || '', + palworldImageEnv: resolvePalworldEnvVars( + process.env.PALWORLD_IMAGE_ENV_VARS_JSON + ), + rconPassword: process.env.RCON_PASSWORD!, // Rather fail than have an empty password + snsEmailAddress: process.env.SNS_EMAIL_ADDRESS || '', + twilio: { + phoneFrom: process.env.TWILIO_PHONE_FROM || '', + phoneTo: process.env.TWILIO_PHONE_TO || '', + accountId: process.env.TWILIO_ACCOUNT_ID || '', + authCode: process.env.TWILIO_AUTH_CODE || '', + }, + discord: { + webhookUrls: process.env.DISCORD_WEBHOOK_URLS || '', + }, + debug: stringAsBoolean(process.env.DEBUG) || false, + extraTcpPorts: stringAsNumberArray(process.env.EXTRA_TCP_PORTS) || [], + extraUdpPorts: stringAsNumberArray(process.env.EXTRA_UDP_PORTS) || [], +}); diff --git a/cdk/lib/constants.ts b/cdk/lib/constants.ts new file mode 100644 index 0000000..b065e57 --- /dev/null +++ b/cdk/lib/constants.ts @@ -0,0 +1,12 @@ +export const constants = { + CLUSTER_NAME: process.env.CLUSTER_NAME ?? 'palworld', + SERVICE_NAME: process.env.SERVICE_NAME ?? 'palworld-server', + PALWORLD_SERVER_CONTAINER_NAME: 'palworld-server', + WATCHDOG_SERVER_CONTAINER_NAME: 'palworld-ecsfargate-watchdog', + DOMAIN_STACK_REGION: 'us-east-1', + ECS_VOLUME_NAME: 'data', + HOSTED_ZONE_SSM_PARAMETER: 'PalworldHostedZoneID', + LAUNCHER_LAMBDA_ARN_SSM_PARAMETER: 'LauncherLambdaRoleArn', + PALWORLD_DOCKER_IMAGE: 'jammsen/palworld-dedicated-server:latest', + RCON_DOCKER_IMAGE: 'outdead/rcon:latest', +}; diff --git a/cdk/lib/cw-global-resource-policy.ts b/cdk/lib/cw-global-resource-policy.ts new file mode 100644 index 0000000..b3c2a4b --- /dev/null +++ b/cdk/lib/cw-global-resource-policy.ts @@ -0,0 +1,60 @@ +import { custom_resources as cr, aws_iam as iam, Duration } from 'aws-cdk-lib'; +import { RetentionDays } from 'aws-cdk-lib/aws-logs'; +import { Construct } from 'constructs'; + +interface CWGlobalResourcePolicyProps { + statements: iam.PolicyStatement[]; + policyName: string; +} + +/** + * Cloudwatch logs have global resource policies that allow EventBridge to + * write logs to a given Cloudwatch Log group. This is currently not + * implemented with CDK, so we use a Custom Resource here. + * See https://github.com/aws/aws-cdk/issues/5343 + */ +export class CWGlobalResourcePolicy extends cr.AwsCustomResource { + constructor( + scope: Construct, + name: string, + props: CWGlobalResourcePolicyProps + ) { + const { statements, policyName } = props; + + const putResourcePolicy: cr.AwsSdkCall = { + service: 'CloudWatchLogs', + action: 'putResourcePolicy', + parameters: { + policyName, + /** + * PolicyDocument must be provided as a string, so we can't use the + * iam.PolicyDocument provisions or other CDK niceties here. + */ + policyDocument: JSON.stringify({ + Version: '2012-10-17', + Statement: statements, + }), + }, + physicalResourceId: cr.PhysicalResourceId.of(policyName), + }; + + const deleteResourcePolicy: cr.AwsSdkCall = { + service: 'CloudWatchLogs', + action: 'deleteResourcePolicy', + parameters: { + policyName, + }, + }; + + super(scope, name, { + onUpdate: putResourcePolicy, + onCreate: putResourcePolicy, + onDelete: deleteResourcePolicy, + timeout: Duration.minutes(2), + policy: cr.AwsCustomResourcePolicy.fromSdkCalls({ + resources: cr.AwsCustomResourcePolicy.ANY_RESOURCE, + }), + logRetention: RetentionDays.THREE_DAYS, + }); + } +} diff --git a/cdk/lib/domain-stack.ts b/cdk/lib/domain-stack.ts new file mode 100644 index 0000000..33a886d --- /dev/null +++ b/cdk/lib/domain-stack.ts @@ -0,0 +1,156 @@ +import * as path from 'path'; +import { + Stack, + StackProps, + aws_lambda as lambda, + aws_route53 as route53, + aws_logs as logs, + aws_ssm as ssm, + aws_iam as iam, + aws_logs_destinations as logDestinations, + Duration, + RemovalPolicy, + Arn, + ArnFormat, +} from 'aws-cdk-lib'; +import { Construct } from 'constructs'; +import { constants } from './constants'; +import { CWGlobalResourcePolicy } from './cw-global-resource-policy'; +import { RetentionDays } from 'aws-cdk-lib/aws-logs'; +import { StackConfig } from './types'; + +interface DomainStackProps extends StackProps { + config: Readonly; +} + +export class DomainStack extends Stack { + constructor(scope: Construct, id: string, props: DomainStackProps) { + super(scope, id, props); + + const { config } = props; + + const subdomain = `${config.subdomainPart}.${config.domainName}`; + + const queryLogGroup = new logs.LogGroup(this, 'LogGroup', { + logGroupName: `/aws/route53/${subdomain}`, + retention: RetentionDays.THREE_DAYS, + removalPolicy: RemovalPolicy.DESTROY, + }); + + /* Create policy to allow route53 to log to cloudwatch */ + const policyName = 'cw.r.route53-dns'; + const dnsWriteToCw = [ + new iam.PolicyStatement({ + sid: 'AllowR53LogToCloudwatch', + effect: iam.Effect.ALLOW, + principals: [new iam.ServicePrincipal('route53.amazonaws.com')], + actions: ['logs:CreateLogStream', 'logs:PutLogEvents'], + resources: [ + Arn.format( + { + resource: 'log-group', + service: 'logs', + resourceName: '*', + arnFormat: ArnFormat.COLON_RESOURCE_NAME, + }, + this + ), + ], + }), + ]; + const cloudwatchLogResourcePolicy = new CWGlobalResourcePolicy( + this, + 'CloudwatchLogResourcePolicy', + { policyName, statements: dnsWriteToCw } + ); + + const subdomainHostedZone = route53.HostedZone.fromLookup( + this, + 'SubdomainHostedZone', + { + domainName: subdomain, + } + ); + + /* Resource policy for CloudWatch Logs is needed before the zone can be created */ + subdomainHostedZone.node.addDependency(cloudwatchLogResourcePolicy); + + const aRecord = new route53.ARecord(this, 'ARecord', { + target: { + /** + * The value of the record is irrelevant because it will be updated + * every time our container launches. + */ + values: ['192.168.1.1'], + }, + /** + * The low TTL is so that the DNS clients and non-authoritative DNS + * servers won't cache the record long and you can connect quicker after + * the IP updates. + */ + ttl: Duration.seconds(30), + recordName: subdomain, + zone: subdomainHostedZone, + }); + + /* Set dependency on A record to ensure it is removed first on deletion */ + aRecord.node.addDependency(subdomainHostedZone); + + const launcherLambda = new lambda.Function(this, 'LauncherLambda', { + code: lambda.Code.fromAsset(path.resolve(__dirname, '../../lambda')), + handler: 'lambda_function.lambda_handler', + runtime: lambda.Runtime.PYTHON_3_8, + environment: { + REGION: config.serverRegion, + CLUSTER: constants.CLUSTER_NAME, + SERVICE: constants.SERVICE_NAME, + }, + logRetention: logs.RetentionDays.THREE_DAYS, // TODO: parameterize + }); + + /** + * Give cloudwatch permission to invoke our lambda when our subscription filter + * picks up DNS queries. + */ + launcherLambda.addPermission('CWPermission', { + principal: new iam.ServicePrincipal( + `logs.${constants.DOMAIN_STACK_REGION}.amazonaws.com` + ), + action: 'lambda:InvokeFunction', + sourceAccount: this.account, + sourceArn: queryLogGroup.logGroupArn, + }); + + /** + * Create our log subscription filter to catch any log events containing + * our subdomain name and send them to our launcher lambda. + */ + queryLogGroup.addSubscriptionFilter('SubscriptionFilter', { + destination: new logDestinations.LambdaDestination(launcherLambda), + filterPattern: logs.FilterPattern.anyTerm(subdomain), + }); + + /** + * Add the subdomain hosted zone ID to SSM since we cannot consume a cross-stack + * references across regions. + */ + new ssm.StringParameter(this, 'HostedZoneParam', { + allowedPattern: '.*', + description: 'Hosted zone ID for palworld server', + parameterName: constants.HOSTED_ZONE_SSM_PARAMETER, + stringValue: subdomainHostedZone.hostedZoneId, + }); + + /** + * Add the ARN for the launcher lambda execution role to SSM so we can + * attach the policy for accessing the palworld server after it has been + * created. + */ + new ssm.StringParameter(this, 'LauncherLambdaParam', { + allowedPattern: '.*S.*', + description: 'Palworld launcher execution role ARN', + parameterName: constants.LAUNCHER_LAMBDA_ARN_SSM_PARAMETER, + stringValue: launcherLambda.role?.roleArn || '', + }); + } +} diff --git a/cdk/lib/palworld-stack.ts b/cdk/lib/palworld-stack.ts new file mode 100644 index 0000000..6f40a91 --- /dev/null +++ b/cdk/lib/palworld-stack.ts @@ -0,0 +1,425 @@ +import * as path from 'path'; +import { + Stack, + StackProps, + aws_ec2 as ec2, + aws_efs as efs, + aws_iam as iam, + aws_ecs as ecs, + aws_logs as logs, + aws_sns as sns, + RemovalPolicy, + Arn, + ArnFormat, +} from 'aws-cdk-lib'; +import { Construct } from 'constructs'; +import { constants } from './constants'; +import { SSMParameterReader } from './ssm-parameter-reader'; +import { StackConfig } from './types'; +import { + getPalworldServerConfig as getPalworldServerConfig, + isDockerInstalled, +} from './util'; + +interface PalworldStackProps extends StackProps { + config: Readonly; +} + +export class PalworldStack extends Stack { + constructor(scope: Construct, id: string, props: PalworldStackProps) { + super(scope, id, props); + + const { config } = props; + + const vpc = config.vpcId + ? ec2.Vpc.fromLookup(this, 'Vpc', { vpcId: config.vpcId }) + : new ec2.Vpc(this, 'Vpc', { + maxAzs: 3, + natGateways: 0, + }); + + const fileSystem = new efs.FileSystem(this, 'FileSystem', { + vpc, + removalPolicy: RemovalPolicy.SNAPSHOT, + }); + + const accessPoint = new efs.AccessPoint(this, 'AccessPoint', { + fileSystem, + path: '/palworld', + posixUser: { + uid: '1000', + gid: '1000', + }, + createAcl: { + ownerGid: '1000', + ownerUid: '1000', + permissions: '0755', + }, + }); + + const efsReadWriteDataPolicy = new iam.Policy(this, 'DataRWPolicy', { + statements: [ + new iam.PolicyStatement({ + sid: 'AllowReadWriteOnEFS', + effect: iam.Effect.ALLOW, + actions: [ + 'elasticfilesystem:ClientMount', + 'elasticfilesystem:ClientWrite', + 'elasticfilesystem:DescribeFileSystems', + ], + resources: [fileSystem.fileSystemArn], + conditions: { + StringEquals: { + 'elasticfilesystem:AccessPointArn': accessPoint.accessPointArn, + }, + }, + }), + ], + }); + + const ecsTaskRole = new iam.Role(this, 'TaskRole', { + assumedBy: new iam.ServicePrincipal('ecs-tasks.amazonaws.com'), + description: 'Palworld ECS task role', + }); + + efsReadWriteDataPolicy.attachToRole(ecsTaskRole); + + const cluster = new ecs.Cluster(this, 'Cluster', { + clusterName: constants.CLUSTER_NAME, + vpc, + containerInsights: true, // TODO: Add config for container insights + enableFargateCapacityProviders: true, + }); + + const palworldTaskDefinition = new ecs.FargateTaskDefinition( + this, + 'PalworldTaskDefinition', + { + taskRole: ecsTaskRole, + memoryLimitMiB: config.taskMemory, + cpu: config.taskCpu, + volumes: [ + { + name: constants.ECS_VOLUME_NAME, + efsVolumeConfiguration: { + fileSystemId: fileSystem.fileSystemId, + transitEncryption: 'ENABLED', + authorizationConfig: { + accessPointId: accessPoint.accessPointId, + iam: 'ENABLED', + }, + }, + }, + ], + } + ); + + const palwordServerConfig = getPalworldServerConfig(config.rconPassword); + + const palworldServerContainer = new ecs.ContainerDefinition( + this, + 'ServerContainer', + { + containerName: constants.PALWORLD_SERVER_CONTAINER_NAME, + image: ecs.ContainerImage.fromRegistry( + palwordServerConfig.server.image + ), + portMappings: [ + { + containerPort: palwordServerConfig.server.port, + hostPort: palwordServerConfig.server.port, + protocol: palwordServerConfig.server.protocol, + }, + ...config.extraTcpPorts.map(port => ({ + containerPort: port, + hostPort: port, + protocol: ecs.Protocol.TCP, + })), + ...config.extraUdpPorts.map(port => ({ + containerPort: port, + hostPort: port, + protocol: ecs.Protocol.UDP, + })), + ], + environment: config.palworldImageEnv, + essential: false, + taskDefinition: palworldTaskDefinition, + logging: config.debug + ? new ecs.AwsLogDriver({ + logRetention: logs.RetentionDays.THREE_DAYS, + streamPrefix: constants.PALWORLD_SERVER_CONTAINER_NAME, + }) + : undefined, + } + ); + + palworldServerContainer.addMountPoints({ + containerPath: '/data', + sourceVolume: constants.ECS_VOLUME_NAME, + readOnly: false, + }); + + const serviceSecurityGroup = new ec2.SecurityGroup( + this, + 'ServiceSecurityGroup', + { + vpc, + description: 'Security group for Palworld on-demand', + } + ); + + // Ingress rules for Palworld server, RCON, and extra ports + serviceSecurityGroup.addIngressRule( + ec2.Peer.anyIpv4(), + palwordServerConfig.server.ingressRulePort + ); + serviceSecurityGroup.addIngressRule( + ec2.Peer.anyIpv4(), + palwordServerConfig.rcon.ingressRulePort + ); + config.extraTcpPorts.forEach(port => { + serviceSecurityGroup.addIngressRule( + ec2.Peer.anyIpv4(), + ec2.Port.tcp(port) + ); + }); + config.extraUdpPorts.forEach(port => { + serviceSecurityGroup.addIngressRule( + ec2.Peer.anyIpv4(), + ec2.Port.udp(port) + ); + }); + + const palworldServerService = new ecs.FargateService( + this, + 'FargateService', + { + cluster, + capacityProviderStrategies: [ + { + capacityProvider: config.useFargateSpot + ? 'FARGATE_SPOT' + : 'FARGATE', + weight: 1, + base: 1, + }, + ], + taskDefinition: palworldTaskDefinition, + platformVersion: ecs.FargatePlatformVersion.LATEST, + serviceName: constants.SERVICE_NAME, + desiredCount: 0, + assignPublicIp: true, + securityGroups: [serviceSecurityGroup], + enableExecuteCommand: true, + } + ); + + /* Allow access to EFS from Fargate service security group */ + fileSystem.connections.allowDefaultPortFrom( + palworldServerService.connections + ); + + const hostedZoneId = new SSMParameterReader( + this, + 'Route53HostedZoneIdReader', + { + parameterName: constants.HOSTED_ZONE_SSM_PARAMETER, + region: constants.DOMAIN_STACK_REGION, + } + ).getParameterValue(); + + let snsTopicArn = ''; + /* Create SNS Topic if SNS_EMAIL is provided */ + if (config.snsEmailAddress) { + const snsTopic = new sns.Topic(this, 'ServerSnsTopic', { + displayName: 'Palworld Server Notifications', + }); + + snsTopic.grantPublish(ecsTaskRole); + + const emailSubscription = new sns.Subscription( + this, + 'EmailSubscription', + { + protocol: sns.SubscriptionProtocol.EMAIL, + topic: snsTopic, + endpoint: config.snsEmailAddress, + } + ); + snsTopicArn = snsTopic.topicArn; + } + + const watchdogContainer = new ecs.ContainerDefinition( + this, + 'WatchDogContainer', + { + containerName: constants.WATCHDOG_SERVER_CONTAINER_NAME, + image: ecs.ContainerImage.fromRegistry( + 'yurisasc/palworld-ecsfargate-watchdog' + ), + essential: true, + taskDefinition: palworldTaskDefinition, + environment: { + CLUSTER: constants.CLUSTER_NAME, + SERVICE: constants.SERVICE_NAME, + DNSZONE: hostedZoneId, + SERVERNAME: `${config.subdomainPart}.${config.domainName}`, + SNSTOPIC: snsTopicArn, + TWILIOFROM: config.twilio.phoneFrom, + TWILIOTO: config.twilio.phoneTo, + TWILIOAID: config.twilio.accountId, + TWILIOAUTH: config.twilio.authCode, + DISCORDWEBHOOKS: config.discord.webhookUrls, + STARTUPMIN: config.startupMinutes, + SHUTDOWNMIN: config.shutdownMinutes, + RCON_PORT: `${palwordServerConfig.rcon.port}`, + RCON_PASSWORD: palwordServerConfig.rcon.password, + }, + logging: config.debug + ? new ecs.AwsLogDriver({ + logRetention: logs.RetentionDays.THREE_DAYS, + streamPrefix: constants.WATCHDOG_SERVER_CONTAINER_NAME, + }) + : undefined, + } + ); + + const serviceControlPolicy = new iam.Policy(this, 'ServiceControlPolicy', { + statements: [ + new iam.PolicyStatement({ + sid: 'AllowAllOnServiceAndTask', + effect: iam.Effect.ALLOW, + actions: ['ecs:*'], + resources: [ + palworldServerService.serviceArn, + /* arn:aws:ecs:::task/palworld/* */ + Arn.format( + { + service: 'ecs', + resource: 'task', + resourceName: `${constants.CLUSTER_NAME}/*`, + arnFormat: ArnFormat.SLASH_RESOURCE_NAME, + }, + this + ), + ], + }), + new iam.PolicyStatement({ + effect: iam.Effect.ALLOW, + actions: ['ec2:DescribeNetworkInterfaces'], + resources: ['*'], + }), + new iam.PolicyStatement({ + effect: iam.Effect.ALLOW, + actions: [ + 'ssmmessages:CreateControlChannel', + 'ssmmessages:CreateDataChannel', + 'ssmmessages:OpenControlChannel', + 'ssmmessages:OpenDataChannel', + ], + resources: ['*'], + }), + ], + }); + + serviceControlPolicy.attachToRole(ecsTaskRole); + + /** + * Add service control policy to the launcher lambda from the other stack + */ + const launcherLambdaRoleArn = new SSMParameterReader( + this, + 'launcherLambdaRoleArn', + { + parameterName: constants.LAUNCHER_LAMBDA_ARN_SSM_PARAMETER, + region: constants.DOMAIN_STACK_REGION, + } + ).getParameterValue(); + const launcherLambdaRole = iam.Role.fromRoleArn( + this, + 'LauncherLambdaRole', + launcherLambdaRoleArn + ); + serviceControlPolicy.attachToRole(launcherLambdaRole); + + /** + * This policy gives permission to our ECS task to update the A record + * associated with our palworld server. Retrieve the hosted zone identifier + * from Route 53 and place it in the Resource line within this policy. + */ + const iamRoute53Policy = new iam.Policy(this, 'IamRoute53Policy', { + statements: [ + new iam.PolicyStatement({ + sid: 'AllowEditRecordSets', + effect: iam.Effect.ALLOW, + actions: [ + 'route53:GetHostedZone', + 'route53:ChangeResourceRecordSets', + 'route53:ListResourceRecordSets', + ], + resources: [`arn:aws:route53:::hostedzone/${hostedZoneId}`], + }), + ], + }); + iamRoute53Policy.attachToRole(ecsTaskRole); + + // Create a new EC2 template for the on-demand EFS maintenance instances + const efsMaintenanceInstanceRole = new iam.Role( + this, + 'EFSMaintenanceRole', + { + assumedBy: new iam.ServicePrincipal('ec2.amazonaws.com'), + description: 'Palworld EC2 instance role', + } + ); + efsReadWriteDataPolicy.attachToRole(efsMaintenanceInstanceRole); + + const efsMaintenanceSecurityGroup = new ec2.SecurityGroup( + this, + 'EfsMaintenanceSecurityGroup', + { + vpc, + description: + 'Security group for Palworld on-demand EFS Maintenance Instances', + } + ); + + efsMaintenanceSecurityGroup.addIngressRule( + ec2.Peer.anyIpv4(), + ec2.Port.tcp(22) + ); + + /* Allow access to EFS from Fargate service security group */ + fileSystem.connections.allowDefaultPortFrom(efsMaintenanceSecurityGroup); + + const efsMaintenanceLaunchTemplate = new ec2.LaunchTemplate( + this, + 'EFSMaintenanceLaunchTemplate', + { + userData: ec2.UserData.custom(`#cloud-config +package_update: true +package_upgrade: true +runcmd: +- yum install -y amazon-efs-utils +- apt-get -y install amazon-efs-utils +- yum install -y nfs-utils +- apt-get -y install nfs-common +- file_system_id_1=${fileSystem.fileSystemId} +- efs_mount_point_1=/mnt/efs/fs1 +- mkdir -p "\${efs_mount_point_1}" +- test -f "/sbin/mount.efs" && printf "\\n\${file_system_id_1}:/ \${efs_mount_point_1} efs iam,tls,_netdev\\n" >> /etc/fstab || printf "\\n\${file_system_id_1}.efs.${config.serverRegion}.amazonaws.com:/ \${efs_mount_point_1} nfs4 nfsvers=4.1,rsize=1048576,wsize=1048576,hard,timeo=600,retrans=2,noresvport,_netdev 0 0\\n" >> /etc/fstab +- test -f "/sbin/mount.efs" && grep -ozP 'client-info]\\nsource' '/etc/amazon/efs/efs-utils.conf'; if [[ $? == 1 ]]; then printf "\\n[client-info]\\nsource=liw\\n" >> /etc/amazon/efs/efs-utils.conf; fi; +- retryCnt=15; waitTime=30; while true; do mount -a -t efs,nfs4 defaults; if [ $? = 0 ] || [ $retryCnt -lt 1 ]; then echo File system mounted successfully; break; fi; echo File system not available, retrying to mount.; ((retryCnt--)); sleep $waitTime; done; +`), + role: efsMaintenanceInstanceRole, + spotOptions: { + interruptionBehavior: ec2.SpotInstanceInterruption.TERMINATE, + requestType: ec2.SpotRequestType.ONE_TIME, + }, + securityGroup: efsMaintenanceSecurityGroup, + instanceInitiatedShutdownBehavior: + ec2.InstanceInitiatedShutdownBehavior.TERMINATE, + } + ); + } +} diff --git a/cdk/lib/ssm-parameter-reader.ts b/cdk/lib/ssm-parameter-reader.ts new file mode 100644 index 0000000..631ca6e --- /dev/null +++ b/cdk/lib/ssm-parameter-reader.ts @@ -0,0 +1,35 @@ +import { custom_resources as cr, aws_iam as iam } from 'aws-cdk-lib'; +import { Construct } from 'constructs'; + +interface SSMParameterReaderProps { + parameterName: string; + region: string; +} + +export class SSMParameterReader extends cr.AwsCustomResource { + constructor(scope: Construct, name: string, props: SSMParameterReaderProps) { + const { parameterName, region } = props; + + const ssmAwsSdkCall: cr.AwsSdkCall = { + service: 'SSM', + action: 'getParameter', + parameters: { + Name: parameterName, + }, + region, + /* Update physical id to always fetch the latest version */ + physicalResourceId: { id: `SSMParam-${parameterName}-${Date.now()}` }, + }; + + super(scope, name, { + onUpdate: ssmAwsSdkCall, + policy: cr.AwsCustomResourcePolicy.fromSdkCalls({ + resources: cr.AwsCustomResourcePolicy.ANY_RESOURCE, + }), + }); + } + + public getParameterValue(): string { + return this.getResponseField('Parameter.Value').toString(); + } +} diff --git a/cdk/lib/types.ts b/cdk/lib/types.ts new file mode 100644 index 0000000..4711855 --- /dev/null +++ b/cdk/lib/types.ts @@ -0,0 +1,211 @@ +import type { Protocol } from 'aws-cdk-lib/aws-ecs'; +import type { Port } from 'aws-cdk-lib/aws-ec2'; + +interface TwilioConfig { + /** + * Your twilio phone number. + * + * @example + * `+1XXXYYYZZZZ` + */ + phoneFrom: string; + /** + * Phone number to receive text notifications at. + * + * @example + * `+1XXXYYYZZZZ` + */ + phoneTo: string; + /** + * Twilio account ID + */ + accountId: string; + /** + * Twilio auth code + */ + authCode: string; +} + +interface DiscordConfig { + /** + * Discord webhook URLs, comma separated. + * + * @example + * `https://discord.com/api/webhooks/1234567890/abcdefghijklmnopqrstuvwxyz` + */ + webhookUrls: string; +} + +export type PalworldImageEnv = Record; + +export interface StackConfig { + /** + * **Required**. Domain name of existing Route53 Hosted Zone + * + */ + domainName: string; + /** + * Name of the subdomain part to be used for creating a delegated hosted zone + * (palworld.example.com) and an NS record on your existing (example.com) + * hosted zone. This subdomain should not already be in use. + * + * @default "palworld" + */ + subdomainPart: string; + /** + * The AWS region to deploy your palworld server in. + * + * @default "us-east-1" + */ + serverRegion: string; + /** + * Number of minutes to wait for a connection after starting before terminating (optional, default 10) + * + * @default "10" + */ + startupMinutes: string; + /** + * Number of minutes to wait after the last client disconnects before terminating (optional, default 20) + * + * @default "20" + */ + shutdownMinutes: string; + /** + * Sets the preference for Fargate Spot. + * + * If you leave it 'false', your tasks will launch under the FARGATE strategy + * which currently will run about 5 cents per hour. You can switch it to true + * to enable FARGATE_SPOT, and pay 1.5 cents per hour. While this is cheaper, + * technically AWS can terminate your instance at any time if they need the + * capacity. The watchdog is designed to intercept this termination command + * and shut down safely, so it's fine to use Spot to save a few pennies, at + * the extremely low risk of game interruption. + * + * @default false + */ + useFargateSpot: boolean; + /** + * The number of cpu units used by the task running the Palworld server. + * + * Valid values, which determines your range of valid values for the memory parameter: + * + * 256 (.25 vCPU) - Available memory values: 0.5GB, 1GB, 2GB + * + * 512 (.5 vCPU) - Available memory values: 1GB, 2GB, 3GB, 4GB + * + * 1024 (1 vCPU) - Available memory values: 2GB, 3GB, 4GB, 5GB, 6GB, 7GB, 8GB + * + * 2048 (2 vCPU) - Available memory values: Between 4GB and 16GB in 1GB increments + * + * 4096 (4 vCPU) - Available memory values: Between 8GB and 30GB in 1GB increments + * + * @default 1024 1 vCPU + */ + taskCpu: number; + /** + * The amount (in MiB) of memory used by the task running the Palworld server. + * + * 512 (0.5 GB), 1024 (1 GB), 2048 (2 GB) - Available cpu values: 256 (.25 vCPU) + * + * 1024 (1 GB), 2048 (2 GB), 3072 (3 GB), 4096 (4 GB) - Available cpu values: 512 (.5 vCPU) + * + * 2048 (2 GB), 3072 (3 GB), 4096 (4 GB), 5120 (5 GB), 6144 (6 GB), 7168 (7 GB), 8192 (8 GB) - Available cpu values: 1024 (1 vCPU) + * + * Between 4096 (4 GB) and 16384 (16 GB) in increments of 1024 (1 GB) - Available cpu values: 2048 (2 vCPU) + * + * Between 8192 (8 GB) and 30720 (30 GB) in increments of 1024 (1 GB) - Available cpu values: 4096 (4 vCPU) + * + * @default 2048 2 GB + */ + taskMemory: number; + /** + * The ID of an already existing VPC to deploy the server to. When this valueis not set, a new VPC is automatically created by default. + */ + vpcId: string; + /** + * The password for the RCON commands. This is required for the server to start. + */ + rconPassword: string; + /** + * The email address you would like to receive notifications at. + * + * If this value is specified, an SNS topic is created and you will receive + * email notifications each time the palworld server is launched and ready. + */ + snsEmailAddress: string; + twilio: TwilioConfig; + discord: DiscordConfig; + /** + * Additional environment variables to be passed to the + * [Palworld Docker Server](https://github.com/jammsen/docker-palworld-dedicated-server) + */ + palworldImageEnv: PalworldImageEnv; + /** + * Setting to `true` enables debug mode. + * + * This will enable the following: + * - CloudWatch Logs for the `palworld-server` ECS Container + * - CloudWatch Logs for the `palworld-ecsfargate-watchdog` ECS Container + */ + debug: boolean; + /** + * Extra TCP ports to open on the security group + * + * @default [] + * @example [25565, 25575] + */ + extraTcpPorts: number[]; + /** + * Extra UDP ports to open on the security group + * + * @default [] + * @example [19132] + */ + extraUdpPorts: number[]; +} + +export interface PalworldConfig { + server: { + /** + * Name of the docker image to pull for the Palworld server + * + * @example 'jammsen/palworld-dedicated-server' + */ + image: string; + /** + * Port number to run the Palworld server on + */ + port: number; + /** + * Protocol for the Palworld server + */ + protocol: Protocol; + /** + * The ingress rule port to be used for the service security group + */ + ingressRulePort: Port; + }; + rcon: { + /** + * Name of the docker image to pull for the RCON commands + * @example 'outdead/rcon' + */ + image: string; + /** + * Port number for the RCON commands + */ + port: number; + /** + * Password for the RCON commands + */ + password: string; + /** + * Protocol for the RCON commands + */ + protocol: Protocol; + /** + * The ingress rule port to be used for the service security group + */ + ingressRulePort: Port; + }; +} diff --git a/cdk/lib/util.ts b/cdk/lib/util.ts new file mode 100644 index 0000000..6c47404 --- /dev/null +++ b/cdk/lib/util.ts @@ -0,0 +1,38 @@ +import { Port } from 'aws-cdk-lib/aws-ec2'; +import { Protocol } from 'aws-cdk-lib/aws-ecs'; +import * as execa from 'execa'; +import { constants } from './constants'; +import { PalworldConfig, StackConfig } from './types'; + +export const stringAsBoolean = (str?: string): boolean => + Boolean(str === 'true'); + +export const stringAsNumberArray = (str?: string): number[] => + JSON.parse(str ?? '[]').map(Number) ?? []; + +export const isDockerInstalled = (): boolean => { + try { + execa.sync('docker', ['version']); + return true; + } catch (e) { + return false; + } +}; + +export const getPalworldServerConfig = (rconPassword: string): PalworldConfig => { + return { + server: { + image: constants.PALWORLD_DOCKER_IMAGE, + port: 8211, + protocol: Protocol.UDP, + ingressRulePort: Port.udp(8211), + }, + rcon: { + image: constants.RCON_DOCKER_IMAGE, + port: 25575, + password: rconPassword, + protocol: Protocol.TCP, + ingressRulePort: Port.tcp(25575), + }, + }; +}; diff --git a/cdk/minecraft-env.json b/cdk/minecraft-env.json new file mode 100644 index 0000000..7a64d6d --- /dev/null +++ b/cdk/minecraft-env.json @@ -0,0 +1,21 @@ +{ + "EULA": "TRUE", + "MEMORY": "3500M", + "TYPE": "FABRIC", + "OPS": "Vostox", + "WHITELIST": "Vostox, sempolayam", + "DIFFICULTY": "normal", + "ONLINE_MODE": "FALSE", + "MOTD": "Hello World!", + "MAX_PLAYERS": "5", + "VIEW_DISTANCE": "12", + "MODE": "survival", + "ENABLE_COMMAND_BLOCK": "true", + "TZ": "Australia/Brisbane", + "MODRINTH_PROJECTS": "fabric-api,lithium,lazydfu,ferrite-core,starlight,memoryleakfix,tabtps,fabrictailor,no-chat-reports,easywhitelist,convenient-mobgriefing", + "MODRINTH_DOWNLOAD_OPTIONAL_DEPENDENCIES": "TRUE", + "FETCH_RESPONSE_TIMEOUT": "PT20S", + "MODS": "https://ci.opencollab.dev/job/GeyserMC/job/Geyser/job/master/lastSuccessfulBuild/artifact/bootstrap/fabric/build/libs/Geyser-Fabric.jar", + "DATAPACKS": "https://www.dropbox.com/s/z7xv1sr7f4fleqv/Vein%20miner.zip?dl=1,https://www.dropbox.com/s/x3bx5pvf9zjqhh5/vanillarefresh-v1-4-8c.zip?dl=1,https://www.dropbox.com/s/zdrwwd6fer57efh/pk-waystones-v-1-0-5.zip?dl=1,https://www.dropbox.com/s/w5hmtyh73juqq97/BlazeandCaves%20Advancements%20Pack%201.15.3.zip?dl=1,https://www.dropbox.com/s/u6eeyul62vfxexg/Health%20Indications%20v2.2%201.19.zip?dl=1,https://www.dropbox.com/s/t87nhnc39t2nkjr/new-in-town-by-kanokarob-v2.14.zip?dl=1", + "REMOVE_OLD_DATAPACKS": "TRUE" +} diff --git a/cdk/package-lock.json b/cdk/package-lock.json new file mode 100644 index 0000000..66de5c4 --- /dev/null +++ b/cdk/package-lock.json @@ -0,0 +1,863 @@ +{ + "name": "palworld-ondemand", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "palworld-ondemand", + "version": "1.0.0", + "dependencies": { + "aws-cdk-lib": "^2.89.0", + "constructs": "^10.0.0", + "execa": "^5.1.1", + "source-map-support": "^0.5.16" + }, + "bin": { + "cdk": "bin/cdk.js" + }, + "devDependencies": { + "@types/jest": "^26.0.10", + "@types/node": "10.17.27", + "dotenv": "^10.0.0", + "ts-node": "^9.0.0", + "typescript": "~3.9.7" + } + }, + "node_modules/@aws-cdk/asset-awscli-v1": { + "version": "2.2.202", + "resolved": "https://registry.npmjs.org/@aws-cdk/asset-awscli-v1/-/asset-awscli-v1-2.2.202.tgz", + "integrity": "sha512-JqlF0D4+EVugnG5dAsNZMqhu3HW7ehOXm5SDMxMbXNDMdsF0pxtQKNHRl52z1U9igsHmaFpUgSGjbhAJ+0JONg==" + }, + "node_modules/@aws-cdk/asset-kubectl-v20": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@aws-cdk/asset-kubectl-v20/-/asset-kubectl-v20-2.1.2.tgz", + "integrity": "sha512-3M2tELJOxQv0apCIiuKQ4pAbncz9GuLwnKFqxifWfe77wuMxyTRPmxssYHs42ePqzap1LT6GDcPygGs+hHstLg==" + }, + "node_modules/@aws-cdk/asset-node-proxy-agent-v6": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@aws-cdk/asset-node-proxy-agent-v6/-/asset-node-proxy-agent-v6-2.0.1.tgz", + "integrity": "sha512-DDt4SLdLOwWCjGtltH4VCST7hpOI5DzieuhGZsBpZ+AgJdSI2GCjklCXm0GCTwJG/SolkL5dtQXyUKgg9luBDg==" + }, + "node_modules/@jest/types": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz", + "integrity": "sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^15.0.0", + "chalk": "^4.0.0" + }, + "engines": { + "node": ">= 10.14.2" + } + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz", + "integrity": "sha512-sz7iLqvVUg1gIedBOvlkxPlc8/uVzyS5OwGz1cKjXzkl3FpL3al0crU8YGU1WoHkxn0Wxbw5tyi6hvzJKNzFsw==", + "dev": true + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", + "integrity": "sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.1.tgz", + "integrity": "sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "26.0.24", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-26.0.24.tgz", + "integrity": "sha512-E/X5Vib8BWqZNRlDxj9vYXhsDwPYbPINqKF9BsnSoon4RQ0D9moEuLD8txgyypFLH7J4+Lho9Nr/c8H0Fi+17w==", + "dev": true, + "dependencies": { + "jest-diff": "^26.0.0", + "pretty-format": "^26.0.0" + } + }, + "node_modules/@types/node": { + "version": "10.17.27", + "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.27.tgz", + "integrity": "sha512-J0oqm9ZfAXaPdwNXMMgAhylw5fhmXkToJd06vuDUSAgEDZ/n/69/69UmyBZbc+zT34UnShuDSBqvim3SPnozJg==", + "dev": true + }, + "node_modules/@types/yargs": { + "version": "15.0.14", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.14.tgz", + "integrity": "sha512-yEJzHoxf6SyQGhBhIYGXQDSCkJjB6HohDShto7m8vaKg9Yp0Yn8+71J9eakh2bnPg6BfsH9PRMhiRTZnd4eXGQ==", + "dev": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "20.2.1", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-20.2.1.tgz", + "integrity": "sha512-7tFImggNeNBVMsn0vLrpn1H1uPrUBdnARPTpZoitY37ZrdJREzf7I16tMrlK3hen349gr1NYh8CmZQa7CTG6Aw==", + "dev": true + }, + "node_modules/ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true + }, + "node_modules/aws-cdk-lib": { + "version": "2.122.0", + "resolved": "https://registry.npmjs.org/aws-cdk-lib/-/aws-cdk-lib-2.122.0.tgz", + "integrity": "sha512-NBUfYk/SialvKFsvBG/Ucd7lM+BID3Uy3EEOnIBbioDpnMotm5SDaU/RUm4APS4sxzQZX1DjduD5ZUFNHnEWhQ==", + "bundleDependencies": [ + "@balena/dockerignore", + "case", + "fs-extra", + "ignore", + "jsonschema", + "minimatch", + "punycode", + "semver", + "table", + "yaml" + ], + "dependencies": { + "@aws-cdk/asset-awscli-v1": "^2.2.201", + "@aws-cdk/asset-kubectl-v20": "^2.1.2", + "@aws-cdk/asset-node-proxy-agent-v6": "^2.0.1", + "@balena/dockerignore": "^1.0.2", + "case": "1.6.3", + "fs-extra": "^11.2.0", + "ignore": "^5.3.0", + "jsonschema": "^1.4.1", + "minimatch": "^3.1.2", + "punycode": "^2.3.1", + "semver": "^7.5.4", + "table": "^6.8.1", + "yaml": "1.10.2" + }, + "engines": { + "node": ">= 14.15.0" + }, + "peerDependencies": { + "constructs": "^10.0.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/@balena/dockerignore": { + "version": "1.0.2", + "inBundle": true, + "license": "Apache-2.0" + }, + "node_modules/aws-cdk-lib/node_modules/ajv": { + "version": "8.12.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/aws-cdk-lib/node_modules/ansi-regex": { + "version": "5.0.1", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/aws-cdk-lib/node_modules/ansi-styles": { + "version": "4.3.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/aws-cdk-lib/node_modules/astral-regex": { + "version": "2.0.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/aws-cdk-lib/node_modules/balanced-match": { + "version": "1.0.2", + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/brace-expansion": { + "version": "1.1.11", + "inBundle": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/aws-cdk-lib/node_modules/case": { + "version": "1.6.3", + "inBundle": true, + "license": "(MIT OR GPL-3.0-or-later)", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/color-convert": { + "version": "2.0.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/color-name": { + "version": "1.1.4", + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/concat-map": { + "version": "0.0.1", + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/emoji-regex": { + "version": "8.0.0", + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/fast-deep-equal": { + "version": "3.1.3", + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/fs-extra": { + "version": "11.2.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/aws-cdk-lib/node_modules/graceful-fs": { + "version": "4.2.11", + "inBundle": true, + "license": "ISC" + }, + "node_modules/aws-cdk-lib/node_modules/ignore": { + "version": "5.3.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/aws-cdk-lib/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/aws-cdk-lib/node_modules/json-schema-traverse": { + "version": "1.0.0", + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/jsonfile": { + "version": "6.1.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/aws-cdk-lib/node_modules/jsonschema": { + "version": "1.4.1", + "inBundle": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/aws-cdk-lib/node_modules/lodash.truncate": { + "version": "4.4.2", + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/lru-cache": { + "version": "6.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/aws-cdk-lib/node_modules/minimatch": { + "version": "3.1.2", + "inBundle": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/aws-cdk-lib/node_modules/punycode": { + "version": "2.3.1", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/aws-cdk-lib/node_modules/require-from-string": { + "version": "2.0.2", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/semver": { + "version": "7.5.4", + "inBundle": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/aws-cdk-lib/node_modules/slice-ansi": { + "version": "4.0.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/aws-cdk-lib/node_modules/string-width": { + "version": "4.2.3", + "inBundle": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/aws-cdk-lib/node_modules/strip-ansi": { + "version": "6.0.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/aws-cdk-lib/node_modules/table": { + "version": "6.8.1", + "inBundle": true, + "license": "BSD-3-Clause", + "dependencies": { + "ajv": "^8.0.1", + "lodash.truncate": "^4.4.2", + "slice-ansi": "^4.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/universalify": { + "version": "2.0.1", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/uri-js": { + "version": "4.4.1", + "inBundle": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/yallist": { + "version": "4.0.0", + "inBundle": true, + "license": "ISC" + }, + "node_modules/aws-cdk-lib/node_modules/yaml": { + "version": "1.10.2", + "inBundle": true, + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/constructs": { + "version": "10.0.5", + "resolved": "https://registry.npmjs.org/constructs/-/constructs-10.0.5.tgz", + "integrity": "sha512-IwOwekzrASFC3qt4ozCtV09rteAIAesuCGsW0p+uBfqHd2XcvA5CXqJjgf4eUqm6g8e/noXlVCMDWwC8GaLtrg==", + "engines": { + "node": ">= 10.17.0" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/diff-sequences": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-26.6.2.tgz", + "integrity": "sha512-Mv/TDa3nZ9sbc5soK+OoA74BsS3mL37yixCvUAQkiuA4Wz6YtwP/K47n2rv2ovzHZvoiQeA5FTQOschKkEwB0Q==", + "dev": true, + "engines": { + "node": ">= 10.14.2" + } + }, + "node_modules/dotenv": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-10.0.0.tgz", + "integrity": "sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" + }, + "node_modules/jest-diff": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-26.6.2.tgz", + "integrity": "sha512-6m+9Z3Gv9wN0WFVasqjCL/06+EFCMTqDEUl/b87HYK2rAPTyfz4ZIuSlPhY51PIQRWx5TaxeF1qmXKe9gfN3sA==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^26.6.2", + "jest-get-type": "^26.3.0", + "pretty-format": "^26.6.2" + }, + "engines": { + "node": ">= 10.14.2" + } + }, + "node_modules/jest-get-type": { + "version": "26.3.0", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-26.3.0.tgz", + "integrity": "sha512-TpfaviN1R2pQWkIihlfEanwOXK0zcxrKEE4MlU6Tn7keoXdN6/3gK/xl0yEh8DOunn5pOVGKf8hB4R9gVh04ig==", + "dev": true, + "engines": { + "node": ">= 10.14.2" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==" + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "engines": { + "node": ">=8" + } + }, + "node_modules/pretty-format": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-26.6.2.tgz", + "integrity": "sha512-7AeGuCYNGmycyQbCqd/3PWH4eOoX/OiCa0uphp57NVTeAGdJGaAliecxwBDHYQCIvrW7aDBZCYeNTP/WX69mkg==", + "dev": true, + "dependencies": { + "@jest/types": "^26.6.2", + "ansi-regex": "^5.0.0", + "ansi-styles": "^4.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.4.tgz", + "integrity": "sha512-rqYhcAnZ6d/vTPGghdrw7iumdcbXpsk1b8IG/rz+VWV51DM0p7XCtMoJ3qhPLIbp3tvyt3pKRbaaEMZYpHto8Q==" + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.20", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.20.tgz", + "integrity": "sha512-n1lZZ8Ve4ksRqizaBQgxXDgKwttHDhyfQjA6YZZn8+AroHbsIz+JjwxQDxbp+7y5OYCI8t1Yk7etjD9CRd2hIw==", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ts-node": { + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-9.1.1.tgz", + "integrity": "sha512-hPlt7ZACERQGf03M253ytLY3dHbGNGrAq9qIHWUY9XHYl1z7wYngSr3OQ5xmui8o2AaxsONxIzjafLUiWBo1Fg==", + "dev": true, + "dependencies": { + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "source-map-support": "^0.5.17", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "typescript": ">=2.7" + } + }, + "node_modules/typescript": { + "version": "3.9.10", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.10.tgz", + "integrity": "sha512-w6fIxVE/H1PkLKcCPsFqKE7Kv7QUwhU8qQY2MueZXWx5cPZdwFupLgKK3vntcK98BtNHZtAF4LA/yl2a7k8R6Q==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "engines": { + "node": ">=6" + } + } + } +} diff --git a/cdk/package.json b/cdk/package.json new file mode 100644 index 0000000..fa80737 --- /dev/null +++ b/cdk/package.json @@ -0,0 +1,28 @@ +{ + "name": "palworld-ondemand", + "version": "1.0.0", + "bin": { + "cdk": "bin/cdk.js" + }, + "scripts": { + "prebuild": "npm ci", + "build": "npx aws-cdk bootstrap", + "deploy": "npx aws-cdk deploy --all", + "destroy": "npx aws-cdk destroy --all", + "typecheck": "tsc --noEmit --watch", + "diff": "npx aws-cdk diff" + }, + "devDependencies": { + "@types/jest": "^26.0.10", + "@types/node": "10.17.27", + "dotenv": "^10.0.0", + "ts-node": "^9.0.0", + "typescript": "~3.9.7" + }, + "dependencies": { + "aws-cdk-lib": "^2.89.0", + "constructs": "^10.0.0", + "execa": "^5.1.1", + "source-map-support": "^0.5.16" + } +} diff --git a/cdk/tsconfig.json b/cdk/tsconfig.json new file mode 100644 index 0000000..9f8e8be --- /dev/null +++ b/cdk/tsconfig.json @@ -0,0 +1,30 @@ +{ + "compilerOptions": { + "target": "ES2018", + "module": "commonjs", + "lib": [ + "es2018" + ], + "declaration": true, + "strict": true, + "noImplicitAny": true, + "strictNullChecks": true, + "noImplicitThis": true, + "alwaysStrict": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": false, + "inlineSourceMap": true, + "inlineSources": true, + "experimentalDecorators": true, + "strictPropertyInitialization": false, + "typeRoots": [ + "./node_modules/@types" + ] + }, + "exclude": [ + "node_modules", + "cdk.out" + ] +} diff --git a/discord-bot/.dockerignore b/discord-bot/.dockerignore new file mode 100644 index 0000000..b58b87d --- /dev/null +++ b/discord-bot/.dockerignore @@ -0,0 +1,11 @@ +# Folders +/.cache +/.git +/dist +/docs +/misc +/node_modules +/temp + +# Files +/npm-debug.log \ No newline at end of file diff --git a/discord-bot/.env.example b/discord-bot/.env.example new file mode 100644 index 0000000..45f0526 --- /dev/null +++ b/discord-bot/.env.example @@ -0,0 +1,5 @@ +CLIENT_ID='YOUR_BOT_CLIENT_ID_HERE' +CLIENT_TOKEN='YOUR_BOT_TOKEN_HERE' +SERVERS_CONFIG=[{"name": "My Private Server", "awsAccount": {"profileName": "default", "accessKeyId": "", "secretAccessKey": "", "region": "us-east-1"}}] +CLUSTER_NAME=palworld +SERVICE_NAME=palworld-server \ No newline at end of file diff --git a/discord-bot/.gitattributes b/discord-bot/.gitattributes new file mode 100644 index 0000000..dfe0770 --- /dev/null +++ b/discord-bot/.gitattributes @@ -0,0 +1,2 @@ +# Auto detect text files and perform LF normalization +* text=auto diff --git a/discord-bot/.gitignore b/discord-bot/.gitignore new file mode 100644 index 0000000..fd060d1 --- /dev/null +++ b/discord-bot/.gitignore @@ -0,0 +1,104 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# TypeScript v1 declaration files +typings/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# Environment variable files +.env* +!.env.example + +# parcel-bundler cache (https://parceljs.org/) +.cache + +# Next.js build output +.next + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and *not* Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port diff --git a/discord-bot/Dockerfile b/discord-bot/Dockerfile new file mode 100644 index 0000000..b08cf34 --- /dev/null +++ b/discord-bot/Dockerfile @@ -0,0 +1,23 @@ +# Using ARM64 so it can run on Raspberry Pi +FROM arm64v8/node:16 + +# Create app directory +WORKDIR /app + +# Copy package.json and package-lock.json +COPY package*.json ./ + +# Install packages +RUN npm install + +# Copy the app code +COPY . . + +# Build the project +RUN npm run build + +# Expose ports +EXPOSE 3001 + +# Run the application +CMD [ "node", "dist/start-bot.js" ] \ No newline at end of file diff --git a/discord-bot/README.md b/discord-bot/README.md new file mode 100644 index 0000000..4b90e8a --- /dev/null +++ b/discord-bot/README.md @@ -0,0 +1,43 @@ +## Installation +Install dependencies + +```sh +npm install +``` + +Create environment variable files `.env` and `.env.dev` based on [.env.example](./.env.example) on project root folder + +```bash +# linux / macOS +cp .env.example .env +cp .env.example .env.dev +``` + +```bash +# windows +copy .env.example .env +copy .env.example .env.dev +``` + +## Running on production environment + +### With Docker + +> āš  Remember to follow the [Installation](#Installation) steps before proceeding + +```bash +docker build -t your-app-name . +docker run -it --rm -e DISCORD_TOKEN="YOUR TOKEN HERE" --name your-app-name your-app-name +``` + +### Without Docker + +> āš  Remember to follow the [Installation](#Installation) steps before proceeding + +Startup bot + +```bash +npm start # or cross-env NODE_ENV=production env-cmd -f .env node ./dist/index.js +``` + +> āš  Note that the loaded environment variables file is `.env` diff --git a/discord-bot/config/config.json b/discord-bot/config/config.json new file mode 100644 index 0000000..a7d333e --- /dev/null +++ b/discord-bot/config/config.json @@ -0,0 +1,30 @@ +{ + "client": { + "intents": ["Guilds", "GuildMessages", "GuildMessageReactions"], + "partials": ["Message", "Channel", "Reaction"] + }, + "rateLimiting": { + "commands": { + "amount": 10, + "interval": 30 + }, + "buttons": { + "amount": 10, + "interval": 30 + }, + "triggers": { + "amount": 10, + "interval": 30 + }, + "reactions": { + "amount": 10, + "interval": 30 + } + }, + "logging": { + "pretty": true, + "rateLimit": { + "minTimeout": 30 + } + } +} diff --git a/discord-bot/lang/lang.common.json b/discord-bot/lang/lang.common.json new file mode 100644 index 0000000..c565c24 --- /dev/null +++ b/discord-bot/lang/lang.common.json @@ -0,0 +1,25 @@ +{ + "bot": { + "name": "My Bot", + "author": "My Name" + }, + "emojis": { + "yes": "āœ…", + "no": "āŒ", + "enabled": "šŸŸ¢", + "disabled": "šŸ”“", + "info": "ā„¹ļø", + "warning": "āš ļø", + "previous": "ā—€ļø", + "next": "ā–¶ļø", + "first": "āŖ", + "last": "ā©", + "refresh": "šŸ”„" + }, + "colors": { + "default": "#0099ff", + "success": "#00ff83", + "warning": "#ffcc66", + "error": "#ff4a4a" + } +} diff --git a/discord-bot/lang/lang.en-GB.json b/discord-bot/lang/lang.en-GB.json new file mode 100644 index 0000000..1744f18 --- /dev/null +++ b/discord-bot/lang/lang.en-GB.json @@ -0,0 +1,145 @@ +{ + "data": { + "displayEmbeds": { + "startServer": { + "title": "šŸŸ” Starting {{SERVER_NAME}}...", + "description": "Please wait while the server starts. It may take up to 10 minutes." + }, + "stopServer": { + "title": "šŸŸ” Stopping {{SERVER_NAME}}...", + "description": "We will save your progress before stopping the server." + }, + "invalidServerName": { + "title": "āŒ Invalid server", + "description": "{{SERVER_NAME}} does not exist. Please choose the available server." + }, + "emptyServerName": { + "title": "ā“ No server name", + "description": "Please enter the name of the server you want to start." + } + }, + "validationEmbeds": { + "cooldownHit": { + "description": "You can only run this command {{AMOUNT}} time(s) every {{INTERVAL}}. Please wait before attempting this command again.", + "color": "{{COM:colors.warning}}" + }, + "devOnly": { + "description": "This action can only be done by developers.", + "color": "{{COM:colors.warning}}" + }, + "missingClientPerms": { + "description": [ + "I don't have all permissions required to run that command here! Please check the server and channel permissions to make sure I have the following permissions.", + "", + "Required permissions: {{PERMISSIONS}}" + ], + "color": "{{COM:colors.warning}}" + } + }, + "errorEmbeds": { + "command": { + "description": "Something went wrong!", + "fields": [ + { + "name": "Error code", + "value": "{{ERROR_CODE}}" + }, + { + "name": "Server ID", + "value": "{{GUILD_ID}}" + }, + { + "name": "Shard ID", + "value": "{{SHARD_ID}}" + } + ], + "color": "{{COM:colors.error}}" + }, + "startupInProcess": { + "description": "{{COM:bot.name}} is still starting up. Try again later.", + "color": "{{COM:colors.warning}}" + }, + "notImplemented": { + "description": "This feature has not been implemented yet!", + "color": "{{COM:colors.warning}}" + } + }, + "channelRegexes": { + "bot": "/bot|command|cmd/i" + } + }, + "refs": { + "chatCommands": { + "startServer": "start", + "stopServer": "stop" + }, + "arguments": { + "option": "option" + }, + "commandDescs": { + "startServer": "Start the palworld server.", + "stopServer": "Stop the palworld server." + }, + "argDescs": { + "startOption": "Server to start.", + "stopOption": "Server to stop." + }, + "fields": { + "commands": "Commands" + }, + "permissions": { + "AddReactions": "Add Reactions", + "Administrator": "Administrator", + "AttachFiles": "Attach Files", + "BanMembers": "Ban Members", + "ChangeNickname": "Change Nickname", + "Connect": "Connect", + "CreateInstantInvite": "Create Invite", + "CreatePrivateThreads": "Create Private Threads", + "CreatePublicThreads": "Create Public Threads", + "DeafenMembers": "Deafen Members", + "EmbedLinks": "Embed Links", + "KickMembers": "Kick Members", + "ManageChannels": "Manage Channel(s)", + "ManageEmojisAndStickers": "Manage Emoji and Stickers", + "ManageEvents": "Manage Events", + "ManageGuild": "Manage Server", + "ManageMessages": "Manage Messages", + "ManageNicknames": "Manage Nicknames", + "ManageRoles": "Manage Roles / Permissions", + "ManageThreads": "Manage Threads / Posts", + "ManageWebhooks": "Manage Webhooks", + "MentionEveryone": "Mention Everyone, Here, and All Roles", + "ModerateMembers": "Timeout Members", + "MoveMembers": "Move Members", + "MuteMembers": "Mute Members", + "PrioritySpeaker": "Priority Speaker", + "ReadMessageHistory": "Read Message History", + "RequestToSpeak": "Request to Speak", + "SendMessages": "Send Messages / Create Posts", + "SendMessagesInThreads": "Send Messages in Threads / Posts", + "SendTTSMessages": "Send Text-to-Speech Messages", + "Speak": "Speak", + "Stream": "Video", + "UseApplicationCommands": "Use Application Commands", + "UseEmbeddedActivities": "Use Activities", + "UseExternalEmojis": "Use External Emoji", + "UseExternalStickers": "Use External Stickers", + "UseVAD": "Use Voice Activity", + "ViewAuditLog": "View Audit Log", + "ViewChannel": "View Channel(s)", + "ViewGuildInsights": "View Server Insights" + }, + "yesNo": { + "yes": "Yes", + "no": "No" + }, + "boolean": { + "true": "True", + "false": "False" + }, + "other": { + "na": "N/A" + } + } +} diff --git a/discord-bot/lang/lang.en-US.json b/discord-bot/lang/lang.en-US.json new file mode 100644 index 0000000..1744f18 --- /dev/null +++ b/discord-bot/lang/lang.en-US.json @@ -0,0 +1,145 @@ +{ + "data": { + "displayEmbeds": { + "startServer": { + "title": "šŸŸ” Starting {{SERVER_NAME}}...", + "description": "Please wait while the server starts. It may take up to 10 minutes." + }, + "stopServer": { + "title": "šŸŸ” Stopping {{SERVER_NAME}}...", + "description": "We will save your progress before stopping the server." + }, + "invalidServerName": { + "title": "āŒ Invalid server", + "description": "{{SERVER_NAME}} does not exist. Please choose the available server." + }, + "emptyServerName": { + "title": "ā“ No server name", + "description": "Please enter the name of the server you want to start." + } + }, + "validationEmbeds": { + "cooldownHit": { + "description": "You can only run this command {{AMOUNT}} time(s) every {{INTERVAL}}. Please wait before attempting this command again.", + "color": "{{COM:colors.warning}}" + }, + "devOnly": { + "description": "This action can only be done by developers.", + "color": "{{COM:colors.warning}}" + }, + "missingClientPerms": { + "description": [ + "I don't have all permissions required to run that command here! Please check the server and channel permissions to make sure I have the following permissions.", + "", + "Required permissions: {{PERMISSIONS}}" + ], + "color": "{{COM:colors.warning}}" + } + }, + "errorEmbeds": { + "command": { + "description": "Something went wrong!", + "fields": [ + { + "name": "Error code", + "value": "{{ERROR_CODE}}" + }, + { + "name": "Server ID", + "value": "{{GUILD_ID}}" + }, + { + "name": "Shard ID", + "value": "{{SHARD_ID}}" + } + ], + "color": "{{COM:colors.error}}" + }, + "startupInProcess": { + "description": "{{COM:bot.name}} is still starting up. Try again later.", + "color": "{{COM:colors.warning}}" + }, + "notImplemented": { + "description": "This feature has not been implemented yet!", + "color": "{{COM:colors.warning}}" + } + }, + "channelRegexes": { + "bot": "/bot|command|cmd/i" + } + }, + "refs": { + "chatCommands": { + "startServer": "start", + "stopServer": "stop" + }, + "arguments": { + "option": "option" + }, + "commandDescs": { + "startServer": "Start the palworld server.", + "stopServer": "Stop the palworld server." + }, + "argDescs": { + "startOption": "Server to start.", + "stopOption": "Server to stop." + }, + "fields": { + "commands": "Commands" + }, + "permissions": { + "AddReactions": "Add Reactions", + "Administrator": "Administrator", + "AttachFiles": "Attach Files", + "BanMembers": "Ban Members", + "ChangeNickname": "Change Nickname", + "Connect": "Connect", + "CreateInstantInvite": "Create Invite", + "CreatePrivateThreads": "Create Private Threads", + "CreatePublicThreads": "Create Public Threads", + "DeafenMembers": "Deafen Members", + "EmbedLinks": "Embed Links", + "KickMembers": "Kick Members", + "ManageChannels": "Manage Channel(s)", + "ManageEmojisAndStickers": "Manage Emoji and Stickers", + "ManageEvents": "Manage Events", + "ManageGuild": "Manage Server", + "ManageMessages": "Manage Messages", + "ManageNicknames": "Manage Nicknames", + "ManageRoles": "Manage Roles / Permissions", + "ManageThreads": "Manage Threads / Posts", + "ManageWebhooks": "Manage Webhooks", + "MentionEveryone": "Mention Everyone, Here, and All Roles", + "ModerateMembers": "Timeout Members", + "MoveMembers": "Move Members", + "MuteMembers": "Mute Members", + "PrioritySpeaker": "Priority Speaker", + "ReadMessageHistory": "Read Message History", + "RequestToSpeak": "Request to Speak", + "SendMessages": "Send Messages / Create Posts", + "SendMessagesInThreads": "Send Messages in Threads / Posts", + "SendTTSMessages": "Send Text-to-Speech Messages", + "Speak": "Speak", + "Stream": "Video", + "UseApplicationCommands": "Use Application Commands", + "UseEmbeddedActivities": "Use Activities", + "UseExternalEmojis": "Use External Emoji", + "UseExternalStickers": "Use External Stickers", + "UseVAD": "Use Voice Activity", + "ViewAuditLog": "View Audit Log", + "ViewChannel": "View Channel(s)", + "ViewGuildInsights": "View Server Insights" + }, + "yesNo": { + "yes": "Yes", + "no": "No" + }, + "boolean": { + "true": "True", + "false": "False" + }, + "other": { + "na": "N/A" + } + } +} diff --git a/discord-bot/lang/logs.json b/discord-bot/lang/logs.json new file mode 100644 index 0000000..4398de2 --- /dev/null +++ b/discord-bot/lang/logs.json @@ -0,0 +1,60 @@ +{ + "info": { + "appStarted": "Application started.", + "apiStarted": "API started on port {PORT}.", + "commandActionView": "\nLocal and remote:\n {LOCAL_AND_REMOTE_LIST}\nLocal only:\n {LOCAL_ONLY_LIST}\nRemote only:\n {REMOTE_ONLY_LIST}", + "commandActionCreating": "Creating commands: {COMMAND_LIST}", + "commandActionCreated": "Commands created.", + "commandActionUpdating": "Updating commands: {COMMAND_LIST}", + "commandActionUpdated": "Commands updated.", + "commandActionRenaming": "Renaming command: '{OLD_COMMAND_NAME}' --> '{NEW_COMMAND_NAME}'", + "commandActionRenamed": "Command renamed.", + "commandActionDeleting": "Deleting command: '{COMMAND_NAME}'", + "commandActionDeleted": "Command deleted.", + "commandActionClearing": "Deleting all commands: {COMMAND_LIST}", + "commandActionCleared": "Commands deleted.", + "managerSpawningShards": "Spawning {SHARD_COUNT} shards: [{SHARD_LIST}].", + "managerLaunchedShard": "Launched Shard {SHARD_ID}.", + "managerAllShardsSpawned": "All shards have been spawned.", + "clientLogin": "Client logged in as '{USER_TAG}'.", + "clientReady": "Client is ready!", + "jobScheduled": "Scheduled job '{JOB}' for '{SCHEDULE}'.", + "jobRun": "Running job '{JOB}'.", + "jobCompleted": "Job '{JOB}' completed.", + "updatedServerCount": "Updated server count. Connected to {SERVER_COUNT} total servers.", + "updatedServerCountSite": "Updated server count on '{BOT_SITE}'.", + "guildJoined": "Guild '{GUILD_NAME}' ({GUILD_ID}) joined.", + "guildLeft": "Guild '{GUILD_NAME}' ({GUILD_ID}) left." + }, + "warn": { + "managerNoShards": "No shards to spawn." + }, + "error": { + "unspecified": "An unspecified error occurred.", + "unhandledRejection": "An unhandled promise rejection occurred.", + "retrieveShards": "An error occurred while retrieving which shards to spawn.", + "managerSpawningShards": "An error occurred while spawning shards.", + "managerShardInfo": "An error occurred while retrieving shard info.", + "commandAction": "An error occurred while running a command action.", + "commandActionNotFound": "Could not find a command with the name '{COMMAND_NAME}'.", + "commandActionRenameMissingArg": "Please supply the current command name and new command name.", + "commandActionDeleteMissingArg": "Please supply a command name to delete.", + "clientLogin": "An error occurred while the client attempted to login.", + "job": "An error occurred while running the '{JOB}' job.", + "updatedServerCountSite": "An error occurred while updating the server count on '{BOT_SITE}'.", + "guildJoin": "An error occurred while processing a guild join.", + "guildLeave": "An error occurred while processing a guild leave.", + "message": "An error occurred while processing a message.", + "reaction": "An error occurred while processing a reaction.", + "command": "An error occurred while processing a command interaction.", + "button": "An error occurred while processing a button interaction.", + "commandNotFound": "[{INTERACTION_ID}] A command with the name '{COMMAND_NAME}' could not be found.", + "autocompleteNotFound": "[{INTERACTION_ID}] An autocomplete method for the '{COMMAND_NAME}' command could not be found.", + "commandGuild": "[{INTERACTION_ID}] An error occurred while executing the '{COMMAND_NAME}' command for user '{USER_TAG}' ({USER_ID}) in channel '{CHANNEL_NAME}' ({CHANNEL_ID}) in guild '{GUILD_NAME}' ({GUILD_ID}).", + "autocompleteGuild": "[{INTERACTION_ID}] An error occurred while autocompleting the '{OPTION_NAME}' option for the '{COMMAND_NAME}' command for user '{USER_TAG}' ({USER_ID}) in channel '{CHANNEL_NAME}' ({CHANNEL_ID}) in guild '{GUILD_NAME}' ({GUILD_ID}).", + "commandOther": "[{INTERACTION_ID}] An error occurred while executing the '{COMMAND_NAME}' command for user '{USER_TAG}' ({USER_ID}).", + "autocompleteOther": "[{INTERACTION_ID}] An error occurred while autocompleting the '{OPTION_NAME}' option for the '{COMMAND_NAME}' command for user '{USER_TAG}' ({USER_ID}).", + "apiRequest": "An error occurred while processing a '{HTTP_METHOD}' request to '{URL}'.", + "apiRateLimit": "A rate limit was hit while making a request." + } +} diff --git a/discord-bot/package-lock.json b/discord-bot/package-lock.json new file mode 100644 index 0000000..7407300 --- /dev/null +++ b/discord-bot/package-lock.json @@ -0,0 +1,3070 @@ +{ + "name": "palworld-server-bot", + "version": "1.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "palworld-server-bot", + "version": "1.0.0", + "license": "Apache-2.0", + "dependencies": { + "aws-sdk": "^2.1328.0", + "discord.js": "^14.7.1", + "discord.js-rate-limiter": "^1.3.2", + "dotenv": "^10.0.0", + "linguini": "^1.3.1", + "node-fetch": "^3.3.0", + "pino": "^8.11.0", + "pino-pretty": "^9.4.0" + }, + "devDependencies": { + "@types/node": "^16.4.10", + "ts-node-dev": "^2.0.0", + "typescript": "^4.3.5" + }, + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@discordjs/builders": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-1.4.0.tgz", + "integrity": "sha512-nEeTCheTTDw5kO93faM1j8ZJPonAX86qpq/QVoznnSa8WWcCgJpjlu6GylfINTDW6o7zZY0my2SYdxx2mfNwGA==", + "dependencies": { + "@discordjs/util": "^0.1.0", + "@sapphire/shapeshift": "^3.7.1", + "discord-api-types": "^0.37.20", + "fast-deep-equal": "^3.1.3", + "ts-mixer": "^6.0.2", + "tslib": "^2.4.1" + }, + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/@discordjs/collection": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-1.3.0.tgz", + "integrity": "sha512-ylt2NyZ77bJbRij4h9u/wVy7qYw/aDqQLWnadjvDqW/WoWCxrsX6M3CIw9GVP5xcGCDxsrKj5e0r5evuFYwrKg==", + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/@discordjs/rest": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@discordjs/rest/-/rest-1.5.0.tgz", + "integrity": "sha512-lXgNFqHnbmzp5u81W0+frdXN6Etf4EUi8FAPcWpSykKd8hmlWh1xy6BmE0bsJypU1pxohaA8lQCgp70NUI3uzA==", + "dependencies": { + "@discordjs/collection": "^1.3.0", + "@discordjs/util": "^0.1.0", + "@sapphire/async-queue": "^1.5.0", + "@sapphire/snowflake": "^3.2.2", + "discord-api-types": "^0.37.23", + "file-type": "^18.0.0", + "tslib": "^2.4.1", + "undici": "^5.13.0" + }, + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/@discordjs/util": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@discordjs/util/-/util-0.1.0.tgz", + "integrity": "sha512-e7d+PaTLVQav6rOc2tojh2y6FE8S7REkqLldq1XF4soCx74XB/DIjbVbVLtBemf0nLW77ntz0v+o5DytKwFNLQ==", + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", + "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.14", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", + "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@sapphire/async-queue": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@sapphire/async-queue/-/async-queue-1.5.0.tgz", + "integrity": "sha512-JkLdIsP8fPAdh9ZZjrbHWR/+mZj0wvKS5ICibcLrRI1j84UmLMshx5n9QmL8b95d4onJ2xxiyugTgSAX7AalmA==", + "engines": { + "node": ">=v14.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/@sapphire/shapeshift": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/@sapphire/shapeshift/-/shapeshift-3.8.1.tgz", + "integrity": "sha512-xG1oXXBhCjPKbxrRTlox9ddaZTvVpOhYLmKmApD/vIWOV1xEYXnpoFs68zHIZBGbqztq6FrUPNPerIrO1Hqeaw==", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "lodash": "^4.17.21" + }, + "engines": { + "node": ">=v14.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/@sapphire/snowflake": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/@sapphire/snowflake/-/snowflake-3.4.0.tgz", + "integrity": "sha512-zZxymtVO6zeXVMPds+6d7gv/OfnCc25M1Z+7ZLB0oPmeMTPeRWVPQSS16oDJy5ZsyCOLj7M6mbZml5gWXcVRNw==", + "engines": { + "node": ">=v14.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/@tokenizer/token": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", + "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==" + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", + "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==", + "dev": true + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.3.tgz", + "integrity": "sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==", + "dev": true + }, + "node_modules/@types/node": { + "version": "16.18.14", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.14.tgz", + "integrity": "sha512-wvzClDGQXOCVNU4APPopC2KtMYukaF1MN/W3xAmslx22Z4/IF1/izDMekuyoUlwfnDHYCIZGaj7jMwnJKBTxKw==" + }, + "node_modules/@types/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-xevGOReSYGM7g/kUBZzPqCrR/KYAo+F0yiPc85WFTJa0MSLtyFTVTU6cJu/aV4mid7IffDIWqo69THF2o4JiEQ==", + "dev": true + }, + "node_modules/@types/strip-json-comments": { + "version": "0.0.30", + "resolved": "https://registry.npmjs.org/@types/strip-json-comments/-/strip-json-comments-0.0.30.tgz", + "integrity": "sha512-7NQmHra/JILCd1QqpSzl8+mJRc8ZHz3uDm8YV1Ks9IhK0epEiTw8aIErbvH9PI+6XbqhyIQy3462nEsn7UVzjQ==", + "dev": true + }, + "node_modules/@types/ws": { + "version": "8.5.4", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.4.tgz", + "integrity": "sha512-zdQDHKUgcX/zBc4GrwsE/7dVdAD8JR4EuiAXiiUhhfyIJXXb2+PrGshFyeXWQPMmmZ2XxgaqclgpIC7eTXc1mg==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/acorn": { + "version": "8.8.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz", + "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", + "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true + }, + "node_modules/atomic-sleep": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", + "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/aws-sdk": { + "version": "2.1328.0", + "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1328.0.tgz", + "integrity": "sha512-ud8ieE+hGX/cWHkQ9kMxQw6w+onbv71PDrcPmP2j8cmYv5IPlM5Zh8/tpsmXApLYDmQMuZ3TtssiB1KmoSbzgA==", + "dependencies": { + "buffer": "4.9.2", + "events": "1.1.1", + "ieee754": "1.1.13", + "jmespath": "0.16.0", + "querystring": "0.2.0", + "sax": "1.2.1", + "url": "0.10.3", + "util": "^0.12.4", + "uuid": "8.0.0", + "xml2js": "0.4.19" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/aws-sdk/node_modules/buffer": { + "version": "4.9.2", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz", + "integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==", + "dependencies": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4", + "isarray": "^1.0.0" + } + }, + "node_modules/aws-sdk/node_modules/events": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", + "integrity": "sha512-kEcvvCBByWXGnZy6JUlgAp2gBIUjfCAV6P6TgT1/aaQKcmuAEC4OZTV1I4EWQLz2gxZw76atuVyvHhTxvi0Flw==", + "engines": { + "node": ">=0.4.x" + } + }, + "node_modules/aws-sdk/node_modules/ieee754": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", + "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, + "node_modules/call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "dependencies": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/colorette": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.19.tgz", + "integrity": "sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true + }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "engines": { + "node": ">= 12" + } + }, + "node_modules/dateformat": { + "version": "4.6.3", + "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz", + "integrity": "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==", + "engines": { + "node": "*" + } + }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/discord-api-types": { + "version": "0.37.35", + "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.37.35.tgz", + "integrity": "sha512-iyKZ/82k7FX3lcmHiAvvWu5TmyfVo78RtghBV/YsehK6CID83k5SI03DKKopBcln+TiEIYw5MGgq7SJXSpNzMg==" + }, + "node_modules/discord.js": { + "version": "14.7.1", + "resolved": "https://registry.npmjs.org/discord.js/-/discord.js-14.7.1.tgz", + "integrity": "sha512-1FECvqJJjjeYcjSm0IGMnPxLqja/pmG1B0W2l3lUY2Gi4KXiyTeQmU1IxWcbXHn2k+ytP587mMWqva2IA87EbA==", + "dependencies": { + "@discordjs/builders": "^1.4.0", + "@discordjs/collection": "^1.3.0", + "@discordjs/rest": "^1.4.0", + "@discordjs/util": "^0.1.0", + "@sapphire/snowflake": "^3.2.2", + "@types/ws": "^8.5.3", + "discord-api-types": "^0.37.20", + "fast-deep-equal": "^3.1.3", + "lodash.snakecase": "^4.1.1", + "tslib": "^2.4.1", + "undici": "^5.13.0", + "ws": "^8.11.0" + }, + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/discord.js-rate-limiter": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/discord.js-rate-limiter/-/discord.js-rate-limiter-1.3.2.tgz", + "integrity": "sha512-gS0suMdhWKh4IJRf2NbzHGNJfYI9ApKhO5XsBLByT0qy5vbk6SiS/9FofQKoWgbx5XXOmQ8dwqQtzzUVlSh5ZA==", + "dependencies": { + "limiter": "2.1.0" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "discord.js": ">=12.0.0" + } + }, + "node_modules/dotenv": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-10.0.0.tgz", + "integrity": "sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==", + "engines": { + "node": ">=10" + } + }, + "node_modules/dynamic-dedupe": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/dynamic-dedupe/-/dynamic-dedupe-0.3.0.tgz", + "integrity": "sha512-ssuANeD+z97meYOqd50e04Ze5qp4bPqo8cCkI4TRjZkzAUgIDTrXV1R8QCdINpiI+hw14+rYazvTRdQrz0/rFQ==", + "dev": true, + "dependencies": { + "xtend": "^4.0.0" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/fast-copy": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/fast-copy/-/fast-copy-3.0.1.tgz", + "integrity": "sha512-Knr7NOtK3HWRYGtHoJrjkaWepqT8thIVGAwt0p0aUs1zqkAzXZV4vo9fFNwyb5fcqK1GKYFYxldQdIDVKhUAfA==" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, + "node_modules/fast-redact": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/fast-redact/-/fast-redact-3.1.2.tgz", + "integrity": "sha512-+0em+Iya9fKGfEQGcd62Yv6onjBmmhV1uh86XVfOU8VwAe6kaFdQCWI9s0/Nnugx5Vd9tdbZ7e6gE2tR9dzXdw==", + "engines": { + "node": ">=6" + } + }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==" + }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, + "node_modules/file-type": { + "version": "18.2.1", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-18.2.1.tgz", + "integrity": "sha512-Yw5MtnMv7vgD2/6Bjmmuegc8bQEVA9GmAyaR18bMYWKqsWDG9wgYZ1j4I6gNMF5Y5JBDcUcjRQqNQx7Y8uotcg==", + "dependencies": { + "readable-web-to-node-stream": "^3.0.2", + "strtok3": "^7.0.0", + "token-types": "^5.0.1" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sindresorhus/file-type?sponsor=1" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/for-each": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", + "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "dependencies": { + "is-callable": "^1.1.3" + } + }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + }, + "node_modules/get-intrinsic": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.0.tgz", + "integrity": "sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==", + "dependencies": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", + "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/help-me": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/help-me/-/help-me-4.2.0.tgz", + "integrity": "sha512-TAOnTB8Tz5Dw8penUuzHVrKNKlCIbwwbHnXraNJxPwf8LRtE2HlM84RYuezMFcwOJmoYOCWVDyJ8TQGxn9PgxA==", + "dependencies": { + "glob": "^8.0.0", + "readable-stream": "^3.6.0" + } + }, + "node_modules/help-me/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/help-me/node_modules/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/help-me/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/is-arguments": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", + "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz", + "integrity": "sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==", + "dev": true, + "dependencies": { + "has": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-generator-function": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz", + "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==", + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.10.tgz", + "integrity": "sha512-PJqgEHiWZvMpaFZ3uTc8kHPM4+4ADTlDniuQL7cU/UDA0Ql7F70yGfHph3cLNe+c9toaigv+DFzTJKhc2CtO6A==", + "dependencies": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + }, + "node_modules/jmespath": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.16.0.tgz", + "integrity": "sha512-9FzQjJ7MATs1tSpnco1K6ayiYE3figslrXA72G2HQ/n76RzvYlofyi5QM+iX4YRs/pu3yzxlVQSST23+dMDknw==", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/joycon": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", + "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==", + "engines": { + "node": ">=10" + } + }, + "node_modules/just-performance": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/just-performance/-/just-performance-4.3.0.tgz", + "integrity": "sha512-L7RjvtJsL0QO8xFs5wEoDDzzJwoiowRw6Rn/GnvldlchS2JQr9wFYPiwZcDfrbbujEKqKN0tvENdbjXdYhDp5Q==" + }, + "node_modules/limiter": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/limiter/-/limiter-2.1.0.tgz", + "integrity": "sha512-361TYz6iay6n+9KvUUImqdLuFigK+K79qrUtBsXhJTLdH4rIt/r1y8r1iozwh8KbZNpujbFTSh74mJ7bwbAMOw==", + "dependencies": { + "just-performance": "4.3.0" + } + }, + "node_modules/linguini": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/linguini/-/linguini-1.3.1.tgz", + "integrity": "sha512-nGg/+fMQ4zQyEqDLaniETT31y1NQs8JhlCkbRhwuxTk8fKiUDFzflie58H0cHFObd2OMHKNncYSXf7HqWOgxxw==", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "node_modules/lodash.snakecase": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz", + "integrity": "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==" + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.0.tgz", + "integrity": "sha512-BKwRP/O0UvoMKp7GNdwPlObhYGB5DQqwhEDQlNKuoqwVYSxkSZCSbHjnFFmUEtwSKRPU4kNK8PbDYYitwaE3QA==", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/on-exit-leak-free": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.0.tgz", + "integrity": "sha512-VuCaZZAjReZ3vUwgOB8LxAosIurDiAW0s13rI1YwmaP++jvcxP77AWoQvenZebpCA2m8WC1/EosPYPMjnRAp/w==" + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/peek-readable": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-5.0.0.tgz", + "integrity": "sha512-YtCKvLUOvwtMGmrniQPdO7MwPjgkFBtFIrmfSbYmYuq3tKDV/mcfAhBth1+C3ru7uXIZasc/pHnb+YDYNkkj4A==", + "engines": { + "node": ">=14.16" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pino": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/pino/-/pino-8.11.0.tgz", + "integrity": "sha512-Z2eKSvlrl2rH8p5eveNUnTdd4AjJk8tAsLkHYZQKGHP4WTh2Gi1cOSOs3eWPqaj+niS3gj4UkoreoaWgF3ZWYg==", + "dependencies": { + "atomic-sleep": "^1.0.0", + "fast-redact": "^3.1.1", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "v1.0.0", + "pino-std-serializers": "^6.0.0", + "process-warning": "^2.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^3.1.0", + "thread-stream": "^2.0.0" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/pino-abstract-transport": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-1.0.0.tgz", + "integrity": "sha512-c7vo5OpW4wIS42hUVcT5REsL8ZljsUfBjqV/e2sFxmFEFZiq1XLUp5EYLtuDH6PEHq9W1egWqRbnLUP5FuZmOA==", + "dependencies": { + "readable-stream": "^4.0.0", + "split2": "^4.0.0" + } + }, + "node_modules/pino-abstract-transport/node_modules/readable-stream": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.3.0.tgz", + "integrity": "sha512-MuEnA0lbSi7JS8XM+WNJlWZkHAAdm7gETHdFK//Q/mChGyj2akEFtdLZh32jSdkWGbRwCW9pn6g3LWDdDeZnBQ==", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/pino-pretty": { + "version": "9.4.0", + "resolved": "https://registry.npmjs.org/pino-pretty/-/pino-pretty-9.4.0.tgz", + "integrity": "sha512-NIudkNLxnl7MGj1XkvsqVyRgo6meFP82ECXF2PlOI+9ghmbGuBUUqKJ7IZPIxpJw4vhhSva0IuiDSAuGh6TV9g==", + "dependencies": { + "colorette": "^2.0.7", + "dateformat": "^4.6.3", + "fast-copy": "^3.0.0", + "fast-safe-stringify": "^2.1.1", + "help-me": "^4.0.1", + "joycon": "^3.1.1", + "minimist": "^1.2.6", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^1.0.0", + "pump": "^3.0.0", + "readable-stream": "^4.0.0", + "secure-json-parse": "^2.4.0", + "sonic-boom": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "bin": { + "pino-pretty": "bin.js" + } + }, + "node_modules/pino-pretty/node_modules/readable-stream": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.3.0.tgz", + "integrity": "sha512-MuEnA0lbSi7JS8XM+WNJlWZkHAAdm7gETHdFK//Q/mChGyj2akEFtdLZh32jSdkWGbRwCW9pn6g3LWDdDeZnBQ==", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/pino-pretty/node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pino-std-serializers": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-6.1.0.tgz", + "integrity": "sha512-KO0m2f1HkrPe9S0ldjx7za9BJjeHqBku5Ch8JyxETxT8dEFGz1PwgrHaOQupVYitpzbFSYm7nnljxD8dik2c+g==" + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/process-warning": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-2.1.0.tgz", + "integrity": "sha512-9C20RLxrZU/rFnxWncDkuF6O999NdIf3E1ws4B0ZeY3sRVPzWBMsYDE2lxjxhiXxg464cQTgKUGm8/i6y2YGXg==" + }, + "node_modules/pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", + "integrity": "sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw==" + }, + "node_modules/querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g==", + "deprecated": "The querystring API is considered Legacy. new code should use the URLSearchParams API instead.", + "engines": { + "node": ">=0.4.x" + } + }, + "node_modules/quick-format-unescaped": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==" + }, + "node_modules/readable-stream": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.1.tgz", + "integrity": "sha512-+rQmrWMYGA90yenhTYsLWAsLsqVC8osOw6PKE1HDYiO0gdPeKe/xDHNzIAIn4C91YQ6oenEhfYqqc1883qHbjQ==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readable-web-to-node-stream": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/readable-web-to-node-stream/-/readable-web-to-node-stream-3.0.2.tgz", + "integrity": "sha512-ePeK6cc1EcKLEhJFt/AebMCLL+GgSKhuygrZ/GLaKZYEecIgIECf4UaUuaByiGtzckwR4ain9VzUh95T1exYGw==", + "dependencies": { + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/real-require": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/resolve": { + "version": "1.22.1", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", + "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", + "dev": true, + "dependencies": { + "is-core-module": "^2.9.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safe-stable-stringify": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.4.2.tgz", + "integrity": "sha512-gMxvPJYhP0O9n2pvcfYfIuYgbledAOJFcqRThtPRmjscaipiwcwPPKLytpVzMkG2HAN87Qmo2d4PtGiri1dSLA==", + "engines": { + "node": ">=10" + } + }, + "node_modules/sax": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz", + "integrity": "sha512-8I2a3LovHTOpm7NV5yOyO8IHqgVsfK4+UuySrXU8YXkSRX7k6hCV9b3HrkKCr3nMpgj+0bmocaJJWpvp1oc7ZA==" + }, + "node_modules/secure-json-parse": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz", + "integrity": "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==" + }, + "node_modules/sonic-boom": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-3.2.1.tgz", + "integrity": "sha512-iITeTHxy3B9FGu8aVdiDXUVAcHMF9Ss0cCsAOo2HfCrmVGT3/DT5oYaeu0M/YKZDlKTvChEyPq0zI9Hf33EX6A==", + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/split2": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.1.0.tgz", + "integrity": "sha512-VBiJxFkxiXRlUIeyMQi8s4hgvKCSjtknJv/LVYbrgALPwf5zSKmEwV9Lst25AkvMDnvxODugjdl6KZgwKM1WYQ==", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strtok3": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-7.0.0.tgz", + "integrity": "sha512-pQ+V+nYQdC5H3Q7qBZAz/MO6lwGhoC2gOAjuouGf/VO0m7vQRh8QNMl2Uf6SwAtzZ9bOw3UIeBukEGNJl5dtXQ==", + "dependencies": { + "@tokenizer/token": "^0.3.0", + "peek-readable": "^5.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/thread-stream": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-2.3.0.tgz", + "integrity": "sha512-kaDqm1DET9pp3NXwR8382WHbnpXnRkN9xGN9dQt3B2+dmXiW8X1SOwmFOxAErEQ47ObhZ96J6yhZNXuyCOL7KA==", + "dependencies": { + "real-require": "^0.2.0" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/token-types": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/token-types/-/token-types-5.0.1.tgz", + "integrity": "sha512-Y2fmSnZjQdDb9W4w4r1tswlMHylzWIeOKpx0aZH9BgGtACHhrk3OkT52AzwcuqTRBZtvvnTjDBh8eynMulu8Vg==", + "dependencies": { + "@tokenizer/token": "^0.3.0", + "ieee754": "^1.2.1" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/ts-mixer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/ts-mixer/-/ts-mixer-6.0.3.tgz", + "integrity": "sha512-k43M7uCG1AkTyxgnmI5MPwKoUvS/bRvLvUb7+Pgpdlmok8AoqmUaZxUUw8zKM5B1lqZrt41GjYgnvAi0fppqgQ==" + }, + "node_modules/ts-node": { + "version": "10.9.1", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", + "integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==", + "dev": true, + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/ts-node-dev": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ts-node-dev/-/ts-node-dev-2.0.0.tgz", + "integrity": "sha512-ywMrhCfH6M75yftYvrvNarLEY+SUXtUvU8/0Z6llrHQVBx12GiFk5sStF8UdfE/yfzk9IAq7O5EEbTQsxlBI8w==", + "dev": true, + "dependencies": { + "chokidar": "^3.5.1", + "dynamic-dedupe": "^0.3.0", + "minimist": "^1.2.6", + "mkdirp": "^1.0.4", + "resolve": "^1.0.0", + "rimraf": "^2.6.1", + "source-map-support": "^0.5.12", + "tree-kill": "^1.2.2", + "ts-node": "^10.4.0", + "tsconfig": "^7.0.0" + }, + "bin": { + "ts-node-dev": "lib/bin.js", + "tsnd": "lib/bin.js" + }, + "engines": { + "node": ">=0.8.0" + }, + "peerDependencies": { + "node-notifier": "*", + "typescript": "*" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/tsconfig": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/tsconfig/-/tsconfig-7.0.0.tgz", + "integrity": "sha512-vZXmzPrL+EmC4T/4rVlT2jNVMWCi/O4DIiSj3UHg1OE5kCKbk4mfrXc6dZksLgRM/TZlKnousKH9bbTazUWRRw==", + "dev": true, + "dependencies": { + "@types/strip-bom": "^3.0.0", + "@types/strip-json-comments": "0.0.30", + "strip-bom": "^3.0.0", + "strip-json-comments": "^2.0.0" + } + }, + "node_modules/tslib": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", + "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==" + }, + "node_modules/typescript": { + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/undici": { + "version": "5.20.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.20.0.tgz", + "integrity": "sha512-J3j60dYzuo6Eevbawwp1sdg16k5Tf768bxYK4TUJRH7cBM4kFCbf3mOnM/0E3vQYXvpxITbbWmBafaDbxLDz3g==", + "dependencies": { + "busboy": "^1.6.0" + }, + "engines": { + "node": ">=12.18" + } + }, + "node_modules/url": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz", + "integrity": "sha512-hzSUW2q06EqL1gKM/a+obYHLIO6ct2hwPuviqTTOcfFVc61UbfJ2Q32+uGL/HCPxKqrdGB5QUwIe7UqlDgwsOQ==", + "dependencies": { + "punycode": "1.3.2", + "querystring": "0.2.0" + } + }, + "node_modules/util": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", + "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", + "dependencies": { + "inherits": "^2.0.3", + "is-arguments": "^1.0.4", + "is-generator-function": "^1.0.7", + "is-typed-array": "^1.1.3", + "which-typed-array": "^1.1.2" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, + "node_modules/uuid": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.0.0.tgz", + "integrity": "sha512-jOXGuXZAWdsTH7eZLtyXMqUb9EcWMGZNbL9YcGBJl4MH4nrxHmZJhEHvyLFrkxo+28uLb/NYRcStH48fnD0Vzw==", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true + }, + "node_modules/web-streams-polyfill": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz", + "integrity": "sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==", + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.9.tgz", + "integrity": "sha512-w9c4xkx6mPidwp7180ckYWfMmvxpjlZuIudNtDf4N/tTAUB8VJbX25qZoAsrtGuYNnGw3pa0AXgbGKRB8/EceA==", + "dependencies": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-tostringtag": "^1.0.0", + "is-typed-array": "^1.1.10" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + }, + "node_modules/ws": { + "version": "8.12.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.12.1.tgz", + "integrity": "sha512-1qo+M9Ba+xNhPB+YTWUlK6M17brTut5EXbcBaMRN5pH5dFrXz7lzz1ChFSUq3bOUl8yEvSenhHmYUNJxFzdJew==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml2js": { + "version": "0.4.19", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.19.tgz", + "integrity": "sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q==", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~9.0.1" + } + }, + "node_modules/xmlbuilder": { + "version": "9.0.7", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz", + "integrity": "sha512-7YXTQc3P2l9+0rjaUbLwMKRhtmwg1M1eDf6nag7urC7pIPYLD9W/jmzQ4ptRSUbodw5S0jfoGTflLemQibSpeQ==", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "dev": true, + "engines": { + "node": ">=0.4" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "engines": { + "node": ">=6" + } + } + }, + "dependencies": { + "@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "requires": { + "@jridgewell/trace-mapping": "0.3.9" + } + }, + "@discordjs/builders": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-1.4.0.tgz", + "integrity": "sha512-nEeTCheTTDw5kO93faM1j8ZJPonAX86qpq/QVoznnSa8WWcCgJpjlu6GylfINTDW6o7zZY0my2SYdxx2mfNwGA==", + "requires": { + "@discordjs/util": "^0.1.0", + "@sapphire/shapeshift": "^3.7.1", + "discord-api-types": "^0.37.20", + "fast-deep-equal": "^3.1.3", + "ts-mixer": "^6.0.2", + "tslib": "^2.4.1" + } + }, + "@discordjs/collection": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-1.3.0.tgz", + "integrity": "sha512-ylt2NyZ77bJbRij4h9u/wVy7qYw/aDqQLWnadjvDqW/WoWCxrsX6M3CIw9GVP5xcGCDxsrKj5e0r5evuFYwrKg==" + }, + "@discordjs/rest": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@discordjs/rest/-/rest-1.5.0.tgz", + "integrity": "sha512-lXgNFqHnbmzp5u81W0+frdXN6Etf4EUi8FAPcWpSykKd8hmlWh1xy6BmE0bsJypU1pxohaA8lQCgp70NUI3uzA==", + "requires": { + "@discordjs/collection": "^1.3.0", + "@discordjs/util": "^0.1.0", + "@sapphire/async-queue": "^1.5.0", + "@sapphire/snowflake": "^3.2.2", + "discord-api-types": "^0.37.23", + "file-type": "^18.0.0", + "tslib": "^2.4.1", + "undici": "^5.13.0" + } + }, + "@discordjs/util": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@discordjs/util/-/util-0.1.0.tgz", + "integrity": "sha512-e7d+PaTLVQav6rOc2tojh2y6FE8S7REkqLldq1XF4soCx74XB/DIjbVbVLtBemf0nLW77ntz0v+o5DytKwFNLQ==" + }, + "@jridgewell/resolve-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", + "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", + "dev": true + }, + "@jridgewell/sourcemap-codec": { + "version": "1.4.14", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", + "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", + "dev": true + }, + "@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "requires": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "@sapphire/async-queue": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@sapphire/async-queue/-/async-queue-1.5.0.tgz", + "integrity": "sha512-JkLdIsP8fPAdh9ZZjrbHWR/+mZj0wvKS5ICibcLrRI1j84UmLMshx5n9QmL8b95d4onJ2xxiyugTgSAX7AalmA==" + }, + "@sapphire/shapeshift": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/@sapphire/shapeshift/-/shapeshift-3.8.1.tgz", + "integrity": "sha512-xG1oXXBhCjPKbxrRTlox9ddaZTvVpOhYLmKmApD/vIWOV1xEYXnpoFs68zHIZBGbqztq6FrUPNPerIrO1Hqeaw==", + "requires": { + "fast-deep-equal": "^3.1.3", + "lodash": "^4.17.21" + } + }, + "@sapphire/snowflake": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/@sapphire/snowflake/-/snowflake-3.4.0.tgz", + "integrity": "sha512-zZxymtVO6zeXVMPds+6d7gv/OfnCc25M1Z+7ZLB0oPmeMTPeRWVPQSS16oDJy5ZsyCOLj7M6mbZml5gWXcVRNw==" + }, + "@tokenizer/token": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", + "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==" + }, + "@tsconfig/node10": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", + "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==", + "dev": true + }, + "@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true + }, + "@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true + }, + "@tsconfig/node16": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.3.tgz", + "integrity": "sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==", + "dev": true + }, + "@types/node": { + "version": "16.18.14", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.14.tgz", + "integrity": "sha512-wvzClDGQXOCVNU4APPopC2KtMYukaF1MN/W3xAmslx22Z4/IF1/izDMekuyoUlwfnDHYCIZGaj7jMwnJKBTxKw==" + }, + "@types/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-xevGOReSYGM7g/kUBZzPqCrR/KYAo+F0yiPc85WFTJa0MSLtyFTVTU6cJu/aV4mid7IffDIWqo69THF2o4JiEQ==", + "dev": true + }, + "@types/strip-json-comments": { + "version": "0.0.30", + "resolved": "https://registry.npmjs.org/@types/strip-json-comments/-/strip-json-comments-0.0.30.tgz", + "integrity": "sha512-7NQmHra/JILCd1QqpSzl8+mJRc8ZHz3uDm8YV1Ks9IhK0epEiTw8aIErbvH9PI+6XbqhyIQy3462nEsn7UVzjQ==", + "dev": true + }, + "@types/ws": { + "version": "8.5.4", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.4.tgz", + "integrity": "sha512-zdQDHKUgcX/zBc4GrwsE/7dVdAD8JR4EuiAXiiUhhfyIJXXb2+PrGshFyeXWQPMmmZ2XxgaqclgpIC7eTXc1mg==", + "requires": { + "@types/node": "*" + } + }, + "abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "requires": { + "event-target-shim": "^5.0.0" + } + }, + "acorn": { + "version": "8.8.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz", + "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==", + "dev": true + }, + "acorn-walk": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", + "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", + "dev": true + }, + "anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true + }, + "atomic-sleep": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==" + }, + "available-typed-arrays": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", + "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==" + }, + "aws-sdk": { + "version": "2.1328.0", + "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1328.0.tgz", + "integrity": "sha512-ud8ieE+hGX/cWHkQ9kMxQw6w+onbv71PDrcPmP2j8cmYv5IPlM5Zh8/tpsmXApLYDmQMuZ3TtssiB1KmoSbzgA==", + "requires": { + "buffer": "4.9.2", + "events": "1.1.1", + "ieee754": "1.1.13", + "jmespath": "0.16.0", + "querystring": "0.2.0", + "sax": "1.2.1", + "url": "0.10.3", + "util": "^0.12.4", + "uuid": "8.0.0", + "xml2js": "0.4.19" + }, + "dependencies": { + "buffer": { + "version": "4.9.2", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz", + "integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==", + "requires": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4", + "isarray": "^1.0.0" + } + }, + "events": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", + "integrity": "sha512-kEcvvCBByWXGnZy6JUlgAp2gBIUjfCAV6P6TgT1/aaQKcmuAEC4OZTV1I4EWQLz2gxZw76atuVyvHhTxvi0Flw==" + }, + "ieee754": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", + "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==" + } + } + }, + "balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" + }, + "binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true + }, + "busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "requires": { + "streamsearch": "^1.1.0" + } + }, + "call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "requires": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + } + }, + "chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, + "requires": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "fsevents": "~2.3.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + } + }, + "colorette": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.19.tgz", + "integrity": "sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==" + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true + }, + "data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==" + }, + "dateformat": { + "version": "4.6.3", + "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz", + "integrity": "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==" + }, + "diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true + }, + "discord-api-types": { + "version": "0.37.35", + "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.37.35.tgz", + "integrity": "sha512-iyKZ/82k7FX3lcmHiAvvWu5TmyfVo78RtghBV/YsehK6CID83k5SI03DKKopBcln+TiEIYw5MGgq7SJXSpNzMg==" + }, + "discord.js": { + "version": "14.7.1", + "resolved": "https://registry.npmjs.org/discord.js/-/discord.js-14.7.1.tgz", + "integrity": "sha512-1FECvqJJjjeYcjSm0IGMnPxLqja/pmG1B0W2l3lUY2Gi4KXiyTeQmU1IxWcbXHn2k+ytP587mMWqva2IA87EbA==", + "requires": { + "@discordjs/builders": "^1.4.0", + "@discordjs/collection": "^1.3.0", + "@discordjs/rest": "^1.4.0", + "@discordjs/util": "^0.1.0", + "@sapphire/snowflake": "^3.2.2", + "@types/ws": "^8.5.3", + "discord-api-types": "^0.37.20", + "fast-deep-equal": "^3.1.3", + "lodash.snakecase": "^4.1.1", + "tslib": "^2.4.1", + "undici": "^5.13.0", + "ws": "^8.11.0" + } + }, + "discord.js-rate-limiter": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/discord.js-rate-limiter/-/discord.js-rate-limiter-1.3.2.tgz", + "integrity": "sha512-gS0suMdhWKh4IJRf2NbzHGNJfYI9ApKhO5XsBLByT0qy5vbk6SiS/9FofQKoWgbx5XXOmQ8dwqQtzzUVlSh5ZA==", + "requires": { + "limiter": "2.1.0" + } + }, + "dotenv": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-10.0.0.tgz", + "integrity": "sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==" + }, + "dynamic-dedupe": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/dynamic-dedupe/-/dynamic-dedupe-0.3.0.tgz", + "integrity": "sha512-ssuANeD+z97meYOqd50e04Ze5qp4bPqo8cCkI4TRjZkzAUgIDTrXV1R8QCdINpiI+hw14+rYazvTRdQrz0/rFQ==", + "dev": true, + "requires": { + "xtend": "^4.0.0" + } + }, + "end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "requires": { + "once": "^1.4.0" + } + }, + "event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==" + }, + "events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==" + }, + "fast-copy": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/fast-copy/-/fast-copy-3.0.1.tgz", + "integrity": "sha512-Knr7NOtK3HWRYGtHoJrjkaWepqT8thIVGAwt0p0aUs1zqkAzXZV4vo9fFNwyb5fcqK1GKYFYxldQdIDVKhUAfA==" + }, + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, + "fast-redact": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/fast-redact/-/fast-redact-3.1.2.tgz", + "integrity": "sha512-+0em+Iya9fKGfEQGcd62Yv6onjBmmhV1uh86XVfOU8VwAe6kaFdQCWI9s0/Nnugx5Vd9tdbZ7e6gE2tR9dzXdw==" + }, + "fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==" + }, + "fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "requires": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + } + }, + "file-type": { + "version": "18.2.1", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-18.2.1.tgz", + "integrity": "sha512-Yw5MtnMv7vgD2/6Bjmmuegc8bQEVA9GmAyaR18bMYWKqsWDG9wgYZ1j4I6gNMF5Y5JBDcUcjRQqNQx7Y8uotcg==", + "requires": { + "readable-web-to-node-stream": "^3.0.2", + "strtok3": "^7.0.0", + "token-types": "^5.0.1" + } + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "for-each": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", + "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "requires": { + "is-callable": "^1.1.3" + } + }, + "formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "requires": { + "fetch-blob": "^3.1.2" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" + }, + "fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "optional": true + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + }, + "get-intrinsic": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.0.tgz", + "integrity": "sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==", + "requires": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.3" + } + }, + "glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + }, + "gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "requires": { + "get-intrinsic": "^1.1.3" + } + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==" + }, + "has-tostringtag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", + "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "requires": { + "has-symbols": "^1.0.2" + } + }, + "help-me": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/help-me/-/help-me-4.2.0.tgz", + "integrity": "sha512-TAOnTB8Tz5Dw8penUuzHVrKNKlCIbwwbHnXraNJxPwf8LRtE2HlM84RYuezMFcwOJmoYOCWVDyJ8TQGxn9PgxA==", + "requires": { + "glob": "^8.0.0", + "readable-stream": "^3.6.0" + }, + "dependencies": { + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "requires": { + "balanced-match": "^1.0.0" + } + }, + "glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + } + }, + "minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "requires": { + "brace-expansion": "^2.0.1" + } + } + } + }, + "ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "is-arguments": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", + "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", + "requires": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + } + }, + "is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "requires": { + "binary-extensions": "^2.0.0" + } + }, + "is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==" + }, + "is-core-module": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz", + "integrity": "sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==", + "dev": true, + "requires": { + "has": "^1.0.3" + } + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true + }, + "is-generator-function": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz", + "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==", + "requires": { + "has-tostringtag": "^1.0.0" + } + }, + "is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "is-typed-array": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.10.tgz", + "integrity": "sha512-PJqgEHiWZvMpaFZ3uTc8kHPM4+4ADTlDniuQL7cU/UDA0Ql7F70yGfHph3cLNe+c9toaigv+DFzTJKhc2CtO6A==", + "requires": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-tostringtag": "^1.0.0" + } + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + }, + "jmespath": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.16.0.tgz", + "integrity": "sha512-9FzQjJ7MATs1tSpnco1K6ayiYE3figslrXA72G2HQ/n76RzvYlofyi5QM+iX4YRs/pu3yzxlVQSST23+dMDknw==" + }, + "joycon": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", + "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==" + }, + "just-performance": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/just-performance/-/just-performance-4.3.0.tgz", + "integrity": "sha512-L7RjvtJsL0QO8xFs5wEoDDzzJwoiowRw6Rn/GnvldlchS2JQr9wFYPiwZcDfrbbujEKqKN0tvENdbjXdYhDp5Q==" + }, + "limiter": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/limiter/-/limiter-2.1.0.tgz", + "integrity": "sha512-361TYz6iay6n+9KvUUImqdLuFigK+K79qrUtBsXhJTLdH4rIt/r1y8r1iozwh8KbZNpujbFTSh74mJ7bwbAMOw==", + "requires": { + "just-performance": "4.3.0" + } + }, + "linguini": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/linguini/-/linguini-1.3.1.tgz", + "integrity": "sha512-nGg/+fMQ4zQyEqDLaniETT31y1NQs8JhlCkbRhwuxTk8fKiUDFzflie58H0cHFObd2OMHKNncYSXf7HqWOgxxw==" + }, + "lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "lodash.snakecase": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz", + "integrity": "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==" + }, + "make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==" + }, + "mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true + }, + "node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==" + }, + "node-fetch": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.0.tgz", + "integrity": "sha512-BKwRP/O0UvoMKp7GNdwPlObhYGB5DQqwhEDQlNKuoqwVYSxkSZCSbHjnFFmUEtwSKRPU4kNK8PbDYYitwaE3QA==", + "requires": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + } + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true + }, + "on-exit-leak-free": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.0.tgz", + "integrity": "sha512-VuCaZZAjReZ3vUwgOB8LxAosIurDiAW0s13rI1YwmaP++jvcxP77AWoQvenZebpCA2m8WC1/EosPYPMjnRAp/w==" + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "requires": { + "wrappy": "1" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true + }, + "path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "peek-readable": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-5.0.0.tgz", + "integrity": "sha512-YtCKvLUOvwtMGmrniQPdO7MwPjgkFBtFIrmfSbYmYuq3tKDV/mcfAhBth1+C3ru7uXIZasc/pHnb+YDYNkkj4A==" + }, + "picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true + }, + "pino": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/pino/-/pino-8.11.0.tgz", + "integrity": "sha512-Z2eKSvlrl2rH8p5eveNUnTdd4AjJk8tAsLkHYZQKGHP4WTh2Gi1cOSOs3eWPqaj+niS3gj4UkoreoaWgF3ZWYg==", + "requires": { + "atomic-sleep": "^1.0.0", + "fast-redact": "^3.1.1", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "v1.0.0", + "pino-std-serializers": "^6.0.0", + "process-warning": "^2.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^3.1.0", + "thread-stream": "^2.0.0" + } + }, + "pino-abstract-transport": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-1.0.0.tgz", + "integrity": "sha512-c7vo5OpW4wIS42hUVcT5REsL8ZljsUfBjqV/e2sFxmFEFZiq1XLUp5EYLtuDH6PEHq9W1egWqRbnLUP5FuZmOA==", + "requires": { + "readable-stream": "^4.0.0", + "split2": "^4.0.0" + }, + "dependencies": { + "readable-stream": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.3.0.tgz", + "integrity": "sha512-MuEnA0lbSi7JS8XM+WNJlWZkHAAdm7gETHdFK//Q/mChGyj2akEFtdLZh32jSdkWGbRwCW9pn6g3LWDdDeZnBQ==", + "requires": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10" + } + } + } + }, + "pino-pretty": { + "version": "9.4.0", + "resolved": "https://registry.npmjs.org/pino-pretty/-/pino-pretty-9.4.0.tgz", + "integrity": "sha512-NIudkNLxnl7MGj1XkvsqVyRgo6meFP82ECXF2PlOI+9ghmbGuBUUqKJ7IZPIxpJw4vhhSva0IuiDSAuGh6TV9g==", + "requires": { + "colorette": "^2.0.7", + "dateformat": "^4.6.3", + "fast-copy": "^3.0.0", + "fast-safe-stringify": "^2.1.1", + "help-me": "^4.0.1", + "joycon": "^3.1.1", + "minimist": "^1.2.6", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^1.0.0", + "pump": "^3.0.0", + "readable-stream": "^4.0.0", + "secure-json-parse": "^2.4.0", + "sonic-boom": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "dependencies": { + "readable-stream": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.3.0.tgz", + "integrity": "sha512-MuEnA0lbSi7JS8XM+WNJlWZkHAAdm7gETHdFK//Q/mChGyj2akEFtdLZh32jSdkWGbRwCW9pn6g3LWDdDeZnBQ==", + "requires": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10" + } + }, + "strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==" + } + } + }, + "pino-std-serializers": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-6.1.0.tgz", + "integrity": "sha512-KO0m2f1HkrPe9S0ldjx7za9BJjeHqBku5Ch8JyxETxT8dEFGz1PwgrHaOQupVYitpzbFSYm7nnljxD8dik2c+g==" + }, + "process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==" + }, + "process-warning": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-2.1.0.tgz", + "integrity": "sha512-9C20RLxrZU/rFnxWncDkuF6O999NdIf3E1ws4B0ZeY3sRVPzWBMsYDE2lxjxhiXxg464cQTgKUGm8/i6y2YGXg==" + }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "punycode": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", + "integrity": "sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw==" + }, + "querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g==" + }, + "quick-format-unescaped": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==" + }, + "readable-stream": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.1.tgz", + "integrity": "sha512-+rQmrWMYGA90yenhTYsLWAsLsqVC8osOw6PKE1HDYiO0gdPeKe/xDHNzIAIn4C91YQ6oenEhfYqqc1883qHbjQ==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "readable-web-to-node-stream": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/readable-web-to-node-stream/-/readable-web-to-node-stream-3.0.2.tgz", + "integrity": "sha512-ePeK6cc1EcKLEhJFt/AebMCLL+GgSKhuygrZ/GLaKZYEecIgIECf4UaUuaByiGtzckwR4ain9VzUh95T1exYGw==", + "requires": { + "readable-stream": "^3.6.0" + } + }, + "readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "requires": { + "picomatch": "^2.2.1" + } + }, + "real-require": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==" + }, + "resolve": { + "version": "1.22.1", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", + "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", + "dev": true, + "requires": { + "is-core-module": "^2.9.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + } + }, + "rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + }, + "safe-stable-stringify": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.4.2.tgz", + "integrity": "sha512-gMxvPJYhP0O9n2pvcfYfIuYgbledAOJFcqRThtPRmjscaipiwcwPPKLytpVzMkG2HAN87Qmo2d4PtGiri1dSLA==" + }, + "sax": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz", + "integrity": "sha512-8I2a3LovHTOpm7NV5yOyO8IHqgVsfK4+UuySrXU8YXkSRX7k6hCV9b3HrkKCr3nMpgj+0bmocaJJWpvp1oc7ZA==" + }, + "secure-json-parse": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz", + "integrity": "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==" + }, + "sonic-boom": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-3.2.1.tgz", + "integrity": "sha512-iITeTHxy3B9FGu8aVdiDXUVAcHMF9Ss0cCsAOo2HfCrmVGT3/DT5oYaeu0M/YKZDlKTvChEyPq0zI9Hf33EX6A==", + "requires": { + "atomic-sleep": "^1.0.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "split2": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.1.0.tgz", + "integrity": "sha512-VBiJxFkxiXRlUIeyMQi8s4hgvKCSjtknJv/LVYbrgALPwf5zSKmEwV9Lst25AkvMDnvxODugjdl6KZgwKM1WYQ==" + }, + "streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==" + }, + "string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "requires": { + "safe-buffer": "~5.2.0" + } + }, + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true + }, + "strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "dev": true + }, + "strtok3": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-7.0.0.tgz", + "integrity": "sha512-pQ+V+nYQdC5H3Q7qBZAz/MO6lwGhoC2gOAjuouGf/VO0m7vQRh8QNMl2Uf6SwAtzZ9bOw3UIeBukEGNJl5dtXQ==", + "requires": { + "@tokenizer/token": "^0.3.0", + "peek-readable": "^5.0.0" + } + }, + "supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true + }, + "thread-stream": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-2.3.0.tgz", + "integrity": "sha512-kaDqm1DET9pp3NXwR8382WHbnpXnRkN9xGN9dQt3B2+dmXiW8X1SOwmFOxAErEQ47ObhZ96J6yhZNXuyCOL7KA==", + "requires": { + "real-require": "^0.2.0" + } + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + }, + "token-types": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/token-types/-/token-types-5.0.1.tgz", + "integrity": "sha512-Y2fmSnZjQdDb9W4w4r1tswlMHylzWIeOKpx0aZH9BgGtACHhrk3OkT52AzwcuqTRBZtvvnTjDBh8eynMulu8Vg==", + "requires": { + "@tokenizer/token": "^0.3.0", + "ieee754": "^1.2.1" + } + }, + "tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true + }, + "ts-mixer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/ts-mixer/-/ts-mixer-6.0.3.tgz", + "integrity": "sha512-k43M7uCG1AkTyxgnmI5MPwKoUvS/bRvLvUb7+Pgpdlmok8AoqmUaZxUUw8zKM5B1lqZrt41GjYgnvAi0fppqgQ==" + }, + "ts-node": { + "version": "10.9.1", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", + "integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==", + "dev": true, + "requires": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + } + }, + "ts-node-dev": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ts-node-dev/-/ts-node-dev-2.0.0.tgz", + "integrity": "sha512-ywMrhCfH6M75yftYvrvNarLEY+SUXtUvU8/0Z6llrHQVBx12GiFk5sStF8UdfE/yfzk9IAq7O5EEbTQsxlBI8w==", + "dev": true, + "requires": { + "chokidar": "^3.5.1", + "dynamic-dedupe": "^0.3.0", + "minimist": "^1.2.6", + "mkdirp": "^1.0.4", + "resolve": "^1.0.0", + "rimraf": "^2.6.1", + "source-map-support": "^0.5.12", + "tree-kill": "^1.2.2", + "ts-node": "^10.4.0", + "tsconfig": "^7.0.0" + } + }, + "tsconfig": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/tsconfig/-/tsconfig-7.0.0.tgz", + "integrity": "sha512-vZXmzPrL+EmC4T/4rVlT2jNVMWCi/O4DIiSj3UHg1OE5kCKbk4mfrXc6dZksLgRM/TZlKnousKH9bbTazUWRRw==", + "dev": true, + "requires": { + "@types/strip-bom": "^3.0.0", + "@types/strip-json-comments": "0.0.30", + "strip-bom": "^3.0.0", + "strip-json-comments": "^2.0.0" + } + }, + "tslib": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", + "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==" + }, + "typescript": { + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "dev": true + }, + "undici": { + "version": "5.20.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.20.0.tgz", + "integrity": "sha512-J3j60dYzuo6Eevbawwp1sdg16k5Tf768bxYK4TUJRH7cBM4kFCbf3mOnM/0E3vQYXvpxITbbWmBafaDbxLDz3g==", + "requires": { + "busboy": "^1.6.0" + } + }, + "url": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz", + "integrity": "sha512-hzSUW2q06EqL1gKM/a+obYHLIO6ct2hwPuviqTTOcfFVc61UbfJ2Q32+uGL/HCPxKqrdGB5QUwIe7UqlDgwsOQ==", + "requires": { + "punycode": "1.3.2", + "querystring": "0.2.0" + } + }, + "util": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", + "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", + "requires": { + "inherits": "^2.0.3", + "is-arguments": "^1.0.4", + "is-generator-function": "^1.0.7", + "is-typed-array": "^1.1.3", + "which-typed-array": "^1.1.2" + } + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, + "uuid": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.0.0.tgz", + "integrity": "sha512-jOXGuXZAWdsTH7eZLtyXMqUb9EcWMGZNbL9YcGBJl4MH4nrxHmZJhEHvyLFrkxo+28uLb/NYRcStH48fnD0Vzw==" + }, + "v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true + }, + "web-streams-polyfill": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz", + "integrity": "sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==" + }, + "which-typed-array": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.9.tgz", + "integrity": "sha512-w9c4xkx6mPidwp7180ckYWfMmvxpjlZuIudNtDf4N/tTAUB8VJbX25qZoAsrtGuYNnGw3pa0AXgbGKRB8/EceA==", + "requires": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-tostringtag": "^1.0.0", + "is-typed-array": "^1.1.10" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + }, + "ws": { + "version": "8.12.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.12.1.tgz", + "integrity": "sha512-1qo+M9Ba+xNhPB+YTWUlK6M17brTut5EXbcBaMRN5pH5dFrXz7lzz1ChFSUq3bOUl8yEvSenhHmYUNJxFzdJew==" + }, + "xml2js": { + "version": "0.4.19", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.19.tgz", + "integrity": "sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q==", + "requires": { + "sax": ">=0.6.0", + "xmlbuilder": "~9.0.1" + } + }, + "xmlbuilder": { + "version": "9.0.7", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz", + "integrity": "sha512-7YXTQc3P2l9+0rjaUbLwMKRhtmwg1M1eDf6nag7urC7pIPYLD9W/jmzQ4ptRSUbodw5S0jfoGTflLemQibSpeQ==" + }, + "xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "dev": true + }, + "yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true + } + } +} diff --git a/discord-bot/package.json b/discord-bot/package.json new file mode 100644 index 0000000..2d01e20 --- /dev/null +++ b/discord-bot/package.json @@ -0,0 +1,42 @@ +{ + "name": "palworld-server-bot", + "version": "1.0.0", + "license": "Apache-2.0", + "private": true, + "engines": { + "node": ">=16.9.0" + }, + "type": "module", + "exports": [ + "./dist/start-bot.js" + ], + "scripts": { + "lint": "eslint . --cache --ext .js,.jsx,.ts,.tsx", + "lint:fix": "eslint . --fix --cache --ext .js,.jsx,.ts,.tsx", + "format": "prettier --check .", + "format:fix": "prettier --write .", + "build": "tsc --project tsconfig.json", + "commands:view": "npm run build && node --enable-source-maps dist/start-bot.js commands view", + "commands:register": "npm run build && node --enable-source-maps dist/start-bot.js commands register", + "commands:rename": "npm run build && node --enable-source-maps dist/start-bot.js commands rename", + "commands:delete": "npm run build && node --enable-source-maps dist/start-bot.js commands delete", + "commands:clear": "npm run build && node --enable-source-maps dist/start-bot.js commands clear", + "start": "npm run start:bot", + "start:bot": "npm run build && node --enable-source-maps dist/start-bot.js" + }, + "dependencies": { + "aws-sdk": "^2.1328.0", + "discord.js": "^14.7.1", + "discord.js-rate-limiter": "^1.3.2", + "dotenv": "^10.0.0", + "linguini": "^1.3.1", + "node-fetch": "^3.3.0", + "pino": "^8.11.0", + "pino-pretty": "^9.4.0" + }, + "devDependencies": { + "@types/node": "^16.4.10", + "ts-node-dev": "^2.0.0", + "typescript": "^4.3.5" + } +} diff --git a/discord-bot/src/commands/args.ts b/discord-bot/src/commands/args.ts new file mode 100644 index 0000000..964645a --- /dev/null +++ b/discord-bot/src/commands/args.ts @@ -0,0 +1,49 @@ +import { + APIApplicationCommandBasicOption, + ApplicationCommandOptionType, +} from "discord.js"; + +import { Language } from "../models/enum-helpers/language.js"; +import { Lang } from "../services/index.js"; + +export class Args { + public static startOption( + serverNames: string[] + ): APIApplicationCommandBasicOption { + return { + name: Lang.getRef("arguments.option", Language.Default), + name_localizations: Lang.getRefLocalizationMap("arguments.option"), + description: Lang.getRef("argDescs.startOption", Language.Default), + description_localizations: Lang.getRefLocalizationMap( + "argDescs.startOption" + ), + type: ApplicationCommandOptionType.String, + choices: serverNames.map((serverName) => { + return { + name: serverName, + value: serverName, + }; + }), + }; + } + + public static stopOption( + serverNames: string[] + ): APIApplicationCommandBasicOption { + return { + name: Lang.getRef("arguments.option", Language.Default), + name_localizations: Lang.getRefLocalizationMap("arguments.option"), + description: Lang.getRef("argDescs.stopOption", Language.Default), + description_localizations: Lang.getRefLocalizationMap( + "argDescs.stopOption" + ), + type: ApplicationCommandOptionType.String, + choices: serverNames.map((serverName) => { + return { + name: serverName, + value: serverName, + }; + }), + }; + } +} diff --git a/discord-bot/src/commands/chat/index.ts b/discord-bot/src/commands/chat/index.ts new file mode 100644 index 0000000..5a2ac67 --- /dev/null +++ b/discord-bot/src/commands/chat/index.ts @@ -0,0 +1,2 @@ +export { StartServerCommand } from "./start-server-command.js"; +export { StopServerCommand } from "./stop-server-command.js"; diff --git a/discord-bot/src/commands/chat/start-server-command.ts b/discord-bot/src/commands/chat/start-server-command.ts new file mode 100644 index 0000000..d8386c0 --- /dev/null +++ b/discord-bot/src/commands/chat/start-server-command.ts @@ -0,0 +1,57 @@ +import { + ChatInputCommandInteraction, + EmbedBuilder, + PermissionsString, +} from "discord.js"; +import { Language } from "../../models/enum-helpers/language.js"; +import { EventData } from "../../models/internal-models.js"; +import { Lang } from "../../services/index.js"; +import { InteractionUtils } from "../../utils/interaction-utils.js"; +import { Command, CommandDeferType } from "../index.js"; +import { MultiServersService } from "../../services/multi-servers-service.js"; + +export class StartServerCommand implements Command { + constructor(private readonly multiServersService: MultiServersService) {} + + public names = [Lang.getRef("chatCommands.startServer", Language.Default)]; + public deferType = CommandDeferType.PUBLIC; + public requireClientPerms: PermissionsString[] = []; + + public async execute( + intr: ChatInputCommandInteraction, + data: EventData + ): Promise { + try { + let embed: EmbedBuilder; + let args = { + option: intr.options.getString( + Lang.getRef("arguments.option", Language.Default) + ), + }; + + if (args.option) { + try { + await this.multiServersService.startServerByName(args.option); + embed = Lang.getEmbed("displayEmbeds.startServer", data.lang, { + SERVER_NAME: args.option, + }); + } catch (error) { + embed = Lang.getEmbed("displayEmbeds.invalidServerName", data.lang, { + SERVER_NAME: args.option, + }); + } + } else { + embed = Lang.getEmbed("displayEmbeds.emptyServerName", data.lang); + } + + await InteractionUtils.send(intr, embed); + } catch { + const embed: EmbedBuilder = Lang.getEmbed( + "displayEmbeds.noCredentials", + data.lang + ); + + await InteractionUtils.send(intr, embed); + } + } +} diff --git a/discord-bot/src/commands/chat/stop-server-command.ts b/discord-bot/src/commands/chat/stop-server-command.ts new file mode 100644 index 0000000..a309fb6 --- /dev/null +++ b/discord-bot/src/commands/chat/stop-server-command.ts @@ -0,0 +1,58 @@ +import { + ChatInputCommandInteraction, + EmbedBuilder, + PermissionsString, +} from "discord.js"; +import { Language } from "../../models/enum-helpers/language.js"; +import { EventData } from "../../models/internal-models.js"; +import { Lang } from "../../services/index.js"; +import { InteractionUtils } from "../../utils/interaction-utils.js"; +import { Command, CommandDeferType } from "../index.js"; +import { MultiServersService } from "../../services/multi-servers-service.js"; + +export class StopServerCommand implements Command { + constructor(private readonly multiServersService: MultiServersService) {} + + public names = [Lang.getRef("chatCommands.stopServer", Language.Default)]; + public deferType = CommandDeferType.PUBLIC; + public requireClientPerms: PermissionsString[] = []; + + public async execute( + intr: ChatInputCommandInteraction, + data: EventData + ): Promise { + try { + let embed: EmbedBuilder; + let args = { + option: intr.options.getString( + Lang.getRef("arguments.option", Language.Default) + ), + }; + + if (args.option) { + try { + // TODO: Check that the server is running before stopping it + await this.multiServersService.stopServerByName(args.option); + embed = Lang.getEmbed("displayEmbeds.stopServer", data.lang, { + SERVER_NAME: args.option, + }); + } catch (error) { + embed = Lang.getEmbed("displayEmbeds.invalidServerName", data.lang, { + SERVER_NAME: args.option, + }); + } + } else { + embed = Lang.getEmbed("displayEmbeds.emptyServerName", data.lang); + } + + await InteractionUtils.send(intr, embed); + } catch { + const embed: EmbedBuilder = Lang.getEmbed( + "displayEmbeds.noCredentials", + data.lang + ); + + await InteractionUtils.send(intr, embed); + } + } +} diff --git a/discord-bot/src/commands/command.ts b/discord-bot/src/commands/command.ts new file mode 100644 index 0000000..758bf33 --- /dev/null +++ b/discord-bot/src/commands/command.ts @@ -0,0 +1,28 @@ +import { + ApplicationCommandOptionChoiceData, + AutocompleteFocusedOption, + AutocompleteInteraction, + CommandInteraction, + PermissionsString, +} from "discord.js"; +import { RateLimiter } from "discord.js-rate-limiter"; + +import { EventData } from "../models/internal-models.js"; + +export interface Command { + names: string[]; + cooldown?: RateLimiter; + deferType: CommandDeferType; + requireClientPerms: PermissionsString[]; + autocomplete?( + intr: AutocompleteInteraction, + option: AutocompleteFocusedOption + ): Promise; + execute(intr: CommandInteraction, data: EventData): Promise; +} + +export enum CommandDeferType { + PUBLIC = "PUBLIC", + HIDDEN = "HIDDEN", + NONE = "NONE", +} diff --git a/discord-bot/src/commands/index.ts b/discord-bot/src/commands/index.ts new file mode 100644 index 0000000..85025f7 --- /dev/null +++ b/discord-bot/src/commands/index.ts @@ -0,0 +1,3 @@ +export { Args } from "./args.js"; +export { Command, CommandDeferType } from "./command.js"; +export { ChatCommandMetadata } from "./metadata.js"; diff --git a/discord-bot/src/commands/metadata.ts b/discord-bot/src/commands/metadata.ts new file mode 100644 index 0000000..b15ddb2 --- /dev/null +++ b/discord-bot/src/commands/metadata.ts @@ -0,0 +1,51 @@ +import { + ApplicationCommandType, + RESTPostAPIChatInputApplicationCommandsJSONBody, +} from "discord.js"; + +import { Args } from "./args.js"; +import { Language } from "../models/enum-helpers/language.js"; +import { Lang } from "../services/index.js"; + +export function ChatCommandMetadata(serverNames: string[]): { + [command: string]: RESTPostAPIChatInputApplicationCommandsJSONBody; +} { + return { + START_SERVER: { + type: ApplicationCommandType.ChatInput, + name: Lang.getRef("chatCommands.startServer", Language.Default), + name_localizations: Lang.getRefLocalizationMap( + "chatCommands.startServer" + ), + description: Lang.getRef("commandDescs.startServer", Language.Default), + description_localizations: Lang.getRefLocalizationMap( + "commandDescs.startServer" + ), + dm_permission: true, + default_member_permissions: undefined, + options: [ + { + ...Args.startOption(serverNames), + required: true, + }, + ], + }, + STOP_SERVER: { + type: ApplicationCommandType.ChatInput, + name: Lang.getRef("chatCommands.stopServer", Language.Default), + name_localizations: Lang.getRefLocalizationMap("chatCommands.stopServer"), + description: Lang.getRef("commandDescs.stopServer", Language.Default), + description_localizations: Lang.getRefLocalizationMap( + "commandDescs.stopServer" + ), + dm_permission: true, + default_member_permissions: undefined, + options: [ + { + ...Args.stopOption(serverNames), + required: true, + }, + ], + }, + }; +} diff --git a/discord-bot/src/constants/discord-limits.ts b/discord-bot/src/constants/discord-limits.ts new file mode 100644 index 0000000..3d3a54c --- /dev/null +++ b/discord-bot/src/constants/discord-limits.ts @@ -0,0 +1,3 @@ +export class DiscordLimits { + public static readonly CHOICES_PER_AUTOCOMPLETE = 25; +} diff --git a/discord-bot/src/constants/index.ts b/discord-bot/src/constants/index.ts new file mode 100644 index 0000000..1561e22 --- /dev/null +++ b/discord-bot/src/constants/index.ts @@ -0,0 +1 @@ +export { DiscordLimits } from "./discord-limits.js"; diff --git a/discord-bot/src/enums/help-option.ts b/discord-bot/src/enums/help-option.ts new file mode 100644 index 0000000..a49cc08 --- /dev/null +++ b/discord-bot/src/enums/help-option.ts @@ -0,0 +1,4 @@ +export enum HelpOption { + CONTACT_SUPPORT = "CONTACT_SUPPORT", + COMMANDS = "COMMANDS", +} diff --git a/discord-bot/src/events/command-handler.ts b/discord-bot/src/events/command-handler.ts new file mode 100644 index 0000000..300365d --- /dev/null +++ b/discord-bot/src/events/command-handler.ts @@ -0,0 +1,192 @@ +import { + AutocompleteInteraction, + ChatInputCommandInteraction, + CommandInteraction, + NewsChannel, + TextChannel, + ThreadChannel, +} from "discord.js"; +import { RateLimiter } from "discord.js-rate-limiter"; + +import { EventHandler } from "./index.js"; +import { Command, CommandDeferType } from "../commands/index.js"; +import { DiscordLimits } from "../constants/index.js"; +import { EventData } from "../models/internal-models"; +import { EventDataService, Lang, Logger } from "../services/index.js"; +import { CommandUtils, InteractionUtils } from "../utils/index.js"; +import { createRequire } from "node:module"; + +const require = createRequire(import.meta.url); +let Config = require("../../config/config.json"); +let Logs = require("../../lang/logs.json"); + +export class CommandHandler implements EventHandler { + private rateLimiter = new RateLimiter( + Config.rateLimiting.commands.amount, + Config.rateLimiting.commands.interval * 1000 + ); + + constructor( + public commands: Command[], + private eventDataService: EventDataService + ) {} + + public async process( + intr: CommandInteraction | AutocompleteInteraction + ): Promise { + // Don't respond to self, or other bots + if (intr.user.id === intr.client.user?.id || intr.user.bot) { + return; + } + + let commandParts = + intr instanceof ChatInputCommandInteraction || + intr instanceof AutocompleteInteraction + ? [ + intr.commandName, + intr.options.getSubcommandGroup(false), + intr.options.getSubcommand(false), + ].filter(Boolean) + : [intr.commandName]; + let commandName = commandParts.join(" "); + + // Try to find the command the user wants + let command = CommandUtils.findCommand(this.commands, commandParts); + if (!command) { + Logger.error( + Logs.error.commandNotFound + .replaceAll("{INTERACTION_ID}", intr.id) + .replaceAll("{COMMAND_NAME}", commandName) + ); + return; + } + + if (intr instanceof AutocompleteInteraction) { + if (!command.autocomplete) { + Logger.error( + Logs.error.autocompleteNotFound + .replaceAll("{INTERACTION_ID}", intr.id) + .replaceAll("{COMMAND_NAME}", commandName) + ); + return; + } + + try { + let option = intr.options.getFocused(true); + let choices = await command.autocomplete(intr, option); + await InteractionUtils.respond( + intr, + choices?.slice(0, DiscordLimits.CHOICES_PER_AUTOCOMPLETE) + ); + } catch (error) { + Logger.error( + intr.channel instanceof TextChannel || + intr.channel instanceof NewsChannel || + intr.channel instanceof ThreadChannel + ? Logs.error.autocompleteGuild + .replaceAll("{INTERACTION_ID}", intr.id) + .replaceAll("{OPTION_NAME}", commandName) + .replaceAll("{COMMAND_NAME}", commandName) + .replaceAll("{USER_TAG}", intr.user.tag) + .replaceAll("{USER_ID}", intr.user.id) + .replaceAll("{CHANNEL_NAME}", intr.channel.name) + .replaceAll("{CHANNEL_ID}", intr.channel.id) + .replaceAll("{GUILD_NAME}", intr.guild?.name) + .replaceAll("{GUILD_ID}", intr.guild?.id) + : Logs.error.autocompleteOther + .replaceAll("{INTERACTION_ID}", intr.id) + .replaceAll("{OPTION_NAME}", commandName) + .replaceAll("{COMMAND_NAME}", commandName) + .replaceAll("{USER_TAG}", intr.user.tag) + .replaceAll("{USER_ID}", intr.user.id), + error + ); + } + return; + } + + // Check if user is rate limited + let limited = this.rateLimiter.take(intr.user.id); + if (limited) { + return; + } + + // Defer interaction + // NOTE: Anything after this point we should be responding to the interaction + switch (command.deferType) { + case CommandDeferType.PUBLIC: { + await InteractionUtils.deferReply(intr, false); + break; + } + case CommandDeferType.HIDDEN: { + await InteractionUtils.deferReply(intr, true); + break; + } + } + + // Return if defer was unsuccessful + if (command.deferType !== CommandDeferType.NONE && !intr.deferred) { + return; + } + + // Get data from database + let data = await this.eventDataService.create({ + user: intr.user, + channel: intr.channel, + guild: intr.guild, + args: + intr instanceof ChatInputCommandInteraction ? intr.options : undefined, + }); + + try { + // Check if interaction passes command checks + let passesChecks = await CommandUtils.runChecks(command, intr, data); + if (passesChecks) { + // Execute the command + await command.execute(intr, data); + } + } catch (error) { + await this.sendError(intr, data); + + // Log command error + Logger.error( + intr.channel instanceof TextChannel || + intr.channel instanceof NewsChannel || + intr.channel instanceof ThreadChannel + ? Logs.error.commandGuild + .replaceAll("{INTERACTION_ID}", intr.id) + .replaceAll("{COMMAND_NAME}", commandName) + .replaceAll("{USER_TAG}", intr.user.tag) + .replaceAll("{USER_ID}", intr.user.id) + .replaceAll("{CHANNEL_NAME}", intr.channel.name) + .replaceAll("{CHANNEL_ID}", intr.channel.id) + .replaceAll("{GUILD_NAME}", intr.guild?.name) + .replaceAll("{GUILD_ID}", intr.guild?.id) + : Logs.error.commandOther + .replaceAll("{INTERACTION_ID}", intr.id) + .replaceAll("{COMMAND_NAME}", commandName) + .replaceAll("{USER_TAG}", intr.user.tag) + .replaceAll("{USER_ID}", intr.user.id), + error + ); + } + } + + private async sendError( + intr: CommandInteraction, + data: EventData + ): Promise { + try { + await InteractionUtils.send( + intr, + Lang.getEmbed("errorEmbeds.command", data.lang, { + ERROR_CODE: intr.id, + GUILD_ID: intr.guild?.id ?? Lang.getRef("other.na", data.lang), + SHARD_ID: (intr.guild?.shardId ?? 0).toString(), + }) + ); + } catch { + // Ignore + } + } +} diff --git a/discord-bot/src/events/event-handler.ts b/discord-bot/src/events/event-handler.ts new file mode 100644 index 0000000..082fa4d --- /dev/null +++ b/discord-bot/src/events/event-handler.ts @@ -0,0 +1,3 @@ +export interface EventHandler { + process(...args: any[]): Promise; +} diff --git a/discord-bot/src/events/index.ts b/discord-bot/src/events/index.ts new file mode 100644 index 0000000..1c6e556 --- /dev/null +++ b/discord-bot/src/events/index.ts @@ -0,0 +1,2 @@ +export { EventHandler } from "./event-handler.js"; +export { CommandHandler } from "./command-handler.js"; diff --git a/discord-bot/src/extensions/custom-client.ts b/discord-bot/src/extensions/custom-client.ts new file mode 100644 index 0000000..bf63ba9 --- /dev/null +++ b/discord-bot/src/extensions/custom-client.ts @@ -0,0 +1,23 @@ +import { ActivityType, Client, ClientOptions, Presence } from "discord.js"; + +export class CustomClient extends Client { + constructor(clientOptions: ClientOptions) { + super(clientOptions); + } + + public setPresence( + type: Exclude, + name: string, + url: string + ): Presence { + return this.user?.setPresence({ + activities: [ + { + type, + name, + url, + }, + ], + }); + } +} diff --git a/discord-bot/src/models/bot.ts b/discord-bot/src/models/bot.ts new file mode 100644 index 0000000..09c84f7 --- /dev/null +++ b/discord-bot/src/models/bot.ts @@ -0,0 +1,86 @@ +import { + AutocompleteInteraction, + Client, + CommandInteraction, + Events, + Interaction, + RateLimitData, + RESTEvents, +} from "discord.js"; +import { createRequire } from "node:module"; + +import { CommandHandler } from "../events/index.js"; +import { Logger } from "../services/index.js"; + +const require = createRequire(import.meta.url); +let Config = require("../../config/config.json"); +let Logs = require("../../lang/logs.json"); + +export class Bot { + private ready = false; + + constructor( + private token: string, + private client: Client, + private commandHandler: CommandHandler + ) {} + + public async start(): Promise { + this.registerListeners(); + await this.login(this.token); + } + + private registerListeners(): void { + this.client.on(Events.ClientReady, () => this.onReady()); + this.client.on(Events.InteractionCreate, (intr: Interaction) => + this.onInteraction(intr) + ); + this.client.rest.on( + RESTEvents.RateLimited, + (rateLimitData: RateLimitData) => this.onRateLimit(rateLimitData) + ); + } + + private async login(token: string): Promise { + try { + await this.client.login(token); + } catch (error) { + Logger.error(Logs.error.clientLogin, error); + return; + } + } + + private async onReady(): Promise { + let userTag = this.client.user?.tag; + Logger.info(Logs.info.clientLogin.replaceAll("{USER_TAG}", userTag)); + + this.ready = true; + Logger.info(Logs.info.clientReady); + } + + private async onInteraction(intr: Interaction): Promise { + if (!this.ready) { + return; + } + + if ( + intr instanceof CommandInteraction || + intr instanceof AutocompleteInteraction + ) { + try { + await this.commandHandler.process(intr); + } catch (error) { + Logger.error(Logs.error.command, error); + } + } + } + + private async onRateLimit(rateLimitData: RateLimitData): Promise { + if ( + rateLimitData.timeToReset >= + Config.logging.rateLimit.minTimeout * 1000 + ) { + Logger.error(Logs.error.apiRateLimit, rateLimitData); + } + } +} diff --git a/discord-bot/src/models/enum-helpers/language.ts b/discord-bot/src/models/enum-helpers/language.ts new file mode 100644 index 0000000..37189ee --- /dev/null +++ b/discord-bot/src/models/enum-helpers/language.ts @@ -0,0 +1,123 @@ +import { Locale } from "discord.js"; + +interface LanguageData { + englishName: string; + nativeName: string; +} + +export class Language { + public static Default = Locale.EnglishUS; + public static Enabled: Locale[] = [Locale.EnglishUS, Locale.EnglishGB]; + + // See https://discord.com/developers/docs/reference#locales + public static Data: { + [key in Locale]: LanguageData; + } = { + bg: { englishName: "Bulgarian", nativeName: "Š±ŃŠŠ»Š³Š°Ń€ŃŠŗŠø" }, + cs: { englishName: "Czech", nativeName: "ČeÅ”tina" }, + da: { englishName: "Danish", nativeName: "Dansk" }, + de: { englishName: "German", nativeName: "Deutsch" }, + el: { englishName: "Greek", nativeName: "Ī•Ī»Ī»Ī·Ī½Ī¹ĪŗĪ¬" }, + "en-GB": { englishName: "English, UK", nativeName: "English, UK" }, + "en-US": { englishName: "English, US", nativeName: "English, US" }, + "es-ES": { englishName: "Spanish", nativeName: "EspaƱol" }, + fi: { englishName: "Finnish", nativeName: "Suomi" }, + fr: { englishName: "French", nativeName: "FranƧais" }, + hi: { englishName: "Hindi", nativeName: "ą¤¹ą¤æą¤Øą„ą¤¦ą„€" }, + hr: { englishName: "Croatian", nativeName: "Hrvatski" }, + hu: { englishName: "Hungarian", nativeName: "Magyar" }, + id: { englishName: "Indonesian", nativeName: "Bahasa Indonesia" }, + it: { englishName: "Italian", nativeName: "Italiano" }, + ja: { englishName: "Japanese", nativeName: "ę—„ęœ¬čŖž" }, + ko: { englishName: "Korean", nativeName: "ķ•œźµ­ģ–“" }, + lt: { englishName: "Lithuanian", nativeName: "LietuviÅ”kai" }, + nl: { englishName: "Dutch", nativeName: "Nederlands" }, + no: { englishName: "Norwegian", nativeName: "Norsk" }, + pl: { englishName: "Polish", nativeName: "Polski" }, + "pt-BR": { + englishName: "Portuguese, Brazilian", + nativeName: "PortuguĆŖs do Brasil", + }, + ro: { englishName: "Romanian, Romania", nativeName: "RomĆ¢nă" }, + ru: { englishName: "Russian", nativeName: "PуссŠŗŠøŠ¹" }, + "sv-SE": { englishName: "Swedish", nativeName: "Svenska" }, + th: { englishName: "Thai", nativeName: "ą¹„ąø—ąø¢" }, + tr: { englishName: "Turkish", nativeName: "TĆ¼rkƧe" }, + uk: { englishName: "Ukrainian", nativeName: "Š£ŠŗрŠ°Ń—Š½ŃŃŒŠŗŠ°" }, + vi: { englishName: "Vietnamese", nativeName: "Tiįŗæng Viį»‡t" }, + "zh-CN": { englishName: "Chinese, China", nativeName: "äø­ę–‡" }, + "zh-TW": { englishName: "Chinese, Taiwan", nativeName: "ē¹é«”äø­ę–‡" }, + }; + + public static find(input: string, enabled: boolean): Locale { + return this.findMultiple(input, enabled, 1)[0]; + } + + public static findMultiple( + input: string, + enabled: boolean, + limit: number = Number.MAX_VALUE + ): Locale[] { + let langCodes = enabled ? this.Enabled : Object.values(Locale).sort(); + let search = input.toLowerCase(); + let found = new Set(); + // Exact match + if (found.size < limit) + langCodes + .filter((langCode) => langCode.toLowerCase() === search) + .forEach((langCode) => found.add(langCode)); + if (found.size < limit) + langCodes + .filter( + (langCode) => this.Data[langCode].nativeName.toLowerCase() === search + ) + .forEach((langCode) => found.add(langCode)); + if (found.size < limit) + langCodes + .filter( + (langCode) => this.Data[langCode].nativeName.toLowerCase() === search + ) + .forEach((langCode) => found.add(langCode)); + if (found.size < limit) + langCodes + .filter( + (langCode) => this.Data[langCode].englishName.toLowerCase() === search + ) + .forEach((langCode) => found.add(langCode)); + // Starts with search term + if (found.size < limit) + langCodes + .filter((langCode) => langCode.toLowerCase().startsWith(search)) + .forEach((langCode) => found.add(langCode)); + if (found.size < limit) + langCodes + .filter((langCode) => + this.Data[langCode].nativeName.toLowerCase().startsWith(search) + ) + .forEach((langCode) => found.add(langCode)); + if (found.size < limit) + langCodes + .filter((langCode) => + this.Data[langCode].englishName.toLowerCase().startsWith(search) + ) + .forEach((langCode) => found.add(langCode)); + // Includes search term + if (found.size < limit) + langCodes + .filter((langCode) => langCode.toLowerCase().startsWith(search)) + .forEach((langCode) => found.add(langCode)); + if (found.size < limit) + langCodes + .filter((langCode) => + this.Data[langCode].nativeName.toLowerCase().startsWith(search) + ) + .forEach((langCode) => found.add(langCode)); + if (found.size < limit) + langCodes + .filter((langCode) => + this.Data[langCode].englishName.toLowerCase().startsWith(search) + ) + .forEach((langCode) => found.add(langCode)); + return [...found]; + } +} diff --git a/discord-bot/src/models/internal-models.ts b/discord-bot/src/models/internal-models.ts new file mode 100644 index 0000000..f5801ee --- /dev/null +++ b/discord-bot/src/models/internal-models.ts @@ -0,0 +1,37 @@ +import { Locale } from "discord.js"; + +// This class is used to store and pass data along in events +export class EventData { + // TODO: Add any data you want to store + constructor( + // Event language + public lang: Locale, + // Guild language + public langGuild: Locale + ) {} +} + +// This class is used to store AWS profile credentials +export class AWSProfile { + constructor( + // AWS profile name + public profileName: string, + // AWS access key ID + public accessKeyId: string, + // AWS secret access key + public secretAccessKey: string, + // Palworld server region + public region: string + ) {} +} + +// Each server is deployed to a different AWS profile, +// hence the need to store the account info +export class PalworldServer { + constructor( + // Server name, used to identify the server in the bot + public name: string, + // Server account info + public awsAccount: AWSProfile + ) {} +} diff --git a/discord-bot/src/services/aws-service.ts b/discord-bot/src/services/aws-service.ts new file mode 100644 index 0000000..2050bec --- /dev/null +++ b/discord-bot/src/services/aws-service.ts @@ -0,0 +1,46 @@ +import AWS from "aws-sdk"; +import { AWSProfile } from "../models/internal-models"; + +export class AWSService { + public async startServer(profile: AWSProfile): Promise { + this.setProfile(profile); + this.setServiceDesiredCount(1); + } + + public async stopServer(profile: AWSProfile): Promise { + this.setProfile(profile); + this.setServiceDesiredCount(0); + } + + private setServiceDesiredCount(desiredCount: number): void { + const ecs = new AWS.ECS(); + + const params = { + cluster: process.env.CLUSTER_NAME, + service: process.env.SERVICE_NAME, + desiredCount: desiredCount, + }; + + ecs.updateService(params, (err, data) => { + if (err) { + console.log(`Error updating service: ${err}`); + } else { + console.log(`Service updated: ${data}`); + } + }); + } + + private setProfile(profile: AWSProfile) { + const credentials = new AWS.Credentials({ + accessKeyId: profile.accessKeyId, + secretAccessKey: profile.secretAccessKey, + }); + + const config = new AWS.Config({ + credentials: credentials, + region: profile.region, + }); + + AWS.config.update(config); + } +} diff --git a/discord-bot/src/services/command-registration-service.ts b/discord-bot/src/services/command-registration-service.ts new file mode 100644 index 0000000..e3650c3 --- /dev/null +++ b/discord-bot/src/services/command-registration-service.ts @@ -0,0 +1,180 @@ +import { REST } from "@discordjs/rest"; +import { + APIApplicationCommand, + RESTGetAPIApplicationCommandsResult, + RESTPatchAPIApplicationCommandJSONBody, + RESTPostAPIApplicationCommandsJSONBody, + Routes, +} from "discord.js"; +import { createRequire } from "node:module"; + +import { Logger } from "./logger.js"; + +const require = createRequire(import.meta.url); +let Logs = require("../../lang/logs.json"); + +export class CommandRegistrationService { + constructor(private rest: REST) {} + private clientId = process.env.CLIENT_ID; + + public async process( + localCmds: RESTPostAPIApplicationCommandsJSONBody[], + args: string[] + ): Promise { + let remoteCmds = (await this.rest.get( + Routes.applicationCommands(this.clientId) + )) as RESTGetAPIApplicationCommandsResult; + + let localCmdsOnRemote = localCmds.filter((localCmd) => + remoteCmds.some((remoteCmd) => remoteCmd.name === localCmd.name) + ); + let localCmdsOnly = localCmds.filter( + (localCmd) => + !remoteCmds.some((remoteCmd) => remoteCmd.name === localCmd.name) + ); + let remoteCmdsOnly = remoteCmds.filter( + (remoteCmd) => + !localCmds.some((localCmd) => localCmd.name === remoteCmd.name) + ); + + switch (args[3]) { + case "view": { + Logger.info( + Logs.info.commandActionView + .replaceAll( + "{LOCAL_AND_REMOTE_LIST}", + this.formatCommandList(localCmdsOnRemote) + ) + .replaceAll( + "{LOCAL_ONLY_LIST}", + this.formatCommandList(localCmdsOnly) + ) + .replaceAll( + "{REMOTE_ONLY_LIST}", + this.formatCommandList(remoteCmdsOnly) + ) + ); + return; + } + case "register": { + if (localCmdsOnly.length > 0) { + Logger.info( + Logs.info.commandActionCreating.replaceAll( + "{COMMAND_LIST}", + this.formatCommandList(localCmdsOnly) + ) + ); + for (let localCmd of localCmdsOnly) { + await this.rest.post(Routes.applicationCommands(this.clientId), { + body: localCmd, + }); + } + Logger.info(Logs.info.commandActionCreated); + } + + if (localCmdsOnRemote.length > 0) { + Logger.info( + Logs.info.commandActionUpdating.replaceAll( + "{COMMAND_LIST}", + this.formatCommandList(localCmdsOnRemote) + ) + ); + for (let localCmd of localCmdsOnRemote) { + await this.rest.post(Routes.applicationCommands(this.clientId), { + body: localCmd, + }); + } + Logger.info(Logs.info.commandActionUpdated); + } + + return; + } + case "rename": { + let oldName = args[4]; + let newName = args[5]; + if (!(oldName && newName)) { + Logger.error(Logs.error.commandActionRenameMissingArg); + return; + } + + let remoteCmd = remoteCmds.find( + (remoteCmd) => remoteCmd.name == oldName + ); + if (!remoteCmd) { + Logger.error( + Logs.error.commandActionNotFound.replaceAll( + "{COMMAND_NAME}", + oldName + ) + ); + return; + } + + Logger.info( + Logs.info.commandActionRenaming + .replaceAll("{OLD_COMMAND_NAME}", remoteCmd.name) + .replaceAll("{NEW_COMMAND_NAME}", newName) + ); + let body: RESTPatchAPIApplicationCommandJSONBody = { + name: newName, + }; + await this.rest.patch( + Routes.applicationCommand(this.clientId, remoteCmd.id), + { + body, + } + ); + Logger.info(Logs.info.commandActionRenamed); + return; + } + case "delete": { + let name = args[4]; + if (!name) { + Logger.error(Logs.error.commandActionDeleteMissingArg); + return; + } + + let remoteCmd = remoteCmds.find((remoteCmd) => remoteCmd.name == name); + if (!remoteCmd) { + Logger.error( + Logs.error.commandActionNotFound.replaceAll("{COMMAND_NAME}", name) + ); + return; + } + + Logger.info( + Logs.info.commandActionDeleting.replaceAll( + "{COMMAND_NAME}", + remoteCmd.name + ) + ); + await this.rest.delete( + Routes.applicationCommand(this.clientId, remoteCmd.id) + ); + Logger.info(Logs.info.commandActionDeleted); + return; + } + case "clear": { + Logger.info( + Logs.info.commandActionClearing.replaceAll( + "{COMMAND_LIST}", + this.formatCommandList(remoteCmds) + ) + ); + await this.rest.put(Routes.applicationCommands(this.clientId), { + body: [], + }); + Logger.info(Logs.info.commandActionCleared); + return; + } + } + } + + private formatCommandList( + cmds: RESTPostAPIApplicationCommandsJSONBody[] | APIApplicationCommand[] + ): string { + return cmds.length > 0 + ? cmds.map((cmd: { name: string }) => `'${cmd.name}'`).join(", ") + : "N/A"; + } +} diff --git a/discord-bot/src/services/event-data-service.ts b/discord-bot/src/services/event-data-service.ts new file mode 100644 index 0000000..ee7ba23 --- /dev/null +++ b/discord-bot/src/services/event-data-service.ts @@ -0,0 +1,42 @@ +import { + Channel, + CommandInteractionOptionResolver, + Guild, + PartialDMChannel, + User, +} from "discord.js"; + +import { Language } from "../models/enum-helpers/language.js"; +import { EventData } from "../models/internal-models.js"; + +export class EventDataService { + public async create( + options: { + user?: User; + channel?: Channel | PartialDMChannel; + guild?: Guild; + args?: Omit< + CommandInteractionOptionResolver, + "getMessage" | "getFocused" + >; + } = {} + ): Promise { + // TODO: Retrieve any data you want to pass along in events + + // Event language + let lang = + options.guild?.preferredLocale && + Language.Enabled.includes(options.guild.preferredLocale) + ? options.guild.preferredLocale + : Language.Default; + + // Guild language + let langGuild = + options.guild?.preferredLocale && + Language.Enabled.includes(options.guild.preferredLocale) + ? options.guild.preferredLocale + : Language.Default; + + return new EventData(lang, langGuild); + } +} diff --git a/discord-bot/src/services/index.ts b/discord-bot/src/services/index.ts new file mode 100644 index 0000000..59e6041 --- /dev/null +++ b/discord-bot/src/services/index.ts @@ -0,0 +1,5 @@ +export { AWSService } from "./aws-service.js"; +export { CommandRegistrationService } from "./command-registration-service.js"; +export { EventDataService } from "./event-data-service.js"; +export { Lang } from "./lang.js"; +export { Logger } from "./logger.js"; diff --git a/discord-bot/src/services/lang.ts b/discord-bot/src/services/lang.ts new file mode 100644 index 0000000..c900d1e --- /dev/null +++ b/discord-bot/src/services/lang.ts @@ -0,0 +1,91 @@ +import { + EmbedBuilder, + Locale, + LocalizationMap, + resolveColor, +} from "discord.js"; +import { Linguini, TypeMapper, TypeMappers, Utils } from "linguini"; +import path, { dirname } from "node:path"; +import { fileURLToPath } from "node:url"; + +import { Language } from "../models/enum-helpers/language.js"; + +export class Lang { + private static linguini = new Linguini( + path.resolve(dirname(fileURLToPath(import.meta.url)), "../../lang"), + "lang" + ); + + public static getEmbed( + location: string, + langCode: Locale, + variables?: { [name: string]: string } + ): EmbedBuilder { + return ( + this.linguini.get(location, langCode, this.embedTm, variables) ?? + this.linguini.get(location, Language.Default, this.embedTm, variables) + ); + } + + public static getRegex(location: string, langCode: Locale): RegExp { + return ( + this.linguini.get(location, langCode, TypeMappers.RegExp) ?? + this.linguini.get(location, Language.Default, TypeMappers.RegExp) + ); + } + + public static getRef( + location: string, + langCode: Locale, + variables?: { [name: string]: string } + ): string { + return ( + this.linguini.getRef(location, langCode, variables) ?? + this.linguini.getRef(location, Language.Default, variables) + ); + } + + public static getRefLocalizationMap( + location: string, + variables?: { [name: string]: string } + ): LocalizationMap { + let obj = {}; + for (let langCode of Language.Enabled) { + obj[langCode] = this.getRef(location, langCode, variables); + } + return obj; + } + + public static getCom( + location: string, + variables?: { [name: string]: string } + ): string { + return this.linguini.getCom(location, variables); + } + + private static embedTm: TypeMapper = (jsonValue: any) => { + return new EmbedBuilder({ + author: jsonValue.author, + title: Utils.join(jsonValue.title, "\n"), + url: jsonValue.url, + thumbnail: { + url: jsonValue.thumbnail, + }, + description: Utils.join(jsonValue.description, "\n"), + fields: jsonValue.fields?.map((field) => ({ + name: Utils.join(field.name, "\n"), + value: Utils.join(field.value, "\n"), + inline: field.inline ? field.inline : false, + })), + image: { + url: jsonValue.image, + }, + footer: { + text: Utils.join(jsonValue.footer?.text, "\n"), + iconURL: jsonValue.footer?.icon, + }, + timestamp: jsonValue.timestamp ? Date.now() : undefined, + color: resolveColor(jsonValue.color ?? Lang.getCom("colors.default")), + }); + }; +} diff --git a/discord-bot/src/services/logger.ts b/discord-bot/src/services/logger.ts new file mode 100644 index 0000000..690db48 --- /dev/null +++ b/discord-bot/src/services/logger.ts @@ -0,0 +1,92 @@ +import { DiscordAPIError } from "discord.js"; +import { Response } from "node-fetch"; +import { createRequire } from "node:module"; +import pino from "pino"; + +const require = createRequire(import.meta.url); +let Config = require("../../config/config.json"); + +let logger = pino( + { + formatters: { + level: (label) => { + return { level: label }; + }, + }, + }, + Config.logging.pretty + ? pino.transport({ + target: "pino-pretty", + options: { + colorize: true, + ignore: "pid,hostname", + translateTime: "yyyy-mm-dd HH:MM:ss.l", + }, + }) + : undefined +); + +export class Logger { + private static shardId: number; + + public static info(message: string, obj?: any): void { + obj ? logger.info(obj, message) : logger.info(message); + } + + public static warn(message: string, obj?: any): void { + obj ? logger.warn(obj, message) : logger.warn(message); + } + + public static async error(message: string, obj?: any): Promise { + // Log just a message if no error object + if (!obj) { + logger.error(message); + return; + } + + // Otherwise log details about the error + if (typeof obj === "string") { + logger + .child({ + message: obj, + }) + .error(message); + } else if (obj instanceof Response) { + let resText: string = ""; + try { + resText = await obj.text(); + } catch { + // Ignore + } + logger + .child({ + path: obj.url, + statusCode: obj.status, + statusName: obj.statusText, + headers: obj.headers.raw(), + body: resText, + }) + .error(message); + } else if (obj instanceof DiscordAPIError) { + logger + .child({ + message: obj.message, + code: obj.code, + statusCode: obj.status, + method: obj.method, + url: obj.url, + stack: obj.stack, + }) + .error(message); + } else { + logger.error(obj, message); + } + } + + public static setShardId(shardId: number): void { + if (this.shardId !== shardId) { + this.shardId = shardId; + logger = logger.child({ shardId }); + } + } +} diff --git a/discord-bot/src/services/multi-servers-service.ts b/discord-bot/src/services/multi-servers-service.ts new file mode 100644 index 0000000..26a638b --- /dev/null +++ b/discord-bot/src/services/multi-servers-service.ts @@ -0,0 +1,37 @@ +import { AWSProfile, PalworldServer } from "../models/internal-models"; +import { AWSService } from "./aws-service"; + +export class MultiServersService { + private servers: PalworldServer[]; + + constructor(private readonly awsService: AWSService) { + this.servers = readServerConfigs(); + } + + public async startServerByName(name: string): Promise { + const awsAccount = this.awsAccountByName(name); + await this.awsService.startServer(awsAccount); + } + + public async stopServerByName(name: string): Promise { + const awsAccount = this.awsAccountByName(name); + await this.awsService.stopServer(awsAccount); + } + + private awsAccountByName(name: string): AWSProfile { + const awsAccount = this.servers.find( + (server) => server.name === name + )?.awsAccount; + + if (!awsAccount) throw Error(`Server ${name} not configured correctly`); + return awsAccount; + } +} + +export function readServerConfigs(): PalworldServer[] { + return JSON.parse(process.env.SERVERS_CONFIG ?? "[]"); +} + +export function getServerNames(): string[] { + return readServerConfigs().map((server) => server.name); +} diff --git a/discord-bot/src/start-bot.ts b/discord-bot/src/start-bot.ts new file mode 100644 index 0000000..f638288 --- /dev/null +++ b/discord-bot/src/start-bot.ts @@ -0,0 +1,95 @@ +import * as dotenv from "dotenv"; +import path, { dirname } from "node:path"; +import { fileURLToPath } from "node:url"; +import { REST } from "@discordjs/rest"; +import { Options, Partials } from "discord.js"; +import { createRequire } from "node:module"; + +dotenv.config({ + path: path.resolve(dirname(fileURLToPath(import.meta.url)), "../.env"), +}); + +import { + StartServerCommand, + StopServerCommand, +} from "./commands/chat/index.js"; +import { Args, ChatCommandMetadata, Command } from "./commands/index.js"; +import { CommandHandler } from "./events/index.js"; +import { CustomClient } from "./extensions/custom-client.js"; +import { Bot } from "./models/bot.js"; +import { + AWSService, + CommandRegistrationService, + EventDataService, + Logger, +} from "./services/index.js"; +import { + MultiServersService, + getServerNames, +} from "./services/multi-servers-service.js"; + +const require = createRequire(import.meta.url); +let Config = require("../config/config.json"); +let Logs = require("../lang/logs.json"); + +async function start(): Promise { + // Services + let eventDataService = new EventDataService(); + let awsService = new AWSService(); + let multiServersService = new MultiServersService(awsService); + + // Client + let client = new CustomClient({ + intents: Config.client.intents, + partials: (Config.client.partials as string[]).map( + (partial) => Partials[partial] + ), + makeCache: Options.cacheWithLimits({ + // Keep default caching behavior + ...Options.DefaultMakeCacheSettings, + // Override specific options from config + ...Config.client.caches, + }), + }); + + // Commands + let commands: Command[] = [ + new StartServerCommand(multiServersService), + new StopServerCommand(multiServersService), + ]; + + let commandHandler = new CommandHandler(commands, eventDataService); + + // Bot + const clientToken = process.env.CLIENT_TOKEN; + let bot = new Bot(clientToken, client, commandHandler); + + // Register + if (process.argv[2] == "commands") { + try { + let rest = new REST({ version: "10" }).setToken(clientToken); + let commandRegistrationService = new CommandRegistrationService(rest); + let localCmds = [ + ...Object.values(ChatCommandMetadata(getServerNames())).sort((a, b) => + a.name > b.name ? 1 : -1 + ), + ]; + await commandRegistrationService.process(localCmds, process.argv); + } catch (error) { + Logger.error(Logs.error.commandAction, error); + } + // Wait for any final logs to be written. + await new Promise((resolve) => setTimeout(resolve, 1000)); + process.exit(); + } + + await bot.start(); +} + +process.on("unhandledRejection", (reason, _promise) => { + Logger.error(Logs.error.unhandledRejection, reason); +}); + +start().catch((error) => { + Logger.error(Logs.error.unspecified, error); +}); diff --git a/discord-bot/src/utils/client-utils.ts b/discord-bot/src/utils/client-utils.ts new file mode 100644 index 0000000..d4ccda8 --- /dev/null +++ b/discord-bot/src/utils/client-utils.ts @@ -0,0 +1,14 @@ +import { + ApplicationCommand, + Client, +} from "discord.js"; + +export class ClientUtils { + public static async findAppCommand( + client: Client, + name: string + ): Promise { + let commands = await client.application.commands.fetch(); + return commands.find((command) => command.name === name); + } +} diff --git a/discord-bot/src/utils/command-utils.ts b/discord-bot/src/utils/command-utils.ts new file mode 100644 index 0000000..4f1debc --- /dev/null +++ b/discord-bot/src/utils/command-utils.ts @@ -0,0 +1,63 @@ +import { + CommandInteraction, + MessageComponentInteraction, + ModalSubmitInteraction, +} from "discord.js"; + +import { InteractionUtils } from "./index.js"; +import { Command } from "../commands/index.js"; +import { EventData } from "../models/internal-models.js"; +import { Lang } from "../services/index.js"; + +export class CommandUtils { + public static findCommand( + commands: Command[], + commandParts: string[] + ): Command { + let found = [...commands]; + let closestMatch: Command; + for (let [index, commandPart] of commandParts.entries()) { + found = found.filter((command) => command.names[index] === commandPart); + if (found.length === 0) { + return closestMatch; + } + + if (found.length === 1) { + return found[0]; + } + + let exactMatch = found.find( + (command) => command.names.length === index + 1 + ); + if (exactMatch) { + closestMatch = exactMatch; + } + } + return closestMatch; + } + + public static async runChecks( + command: Command, + intr: + | CommandInteraction + | MessageComponentInteraction + | ModalSubmitInteraction, + data: EventData + ): Promise { + if (command.cooldown) { + let limited = command.cooldown.take(intr.user.id); + if (limited) { + await InteractionUtils.send( + intr, + Lang.getEmbed("validationEmbeds.cooldownHit", data.lang, { + AMOUNT: command.cooldown.amount.toLocaleString(data.lang), + INTERVAL: command.cooldown.interval.toString(), + }) + ); + return false; + } + } + + return true; + } +} diff --git a/discord-bot/src/utils/format-utils.ts b/discord-bot/src/utils/format-utils.ts new file mode 100644 index 0000000..4ff99c2 --- /dev/null +++ b/discord-bot/src/utils/format-utils.ts @@ -0,0 +1,33 @@ +import { ApplicationCommand, Guild, Locale } from "discord.js"; + +export class FormatUtils { + public static roleMention(guild: Guild, discordId: string): string { + if (discordId === "@here") { + return discordId; + } + + if (discordId === guild.id) { + return "@everyone"; + } + + return `<@&${discordId}>`; + } + + public static channelMention(discordId: string): string { + return `<#${discordId}>`; + } + + public static userMention(discordId: string): string { + return `<@!${discordId}>`; + } + + // TODO: Replace with ApplicationCommand#toString() once discord.js #8818 is merged + // https://github.com/discordjs/discord.js/pull/8818 + public static commandMention( + command: ApplicationCommand, + subParts: string[] = [] + ): string { + let name = [command.name, ...subParts].join(" "); + return ``; + } +} diff --git a/discord-bot/src/utils/index.ts b/discord-bot/src/utils/index.ts new file mode 100644 index 0000000..52dcf14 --- /dev/null +++ b/discord-bot/src/utils/index.ts @@ -0,0 +1,4 @@ +export { ClientUtils } from "./client-utils.js"; +export { CommandUtils } from "./command-utils.js"; +export { FormatUtils } from "./format-utils.js"; +export { InteractionUtils } from "./interaction-utils.js"; diff --git a/discord-bot/src/utils/interaction-utils.ts b/discord-bot/src/utils/interaction-utils.ts new file mode 100644 index 0000000..6a4a43a --- /dev/null +++ b/discord-bot/src/utils/interaction-utils.ts @@ -0,0 +1,185 @@ +import { + ApplicationCommandOptionChoiceData, + AutocompleteInteraction, + CommandInteraction, + DiscordAPIError, + RESTJSONErrorCodes as DiscordApiErrors, + EmbedBuilder, + InteractionReplyOptions, + InteractionResponse, + InteractionUpdateOptions, + Message, + MessageComponentInteraction, + ModalSubmitInteraction, + WebhookEditMessageOptions, +} from "discord.js"; + +const IGNORED_ERRORS = [ + DiscordApiErrors.UnknownMessage, + DiscordApiErrors.UnknownChannel, + DiscordApiErrors.UnknownGuild, + DiscordApiErrors.UnknownUser, + DiscordApiErrors.UnknownInteraction, + DiscordApiErrors.CannotSendMessagesToThisUser, // User blocked bot or DM disabled + DiscordApiErrors.ReactionWasBlocked, // User blocked bot or DM disabled + DiscordApiErrors.MaximumActiveThreads, +]; + +export class InteractionUtils { + public static async deferReply( + intr: + | CommandInteraction + | MessageComponentInteraction + | ModalSubmitInteraction, + hidden: boolean = false + ): Promise { + try { + return await intr.deferReply({ + ephemeral: hidden, + }); + } catch (error) { + if ( + error instanceof DiscordAPIError && + typeof error.code == "number" && + IGNORED_ERRORS.includes(error.code) + ) { + return; + } else { + throw error; + } + } + } + + public static async deferUpdate( + intr: MessageComponentInteraction | ModalSubmitInteraction + ): Promise { + try { + return await intr.deferUpdate(); + } catch (error) { + if ( + error instanceof DiscordAPIError && + typeof error.code == "number" && + IGNORED_ERRORS.includes(error.code) + ) { + return; + } else { + throw error; + } + } + } + + public static async send( + intr: + | CommandInteraction + | MessageComponentInteraction + | ModalSubmitInteraction, + content: string | EmbedBuilder | InteractionReplyOptions, + hidden: boolean = false + ): Promise { + try { + let options: InteractionReplyOptions = + typeof content === "string" + ? { content } + : content instanceof EmbedBuilder + ? { embeds: [content] } + : content; + if (intr.deferred || intr.replied) { + return await intr.followUp({ + ...options, + ephemeral: hidden, + }); + } else { + return await intr.reply({ + ...options, + ephemeral: hidden, + fetchReply: true, + }); + } + } catch (error) { + if ( + error instanceof DiscordAPIError && + typeof error.code == "number" && + IGNORED_ERRORS.includes(error.code) + ) { + return; + } else { + throw error; + } + } + } + + public static async respond( + intr: AutocompleteInteraction, + choices: ApplicationCommandOptionChoiceData[] = [] + ): Promise { + try { + return await intr.respond(choices); + } catch (error) { + if ( + error instanceof DiscordAPIError && + typeof error.code == "number" && + IGNORED_ERRORS.includes(error.code) + ) { + return; + } else { + throw error; + } + } + } + + public static async editReply( + intr: + | CommandInteraction + | MessageComponentInteraction + | ModalSubmitInteraction, + content: string | EmbedBuilder | WebhookEditMessageOptions + ): Promise { + try { + let options: WebhookEditMessageOptions = + typeof content === "string" + ? { content } + : content instanceof EmbedBuilder + ? { embeds: [content] } + : content; + return await intr.editReply(options); + } catch (error) { + if ( + error instanceof DiscordAPIError && + typeof error.code == "number" && + IGNORED_ERRORS.includes(error.code) + ) { + return; + } else { + throw error; + } + } + } + + public static async update( + intr: MessageComponentInteraction, + content: string | EmbedBuilder | InteractionUpdateOptions + ): Promise { + try { + let options: InteractionUpdateOptions = + typeof content === "string" + ? { content } + : content instanceof EmbedBuilder + ? { embeds: [content] } + : content; + return await intr.update({ + ...options, + fetchReply: true, + }); + } catch (error) { + if ( + error instanceof DiscordAPIError && + typeof error.code == "number" && + IGNORED_ERRORS.includes(error.code) + ) { + return; + } else { + throw error; + } + } + } +} diff --git a/discord-bot/tsconfig.json b/discord-bot/tsconfig.json new file mode 100644 index 0000000..2b40440 --- /dev/null +++ b/discord-bot/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "es2021", + "module": "es2022", + "lib": ["es2021"], + "declaration": true, + "sourceMap": true, + "outDir": "./dist", + "rootDir": "./src", + "strict": false, + "moduleResolution": "node", + "esModuleInterop": true, + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true + }, + "exclude": ["dist", "node_modules", "tests"] +} diff --git a/docs/diagrams/.gitkeep b/docs/diagrams/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/docs/diagrams/aws_architecture.drawio b/docs/diagrams/aws_architecture.drawio new file mode 100644 index 0000000..1f8d39f --- /dev/null +++ b/docs/diagrams/aws_architecture.drawio @@ -0,0 +1,147 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/diagrams/aws_architecture.drawio.png b/docs/diagrams/aws_architecture.drawio.png new file mode 100644 index 0000000000000000000000000000000000000000..dc1c8bb3c3e343c857e2f411a7fb486279421584 GIT binary patch literal 68539 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%0D@1O586OcWKB@zpVGPhPv=ac`6vu7bJK3 z1YRU*pnInI3Rrf|P8FEBlE8QO!fwMijMs-3!9nSQsp@OC5(z?g@Dn~p4Z@YrS3U3gIUn#`w3955amyk#sXkKLqpnol@7?z|?FQY~^q+S4CJED2qAfXM zwe2sh^z!>H?_Jlw*WDes^;zJ;dfE~vsq|wcachD?fP+=%u6BX?`VZhh?%r9M;yb@I>&V~O}7V%CW=)SQyC@iAw znOzTrxr}N@hPzlwi+hYD%Za3<0aa4&%ME3d*gqq_S6yTtTiIM<^J@iP8Lt{(%ARKm zB%ahNOXU=22RI~5|KXUCZ+QTi8@K?1xMBQ~gfOoQKG|w|gGH zIna~hGSj;kn0pa9jx0rYxOOt;|Hv~(=RAb<&U80)@5-qqE9=cHPwLh@se^6YHT2By zbN_65jO@ZVsyb(>kI6RipZeqN{pzvg?WkVCVZeimn;iJ08&O3nzr&LPo(?p|CEKP; z7z>9rn8`f0^@snkj11gO>K(`q{(4i5Wr6uk}=S#Oq+6BkM8P<2cuh-cI;oTYs@ zCzoECS@dXTL-+DDYh}S+(*hF|s8hqoWZKbyoDH0)EDJ5D9Uo+rt_wzsi;T%}&X z#A4E+*ersP&is+Ex-oX8)KMOQZU5)1BKN*Xe=yl>yKQV>O_p)7xb3VP;%oDe0zKE1 z#`R9;k&$+PU3t8wVZZWZ>`dkWBuh@g@duYgb}TE~umtI}zg{zE)<+udU4^qrIhi)B zZ{<6_R_YCItec*AJ(fduxSUn>vv{PSIn}-ElM-*p{oBKUyyF8QoBgAICXy>>)8D;+ zeD1ONTx;}iHnAqJZZl@frKjg`NzkG6*zZ5`!_Gxp()!6-8debi<1J?Ss`RM#iv-aW zdQ5Z-`n}@F6j)A5Gqf@C)HG<_xnto}e8B7sv1B9qkD}+|C(oA^#gp7^^L!YBQaoz= zXvZYallJ=YN5_G$spP2Mr2y$-RCO5-#hyntHlRZFd3nE;zhH&tgEwV{c?S!KFSmk^ zUC?)aub8;R5n7$`C+szc*uGIa@e$?H@t8j(=|7aVE&lD6H%eo?lcGT`KRWC)^7I!4 zfaaoq&V;lzwB#Q>q_lbRSw_NZw#2JOpRHK=Sj=$p(@;k zT>DxzXZf2nr`p@QPjF?EHY}SavMGK0**>906Wb=6E(iv3l)pc}o+Dbmd_f!k@7rA6&B(xuPIqsz0r2fu^UiOX`p zliT_G4L7+e4lN_U@BofNDB;N4m%f5>4v6AYki55|@ z_eupLYn@?@i>cQdOLi~$P5Y)IYvahdKRH~T6-nr#B1g*_7C=gqRTo$p_HowtNBRrt z^YXyVcZMPFKXls$NyWTwc5r#Kbx$QamS0?3K|YScLDo1VhRGBl~*W+ja=r_)9}#-O(VYVCNq?L7#5rjP+Y|Pp@FN{Yl0S zmyg5}_3~d3t^9vQ3gaysThR4%vaw!@Sy=q4vIyMejivUA(Unoq0;F;NUF7HfAI3s* z>yXwPS!$f8lcwg5JeGNJ;goDyte?SUr`$CeU7p%QBzC}ddL5g9hP1BN>C-*O--gVs!s@HsDs@ibGZa4-2S^@o|^+mQJloVjfJhCEnYRWp2Gh349r5pjI^A z2c{j~=1YCWg;_0)ZDHBmEEk-v)I0Jas`G6Cz5q8`-BkJma*7KOc9$&0T0P!fc;L=B z*blWV$PcZlk;F*O&6~BELVkYe^H2raR)le2N8Jq%r#MtIj|oa>d>;@*_QH#EqiVpv zEApfHOq*`=>HA#SoZ<=h+f?N|cyg~jc`y6X{`og&O^^P(ze1au<=F1TH1&v$j}LkJ zHa4)yDR@of1^>8by43Ev#}gP@TlApDNO^q`34=X_#~!E87E~Mb%`PRnBeC2wvvx%IQ`U-yL2uRDTTery^)7Po zA*n)sfz#lVg{KwHGHo@V&73U02$|>E&=i-YwVKK86&(E135}yo14Rqd;f;}jU7Cg8 zCV7T3=I~VHUcTh(r0tBHjJ37Y-O%RnCm5>Lmls8jNvs93+EKWM+vvi=-S6KmF^|-_ zSbG{@0^o2CjN~t!ELHmpZY`EaUQm-SI7>~mB!9?)$1XJpTo{ix6#}fjsK`+sbWD$X zz>3SAT@x)+^Yemd&Yb!32*jfsvi`+lD!z;p@UCJ|#lD+e_PF(@8t07BIa+sV0U%v> z>C@Dk!zT_EO*}EtCWaFw?a#2^{OLVNNt;bqi-%#o!5-y+xcC%UZNn1*Ok*%MDYbnOk}IRQp1Qent=?rla# z5I^rxO#Ga$)s?qfXF$^E?OCZK_Z;18EeCu;U+6j&z)uME!=(jRQl>tJuHq8n=mT z&ujmtr0>ubqi9tmVt+(9UT4e#W&^A=0QRhRoXj^!R61w%M+2Ke>8s z+vd!*nOV)PokGs?bimm_x65^~P4_W(+}i3M*gp}00aFTKY4asA00~S*g3tEjA*KPer)zZ1u`|!+#YsF zN-ylrV~DTJxpOh0w@OM%me+rf+&|qp`7}x>zIM(a=}?O0F7fCq$J5J8UmpSDJ2S1h zEFH8H?kZmC6Q30gpF8E(8dCo}l*3y1`Ik)(Imy~vtM>_7)n=zB#IIs5eU=&jHi6py zs98r>9 z>;$7i+Runo|8|rc9!xB9B0p#M78g}JE4mgpySf$8?qby_N6mW>jqdJQ+VVJqJ0sDR zm#`R#G2<2sozHsrcXh?dqdrnKGjO^q`i`Zpd|5>Wj-w?-&E4#`i}3!-@nW!#5^KF# z$g?cf#@@a}p)iSX|3?)gVBOOy$K>?dXfVUyj@Q)U-JIW_LWPcAkpLJb!2(+xG+TZI zk{lcOB0>=kI%mGx*x4!iXR^I%wy5^*2Ts7OY$+`&C*$;|qQbl3LQTK!z0E(JBI)=_ zEzBy2)5)}PATJ-HumT>t;&@)-0B9-)|5oUYH(fn3+T>>dk&#azzRedpv1Mc>=_r0z zcGhy<(kHQ8;A#2Mk4aCS2)T!;tE=B>Lw-~DAF|Z7eQ;#!6Kfw@t??ya*73=>l<)7B zK{ulAo42m0WD^-APQOQNr+#n?I$T@5uefPK=U51JvOIr@)&!4iALLioR7i3LM#F8c z>)G7rxy#bwQJ=Ny@+eYHMe%a+)?uYBwB8m5V(Hl08eH0E}mz8zz zoVq8N>>ceGC3JdeD&8m6e_9G!voi6}S!VvbPUD$o=f=iA^YUb$b*U$9xjY$X!PmYb z`tC4&oS){#3_nXn3BSGl`T5VF?ToYK+miidViCLkE?qUDA!%BfI^qv& zPwqYll)XBc9$dUJpJtKG2>5oqfs%LtrTLvAJ-FJh9flPX`l*za6tr;j{WkyUwO|2_ zn*~5I?OVyo$96I=Xik&99x#S2-UtW?eE1M09N_q(qy+RSPNJzWgErBH;Z!I+G1T8# zFU|PE4rq6CdmX&!lDqb(LeDUGV&bK)u z9aqLa2{)kK4 zuj_LMmsbymJh*jM`>ch^G4`fhc(=`?2%OmRM*N9&`*X%N1_FI0wi3@)d-_(Si*12( zPpos#{&*BMlbRKRvEydnn`nL5FWuR3#+&SosX3->{p(X)+8@2CkweDq4mDS`^LJ0i zeEyi9le%KL^BW2~KhPwjv}K%Z9m;<@Md#F}SnO@fZQ@a0Yd6B_=Eq|`zoaGF+2=<( zci>*);3WlX$V5&w@5Doe$={rSjL~NCS&@=4=o7DjsvXwJ)MQ+(NrpTl;lR0L(frbQ z5H|4oFN=KxwwH?#DG5HGwPg8z)|!%xlG4UD=pAWwwpTb{C_n82@b&U4@)26|{$?+? z&JP{Vwy)a_f95~Z%NdGC4(y5+q)s_>nQ}JOb&J?L*6iM!Z=KRjU&LhWh5^n)YQcp& zZ=F@r!_2m|Q(#kUiCs4~sB1-M?a(z#HUcSAzT(}UF<~8rj+Z3$!Uje1Fm9GNE)v3TRJcQIM|`W z-|@Ud<^6(7=7+?Zd;YGFeMn`ggs>F%$Xq|@VtwTCt>Jg@9AR<4ED2q2RUX5wVd~QN z(#{PjqT%wddX6=&PJBSeQ~kq$gZ_sxEJMd95?G*yprJsaLwB04@?CZrAo`m$<;MC^Odd>niJ%wdW_fPKvzekV5%(w zfS~<7u9)zlkJ%fOlP~+*zWM@-qc=Uz$b|LBD*XVcDKd0caB@y9>?RC(PQ_&F@J}+z zmbJatgcNnFBsjzvQh9z~d!N6#daIg$SoB3)-=xuDfo4pJR9Ba!u4RV zXP2<6m|oDh`&f_Xk-X zSW?mN=;^(E3*@DDhnEZ{_rq*-Cq(_zx@(n>Z(W(~H3Dfp>O9bcMV&+HMYR<)ydao6 z8czR_@9y8RymgcjKk&xGPkO=G>hjZ|^%3shTUTss^1#GFdv~w_3y(&b~i>(R0jD(Qon7>&PAz2+XZ0o6#vr_S)=VY zME6^rU`5)u?8GPE~HS1EtO-AnFT#+yRdq*_u%+h<~N|+{v$ESG86xr-|a;& z&frdYt1CyBs+j2O%BW5b$BH&F&J-PSo8d~6j5C`;KX)WCN&4{-%^kX1GZ*BY@9#>} z{o1(CP%nkfyScbUgt-(hSJ(XDF|EFN2vA!2;CM@?AnPlti;EA~*;@wR34QDXn~fSM zC&seQV7=j5T503}Ble+eoocrIUmX{r8u%lVZR_~PuV z$17H+yFI%OlJKhuk1NAf*lR=6K@*q~;}5^BIoQ9Ih8^U$9{l>zk~>hx<-Bg5T=3|( zRCB*};Z*3G+a|G3_NydD_-9dbEg{(hcQ4$hDxZkp2m`;XUpuhBlOwS4=A8R#@c3hQ zy851Q;R@l@gW>3fN+(~%*sG{;3$ONGC&bN@9N#^ak4;@lt7}}!YjlWHeu3C-)5~J_ zo;&+N7QiVqQCoGlv}2aOZ8TJ$Z}g{TDGV=~r6FX{@H{OyX5+n53g4Df`1eK;`{xS* zi!ybvF8DuoB8ZEK!8(7N73#M8K(!IO8&AD~j&j_|@Nro{3In z`MofsRSu~uz$TU0oFOQ?Sg5#i9V`R-V6R5a1&YJOj!jpI8DCN3N{PG`DXpQ(ZrTt) zOIJd>G>n<^SnIl)^L_JY4;-B=%crx}Z(t*uzdng8K~sQ;WGc0r~ooURi`2!_iXFLo}&-3I!U5FS#oo{+e#k zefs&%F6l|bvuzHZFYcX3d|Ja#*Icnf9aY+2x*YppaDK14C+lidF!GW4Aq>6rcNwER z{Fb@PXlIa&j?<&)VrPOh6Y=^Tzq2#mJh5qb0`uV+OZAHi&yKVYi@rUJ-+qW?ztyKS zk;9%h`BM8^2Lp00hPJ)?^b2pduDoAnc{Ex%>0}KuM{gX7JLFnXZt$qR^Q1OKL0tCt zFX4_})%s+U1+6 z5%rZDKwiqd#SbqLVmj}`1vP})$jW2F7lOt6o`3w%JXF}2wp&t5-c9)bT*6O_H~WrMGkO%Dn02 zkMtwX=8dF3=iPZ!=b<^Z7~PGu)z*p0I_-&Auj@W_!*<+Y+rGuL?-*f{>-`PO^- zE|H_!*ZA=3?kbNB9P7C8J9gGL-eop&ZV$gFVorZ`9ewx|CjK*0(e6mVFP>*Nb7iJE zUyh5hG(SI7LH43e&KbL`GfQmO&w+c+-^g)d2OyL#)HR|;w8U%wJXmlcMg4mE17&DQ zgs0xW|GB*N`-25t{!gERYU>NpxJq21AkV=7;b*1V%obf*URS`}^!n1$dQjw0bxdw9 zSuScwe{;UI>g%~4+D^DYy*?Lki|Nh*i+b)hg3nK{;AKPX+9a~quYub6jT0%YGsUPo zUhg%&T#Yv3Sy`BZ9MDUkzAxpicE=SCiT&jHmVB@huDBP5?8dKTt=#o{+$*V01SOET zvfE-#9`;&TOTY2>eXWz9R1xOW{MB;m6c#;q!wv?!&shrZ6dVcvL)GQQyj$J^L|J|ufMuLXI-SStTOP+xMDwK% zXzm~Ot5SNyd#XP2@^A+r*L}_UX|!bh-6S{D^Io3V(#V8>8w*R!`KZaY^zH%GH)Sxr zU+kO#T5_h6VytDj*5dv2SF`Fd)rdSJ1wKXi$W^I9-sfB{Apja^&&k5oBA@ zN2i@FP`9dWsr7i&Ph&E5*22#|cVMXGX(Co4Uz6%ySGx3vwQDWE_()0bba>{|RPXpT z(G)Y7f%BV}zv8+$F4gCmz(Zfm?KKS(-CUq%W0#6$3n5#FrP3wp!+&29DUCd!!SLE1 zM!*%1dVkofi?_KCz97Gs2k@S15AP9y+px4XNW8)v9MK$MJGc&JTg#vx%R8&NGa5IT$X^Y5Ug-4&jV3s3F6}rncF(pthJdufNEsz>kuT;F0uduM5o(;K&-QmTwg!cUCdl7q;pC7ge zIgA;IYT_(}K-riUe`tI*?`ET~b1=hiv zu}zj!pUY{lhh?8hZg~-=phqETf_co=eR+;@%N6BclT+3fI(oWum575b{m{I*x>i4>cZzlfXGp7Qzd7jXltQ2&2<9)5OAx!>^Jm7kYkc1yJc$2P8# z$={dR+J@SWM??g_zPeS^R~91tJnv#mk`*;^`Z}J&!x1^JZ4Mnb_e?Qg`8M! zZu;b0#WgwjKA}!duKifms*m?-x((NCIB(<4z5K5nl8$~$)23zp%*3(qNY{gW5jh1y zh8teVHeZuwP#a_Q%j(l*TuAP2Ug{||^7e@A#D&xh)0*6}lc|~{1nVC(=Cxs+mrf5^ z4lF+~aDjHXv{{aAP{{d?qQOI{abGGuIu1A$% zhA%m>4hh{PuS-KRT_IJt1W*Ry5@5>$cnz$?!1vMEqV!C}<7QdH!$y8BrU& zrNWpw=F(7vS2beA0)AYb3w35DJo+ZbK+TY_W^iF+tW^tZLvt&+BkEIUz0i*eQvD3% zC1rY4o}RQ8HP)y?7kS7%Tl7Ol32YNO^>kUu#;L7Z`Nl~X0^s_6wDPR1M4j*%CmaWF z^-wtV7?|+5C9x{n%AA z{NmB_v99?IFWEJUoKo1D(A6PAo}&GDPYLh7zpUS2+KHsT%1g4;){~o|9VgKT+^^LG zRhSR?<$>o8Y==s;&X*P4hQ7~U->(w{_+O~z84eQ=t?}m^RUJR?)k{0gnkRj0L&@v- zQgd)-g!h(d@LA?CW<6UOUCxKwcmvbzn+kMOj?X;4-Ycaj+Bh5v7 zff^tGl_1gAG#l^!jn}snl}e7exIH6t@dDI3&HF-j!KXquAxhhoBMVQG2@Ys30FTg| zfrH<9r|AJ>1&`g$R;BaX%M#yr)=0Z2A8-!7jpl#YBeS_;JH}PqkrrYtlA>5N;oUbN zkbx;`cKOxY{5I#ucf#CVapyJt4;n+{K<=+4AT568$GRLR2h7{vF)DvOt;&9J8i z@$skPewt;zD;bzuu7)`bo{9LgBSVGbbN5Zf{}+2-6&2Ugb&E98xRVe(xI4k!U4pwi z1c%`6E{y~TPH=aJV8IhKxCM8&yK~O>e-C$z`*7d?*Y4U~)w5PDtF`9-3Ee5Q6>O{X z>viX9?BJGyG%_~UJUn!+R0X!#@se?~Y3gLh!F@QM52MWpzqzwEOwsL0#dZq>Pc5?c zf)<1hgEz^fV6;=bRK!Tn^mY>MxxZ)dxJak8$rbc@>1B_O;7E;0*i(`3qp#PL1!bT(#kfMa#d%jdh)mwrX-sr`9>vYe`4sH`u^ioJ8~ zBd~=sKold*@V+1b?DsM}oe1JwL zJv(rSAX4h2gq62asISB|`cmJbU7c`s$IXa6tlETQl&>jL317Uxq-D@Dvw3@TWM{Wh zk6#vRC>BkHx_*2RQLdYoZ?5pFhlveVF70S@JN)X<`CHA6g2lRXn_m$m80@A)Hxu^~ zrR}?HdNpkPJ=!Rc=e7b7rjmU+ZS{cz>UfPVyu2j8*5W~6uuK;OMxo1W`6|kZ-=v%~ z^JT2IrG^;>|E7F`a9V!V{CN7Mvd*(Li#dx*GE#W!WJim?D{J6_zE`22%is-5E5Qa! zvA>f9H54Q7EDQn~VGnSpMG6i6wY9$=$>Xw{XDaoJNo@)1o>Pe}^-EUPyFVp)v!*b~ zxj)dUT=hZJztUqve7zju@e=`cxo?57nR9w2aoXOgj!mpjz|N956I@G=Su4SrK#@?! zcKaUwL-T6J;OKPPsP7UoE(ikoS+o$~n)_0w=fFHByw&kyCDY?ENwzDuf?;aadJ*mM zZ5nAuXgM_DUZD)I+F~nSg=@$KQIc{5l1Z02xm$nz$!7*hR+2uMUGVrJH+^W4dX1 zZsW2#CC+dN2pdnW^`%L&j zv`ROY&zN#z5NVE?2<-d9<5H4`HbMs{Tfoly0B!bTrl3=*1G~pfcE*j(&7gsS-|hwm z22O5nzm`=%{rn^9?m#+xC=?mr^BYIm_?Fi)rmCYjy3O-PbPRmJwgz29x zvQ1*C4AfmW3GPh)L`Br~f@JW`pGzH&V}EqI=+2P-2}8XDmY3|dQ04o*#e%5c`A`$TF6i=@ z%-9q9ct=Hm-4=j*sT!kRm=CD&;GcW`_5p3Ig zLTTuL%AQL>cD|Q2+*0L(77T^$KzsQH4+PPBJmsnir-VUm?^_uoLWGir+np z{mH|_qZ6MjP$px4==`c|6yM4yga7iQnWZ(jWS{XD8>&0Ae@LfyPe@gT$~(u7Yir!5 z)%OQ7NO2Bm=4u^(9QF5i9Q^*ACkl;8O?}$S)zZKdnMj>g0ZOhPMy(c>-o-9?29D;t z+PTos%oHG-6ml{0EF%Hwp}V8{$(m1Fl7+?JiqkPu@%b?YuyCj@(9dP47#N^ip(R zAE6_;FC`U6{pa4{!a@EVBZ#3`2@^DGazk3U4#J$p=E&_hNvzZ}BrWAqMM)6`ly$8W zba$UafA7f6OBtztx>5v9s=;HQp1{acRL;!KZmh3EEy1a^IgUby4xy(!jkXVgJXx)r zOanv($r&BE2hdaP1o%+Gl;XiCida=AQg^%Ap_gGP_K|+dT5BYKlLFJ5z9{l|FVQi! zJjhl%#W3Pl;LkKUxuH`WbITPe1&ky-70bQ+})OQiIl zCSZBhE*|J12%(|00S`(#x`RZ2*u$RayK436(N^1gu_OHvej^(F+zCL*~Zc8i^9~{9x{zvq^^miSb%VunDOoh9mbF|A|b6J9HEx zHu!*;{TrgtI}Nz)DEh$> zBgMyjiPNRj>nf7JdIG$ix?6}>o-Q5m<<(joR)1uqU0;kb{+h-C%O`aUpoT*%Yev-- zNMu*o@yDdPqXS_P2$%vwcNZFu$Zjfp%$yGC=bVfa<1-cbl?y(h@ne+g3F1Z;WQpQH z2Nj#rN=WOt(g#c0)3R+AILN6lxcs@N#?w@kAg5hDi&rPD`JY;V%1D2KHLl4mbmwp(D=3cI}kGBhO$K&yHU9p6(i_3D*2hL?N zXxD+DmE!iVU~?|O${nBM!Zn9_e`G(f`dp@a8*9*IJk8IE-E7$HJEg z(ul<(5|fMHpYcHCjBJSLUB6zm)Ate&kTYlBMItHv^hK2S&f+#v)yj1d^9{)7)o*u~ z(9zB-&~sdO7SoYdzJo(;;! zbGUA1kt#0*m9YT)VSoBfb+!Bk{|2;-0>eEa#m@S;YOS8xzcJoXvVcEdyoo1X9oiZS z%xnGDWj^{<^d2gu8UxDGawQ9o=6JO6)1`%3(;sB@QdF~ISo3n2@buDNv&-T&mFYDS zAec_wHWGE$*td3yh?ZFTDgn9mlZqkanMsgqw$g=3UZE$>ydv-!<;|Pr!=0-8tlf0? zh17KGsU5K^nSH!`ekk>qu=Arc3Q$AYzu2iHzl26-cOQB~#XOr0ckz3DT;;WZM;DWS zl8u~d=DNMP6m_jxx87eYok%?d%e#8>k6+FL%lwfKzTU5P#hBv2CI7F8zOS26h?t#Ti(}WT0|k128D+s zB4Q)M!o%@LQ!k|&3Eb9?vOPQ~ zC|MT0j&QJi4xt2u;0V=`&1=uI0B7483IB_wsI7ukyU|jjTn;gSsuI|`m);i1pV@;>&1?w z_NRN(1VIf)&B*)=*nsIbFv)pd%i}7pU(@%69ytGY&xf87E%g@8-0kJD8VFf^n70AL zU=!#`9&zyNh^9-x{<5im5s{s8g=6R!ht5BhKwl;}bx8%--e9&{)YjV~N|#QWPB@tPB@>TyOU@*4;f9dBnIR@# z?t6GyFKd<#k|}zwCL>eBb>CB1+}HkA>06J}bkx8EN3ZmGs;RewmhU~X_H>!BY_Un2 zDzQ7wjtw$a26yYihYgka&HK=aa_Gg6)hgJ`jRzqX%uS;ANU5$C3i9&8!&~8=gclg8 zbRWfjedDpjm42JrvwihT7cpuQ^e{PvM3spBBG-L z>Rl$F{llz`BAY-|AcS3cL35(>lRpPmzct*H@f8*yd{_0e7qM+7Y_u2YnfC=t298i! zYG88Sbp%Q{X9#6?7HI{`^t5odx?)KBAWa2um}ss>M1F}E`ZoEHRgIDHYvo*F(Urke zZGUxZWBGHo$Rf#vDtz)xC(tH^R5p(8d3m{jnU~__K|n}1XTBTRJjojIF-SJ&!qXn9 z58ZRQ&sOsv9gHW#vj52eWkL$So1Hw<`{%${4P?*f;)NwRIJny936G~rS{jP-ak$I=8#9l zj^7mBj-yc}X(=q-=ga7l*g-soN<)vnN4zZOeu){;u=4VBINP-X3jYdPmw)_~DsAf@ zhZsgCOAelGrfK95Wkx}L_|dOSU6s8u65Z{v|H|ieO+k_#W%1S7Y@y*)zHw;^DPHaxxx?L z6qF?1->BzpUeZLP$XmsN71r{t^;{wOyEsf`COnW93h`c69z4wWQ5Mi`*K)$9q<{(#ftX&NuG^kk z=O9Iilr>yVSLwF{)offx%gy-XhBl#LfTNJGxl!ipn}7vLAml!Q-_((ZuS4KRh=^Uw zOSUA?sGR6jpgHcNw8uX>*$C7$@cLL;45ETGAgrp0cpmMbKQ302QBL^S5=Gf)I_P?% z^F^^jD(=hWR8kFU_4&CyUG%1cwf)yOvNzwdg5Nyy(#89a5!R(HuvrTebg6lrw--fk zF6#z>9Z8Uc)<1qTIzswKPm+VFT}qqwJ$tgl5}*8q&BPA z5@|5z0+Z28c@cAKmwsmozrP_4W~+_{%A>;(h1ONd*6sfLFMo1sFKXZM?Q*3FF2vEZdYp4` zBFSWw!Oh#=SBy+AveWOksvey$ccNuwBK9HM9zB zyUr?|WR3JB2#Yvl84#BuW7uZ(d48HIc6(Q$-;D>k)>K?^tvgHHar*28w#Mu&{bLCB zedw4VEpq65XkDU_kVllyS>=zWDzbO7TjnjO?oVQO(09gP_V2VDNzbc4K71L8OwsO> z_-kxG5e2eyZ}lE+f~iu6Fd-fGXp6FT8Fs>L1&|;a62O0sV|)tGEDNgr*Q<_ZfJp0O zr{YDI*WNl&lOcZEk3`Xm^3E@fD38Vd)~*gx*r2n|by;##acrk-h`*6dFZR*z@_Bpx zj@(Ei&yo1e{rgMM3th5^F9$s)OZP(%@6Z$Ov(>70Rx3(X+{{*m26gh1zFv%qhGuZ#GyHl)%PrgoX&ye7D z!5wu8=9XF+iSJ7!Bm|si;som23ixCO5nsJT;bcpbt$ST%=!6wi|amCFeYz?u5R1sTc%7mPux$aZO}x5%2~DgO?E`i zb#Sl9L$KXF=Zj+E;X;9_jhy_?qhGBLCqA@%3Z;}atK$j}ryyhFSTx(gZ45f-Sl9oV zSy(RsoTKPA(rK-~tWfbVn=VY*eS}Cjf0Y(}&j6tS()d^8SX$tUe$H+wsH>B~_E{wH7;2g&nYIT&K-)Z@ z6qq1S83!ZxQZ%u1K~v{ncQD!H)Nd1p6Zh_NQt15G)nbd&ITn@)xv|<;E;jm~V(#iJ zM}4R7Iq7hq8xIRU&sS-&Tz)3YQ_hp71Adk{2^BvU3t*sX!NtgRaVQJ<3jN=e2gKnN z4=O=tB1s;qJFIq$%oTDt{C?ADvm>D2b*JCjLN2En0E9IFsm>BJ4$VGN#H5$ZE!3r+ zzwtfp-#f-1vwQjK|Bz}KX}|&Y!oog)Mi1a}Vrd63a&V87GlL7=(fk{A(vA29iSnPhSQ;mfl4_;>C(_=vtxfG@3{o>Xwp zBE3aiL-{|TVC^Su=KuZb|8tSW4#HRUS||iO(3+0FO(br88nKK0+0o+A&|2>1aqWJ$ z=|21W9ln?j8DzKLDRqstRW_}#cxy$Yxe?B-~?74YwY ziYT=_7QsC#+YXx2VY+{pC=oESSZ_J~?IXR<<8kxdL3#C0A|hPeP@qt)?sdM}F#h%B z5goXB7>pNsL6njj68#hbT#0P?zdn*w8FWSg%^fw^qxejEh>nh9yd9Ow1K6oL*)a8f zI#GuH%L(NxJEITAJIwVem9mrt)#0O+dSx)H2eV(=QN~5;JBT<7n*GO#EP-FoTJ;Zku68w?d-Bunexj~K zN8>mI;=2x!HIlZfWeh{MP(L^o$$z8;4{*FGfy`hl{5^<4wi;i&#HKuW$@OS~64*Zc`pl+iNXNd*)ED5fW)w`K)-YL-?~FI$ z61>L{H6|~^eUHzDOCArDjpELbid-V+7+L<-oMpAXVwvN{aoc}9FCi_pn?Il&pr+Vt z%&9hdJPBrS7L6o-$7o>J2Tyb%SaQ2PO-KOpcJLUP?kRZFwc5)xv8+ zC!m);; zKi?W!TsI_U1uE#!{=`Iyg`?H}8C>rT{*{=ry=~_6a=)k5rp{T)>#`fuChs*MHVSBj zR~9JmhbvkG?|n**34k1Js@hNpcc0rX0!UMnv9TpdxYKk3l7@(iItcU}osZI2*F?lc zWTsT0`^mKEP_j&eK-M)f-oqs?!y96PMyTFEyYmtiDq1RhYv#j+BIRbrk}24C2vAT! z*ZcE(=s?w0=jU6(58sGs#D$`br|AfkrJ+a_GLnM9sb$tu)v-j^;9Z70T*FwBqPEjX zoq&Unuq;PgFDPb;Ih_%S!E2MS3^U1|qwjM^C4wOc@x=AeiQC9)eyo|lL?WRg@`XV1 z_pwhIvag#uaZD`JKKV>5eA3925ru+9e=0$Fad9`i#%kc8oiloYk@U5CqyiG83m1oP zhqDM&nHc21=zXcL{w%T1?`gjXJ~R4bNWNb3lg?h}QE+jg0T!tbe@k+6DOR`DDB^@@ z=2@i8ape73=B)O9ZxqlQSLVuT&hzgc;XpX7XF__RCEi;R%;OvS1im~!veN*S$593t z7#J*5SKkc%?n9vwp;0O6!tKJ)NhW&whK3N{OU4+f!~qQSp#iNaM>Vz$&1k`p$v zmh3+=GN$HjLzOF-cO2Q=#XO#0(cEDVSTHqx`ONt6No>XHu>-)K?Id%JihO zgkIrAS0)SA+W1pOxt{>%>ro-B@N%j1R!ZGe)Y`yd?GxJuauDBuPqqKk@8$pq*WAt^Y#n#<(;Gk-pVmEo;j> zgm)8(>O#zV@+%yd+P>uh+wGHu@z2N+(Ar;o1)FwY`lwkuSp64B`7Ey3z!51D-w=Sw zt=>wq-eSBV2;8ZJkq@FmKqvn3EB*?UI|DRgT{7g91eQOQb?mCMj8dbT>@<;^z%)%r zV#~Ys<`G_4ot3JqYbc$S!_qpFnc>bhF4He#&^54ffUWgRpq!zhJ zUU&ngy=$-vn5@$kv3na4{P7kk8vA_W>_Kw(Md38nw@2Un^2p*FX4FlKR$x+%Drg)V zRHxQRRM*aa+Nv0lR~{pxqYo9yrN2o~PQruOO3F6`M((B%%CIOvP2aR}IHkJ3Vqsz* zb2Ct4By>y89&JyRfT?tw9X=}6CzI=Q6vg)lyU^iR!X=H$%F2$iUq|Vbp~`RjDjkN? zz|4IWL#W)LmH2q)xI8$d>T*{7wro8XV$~N^)eBecy_7Th_5p54*HC2n{P()TB9(o+ zvrM$&>8gL8Us9=KY$V5EVAl_@i4|jbdPz6|Qz3QAPd^xJ7<3(7Ed1P_xo=Yq*@kJA zD$GIiS0rAo>S4P*sRqe&kg2O2$L=@5xVBtn&B%Cat@8UkOJg(eRGGOdMG;_Th_uq^ zj=4=V#7go}d|=>%&=dj{Ad$*~vP8>GCb=FunpA6>x?O_uHW|CSR5ORNY9gE)s%8E~ z;g*h92T|{uPih`RsuTnZA5xYqvc2Jw%k#R0qBtQ_>&bW!IpkS@7l@c;TF;a`H%QD@ zep~6bhb#(%*uFcH>1>^^wo5rpsL0z&eNypvZ~P5g z6D>8FZdyr%REaRZx~Ou@6AnM79PKDE(4M9qHovi&%67`^l8BrmkX8~ZqArzH+JJ-L z#86)bMRF)iLIC~Mi~~-bgIEO{DX=8Y(*wQ1f?v_)8;(aQ*PKKz+a=}|bu+K6)2acL zDX1@ZV0fRxvi~Uk@+^Azds|me?zj*!diU@`fAH=X{hP7AyO%urt+M$|KoucQ%{s%t z+^2PP80GWe$_egB@v?`4Qcg!H{l77jB+SI=0>oPgoPje z?89R7Sp9)X22_CQ#rB9-v8J`(Kx3m6T9)d7c-PM5&oZ_1aqhKJZpkSI%?sM`eSJ8W zXxa{)lpgU*Wbx%Hy$g|(&yLp?y8XbbKZ-<(XMo=={$!1oxpOGZQ4$)#Nfo-so^SCr zpfF&@mC@QBL*Y2t{X_mFKc!A`F%RVDZ*wK~CAu{jotM`1we)Qa1_6q%58KxS&c#Wz zpCOi{mZJ~yDrrreR2PH$nYN-S6Wud<{iU|Zj^n(X-PGIWGN;zj5!Mbu71kxkd1}97kbpF-T%F+NQYg%z2=qA*Gg_zCRn^y;r>MWBHWHJD zQ6Hl{d&Il+>wRNMXA)H@j z>JrJl2ESns4&uoCjhiIuHu6;fj$^21Ai$bnOl9E^4(z+2|9f-xW-77orxV;&U?fgr z-ledW4&gJz)B_eLO77inynF}2@?D|!Pd zicl2V{O>%Xd_Yv!Q};Jw96RaGUwZh@Bsd?dF^~TSlOZs?IP0yw$k2ZSurUo`p9#kH z*C?Aqmdf;<+{<5hbd9>{cLP%YU*pQ`Zh(BdG}4vbB~XoLI*!W z7ahO7)-U%lbXUtZDR14$f3gGH+b5p3ycgQ0>(=!+SVEQNRaWxD3%kmk;i}8z8^ZY+ zX9&1JzP@%xk27di%w1EE<4dJ7mSzp0{>e@$%11&!uEEmhhN|k0n_|LMjMG%JA*Cd440d?&F`Sxj<}N_0Sj?@Z`4S?zT0z zauguW8!mnc#a85^NZ}b98{1#zl72SWlGx(DmDYwb<1v1!yJ&r?UYz0@7-?Z9g%+nu zGOJN^tkVg?$G7CP%8=tzsZ#F2fJ3@;aggQEY@nn+_7>M| zn5)ocpWn19nq4VM?)`jy5DJHk7l77NAn|J~oq6bX%|G{Wz=@li`$vvIR;SW(CsxOB zulPQOz`c8;>UQYGlt|1~_l!1;D@y@LBa6#ENQ=f^o5V!Ukr5SUxyc8UdeXqm5?cE6 zg{`D$Vek9J(MgQFSL=lRgUuLJ|D%vy;$!~cAF0t6#*Zgrf98q=(kisV%U>-|`SS{C zGCvsf><|XL-=h%;BE=GyNU4A!$C9U^k->n*RR;CO8<7V`kOxHsB!R?(lHf#~ld?CQ zCw!{C_d6F`7gttSG8WY@iR0T>PQINO&i@$q|MvOej<$ekJ34E9!_qVFjpuVw&iBcw zwHRxX{;Rjh>#Y+?tb9I`*bK}oJ1f8Wc6a)u+uq{6Dz-5rcQ1B2U-QNg+)x>%Nz%!W zrbPy#3+7n-_SG8+OZ)WRIdFcoJEfVYss%li$Cw-!tL1#fHdwNbXyrcc)W_1Z6=R!E zZ0t6rhC;q@6LS!-rV9@DVV`GE5;Ts6nJ{ZY1K1~BbeCkzSE2*zH)RK^$B>dCkT*@* zWLH<+<|Amu{As- zcOb@#w;M8&0L_e#<$9rTUf9%H`n~1xs-EX;CVC}6sdB4p9E^Sc-D;-ERG(PvuAaZ3 z0FJLt24qLMUUB(*&Rx~(mVCXU1?&8H!|wgZS1)R#~;ysp2G-#G3+z!!)Ws(BZI0Hg_|7=h7CVbD-uj zb@QI|_mE4y=5zGqT{7;MFC6c_Z#CQQxkt^mw3H^q@p1nc3bX9c`t8G+YmTatvfevC zoWL?or!!pj9+d5s!#4wjxl5pITupL6eH{q@)s)!LuK%B@uPn~n(o~3r8Awv>RB~wy ztQO2az}Wi@3@GefdvF$Ff)9V*|Cz5QsnDwJ`w)v9Dw8YdD^H%Jgw0OvsmJG2=60ee z-1Xtzb}RTEW#-*4p}rBO4>|OIP%EBop_B9G?lqsww2ZX;#|dAePVD zvneWVy+zrFjFf!Y@YoHByc*$~gsjKX8)9GfMg+3l0~`Fp2a7DwDAk@;2euhX9ELNn z&;6alFIH>1Cs=CO%w_lNlCJV*bRIGcSFY~k)P8}2fbR!6FE?n!N}(Y(&kmzGerGP< zKww>WXa_UFKlx?F8!Z00=>O(4$<4gI+^6zz6AK;KYZZ$GT2j_!~xI|s) z(m70nU_xFe;KbYAk7L?nw_Mq*zk}x6@(=-Hgt!r%H&60iJS6ChI_lLcGLmVjV49Tx zIs6>Cdejz(R@e6+CAV4%qY88RJJeDh;~>VNYf9O}4*;rqr-6Q^vf};s(V8){pT%>% zv#_alAL5VKZE*pjf((|(s2CQ-#AK!)`1ZjCtn|)-9zVldA1<1mzp~srUF=nSK$6Y2 zq4dpws_SFjwf#^i3R5z);UUoO5vF*&RAX)6JRNkTLgT!A;XX}(T`8G)#GCb3Q&`@9 zd9s!2(f&laIbReCRxH}AM%%R&Ddc{+BQmTH?1P(+S*H(^2n938=Zjg#M(RQG9QA18 zqvus1Mrm*!n5T1pTS{07L8~4%m-ccj%)u4Z@V8KmVSjl3K84Z zR^ASmYG(dQ2aAe-Y^L=&naa^ViOD9tn=^VS_>XRDp)JOT=JrqL-`|;)C5S9{psbpW z;aimxMU&%uHu}mQkF2?eZuJ<(^{n{}*1JI(Z7#Ktr4{736~81|T-;r87sY@d8gANb zisawMBUi+mgewuLdDEw>J~o(1HAM!{KwIYSbBL zrg)0ivWkip(K|(bg&LJQ-y6vRaK|$_qa|i8cE*f*0-(QJ>(BfMbG4a!y1!Jw+m^%s zEq5!CsQAiu+r)f}WNcH(hgHK-Dec`a=<;Hsb#}~l6UMMV{=WNsC`)o5#!1)k`5T42 z6CIu)sF2B6oAK1mml>Vy{~HxY-aNqUWsF3DNrlf=*Wry~sN^ z8RPt+8X5eeG&p9+s+&o!PzwC z9(f`_JARn#G^NllPoIdQ$Lyw%Kk{*7o{SpyDDkMADwxB7CSNZDi8Gz$EO{K@W1iu` zxaNEwlRjKl`seUe<5E3@L3zE05f4Btl@s2EsjiZ$lLSQstb+dV&u6uG+`0T+9i{Om z5vz131+bcaL|`=n6=Oe%$k*L~LAPyW^F{stncEc%?JNq4fWlNx4*(9ie!N*MA_jCP zc*?7a`_t*!-e2V{*EhgtlHe@^OcIWX(c1G_E4_?Qyyp;c%Im~UtV%BY%U``F1Nnmm zvmF9FO`gXD4H%I&;t>K&z;ksbr2*Ga%I^!KFBfwwhGY90{* z^O$*;zYoLo($VR zFyYb}`8izZA6&7hRxyKr$^papV*n&p``hGK$Y2blfI-#oob`E+{zXDO9fOn_cp3xR z?e8Z4x|W^;z?;f~`m*PZeT(M*A6Pry!a0~%QVPf7q*&H4A*fQSe?0>X2s@SE?6=U-UQxxM(KDNh=W*>y*TjmVZ8<_!>^o#86dl+|&VWl?2+a%^? zmD%fZ9B?_JL_fXllCl!dBrO^TDt1=vKL((!gXNyuk1t!67m?>)n=9BO6zN3*_N)7y z*c8jRMtY9$C1KDJm6PM5LqP7$g8o_&eb*FW)RoP~j6w>xB47$Mi1NRCQv_xUkcgdh zDe>1i3XXpda=eV^IL<1`-oru242vB9{*T?#*MMwqTlSxF`-kYh)wF{@iNckI(+DV$ z?MCd9c-E`+`bkA@lq&pEr|;z0YKfFgE|4WQZNON`=2Y}oA8Xn$HxBZF%?el+55M+p zC)Tb@0k52@p+yT}+1D7>_7@U=YO7#bbU(yzN${3$7MbZ+NloxR6EEAla*_~?2si*; zq~)_LR<`MDvS<}rBGb%eg+$WFz%};PBbuGwI2q3bu~Cs@%6%dgmFZ?9hXG!T6!F*h zNJ`8?5;Azex;WXf?j5w|ipHgdXVxG<3l!{Cw1wrNg1NyOel(`ue*KftSNJEIF;6R3 z<0k);tN>t-)CeRMBLhN&Z%)7o^+W9JR9)jkL59)joJ5ZQ_dXP|@MO4RxUXqDvRiqS zPUV|Xdy+npd05dkCL%&Gh9!*a?9!!Fd#&{ZDdhQ`?aI_9C=(jT>O&*_<* zt~)(Bb)uMNcMKqou>z0F@)r$yCM%HV(FSHy`ita*BPE-3>I6BiNdaU@O`m|ZE(wkh zW7ecXttMEU&QY6guI;kVbyQTox3e$S-+9)Ffy1IQ`EwN&Y{H5j_nN+$JGG`Ftf+$p| z%*o7`CiU7fyF>^g4Hh<rQ-zI#8J~5|?;`p`8<~B2HR*jD7r%Ygu>6g_P{8!}6(ZylrDV?;{Er zy96D?*<%5-xb|As#zK1|tw8f&1b1d_Fe@HI!+Bz0LxoN4^?5@0B&~p}c}*1#hsuw+ zb)LRcg6Zs=D7mS44;oAcjlBClg}nC~K9!ss78T==Fck1=wbChaG#^`6_*`%1K*w07 z3e?-WB+|Vjone8dkmnP%YUanPh;HL}x_*x}v57@DQF}bb?GiDxknhPjGh*HFgdp_+ctdcw0YamDP$vf%8~K?l+h}8r?PcVmujm|D=(;VE!HUd!>Zz zwRGcp{nmj}^yOHbMyC-62Mi9#|7elhNfoG#HzysWhpc%|$hn;}xr8aVYzCn% z#Gf)zEP{e9r7lhgLyutDGu9IR&c$o&IjtWD~jwIab?l zS7CT`OqmZp=7z;|rY(i+SQ-J-i5ey&+C%szN$A*y%|hU5&70sU$Ds!#b92i_`uVck zeQ`6@hqwX+U65lALuonrcKFVzb>-&ip(zXsi^~17AWFQ4Fc1ZYim&m@D6Zfp-dIZE z%UO!WA!5qI-kJ>x=P2@!^2nWTNMv2|yIW1Iz+N)Wj2# za>&kz0<@cbu9eb1DYb&ggA*2g@;nrEuou)Q{72)Ufo`ij?OpBrw*fFaq6grX96!tx zi&bTNp93ilYU6wY1-3%!z#pegVrfKug<6fO7#}XQdtOyy%qAAi)WQZ-NmS16+-%_! zGQryAM~3OU@qOE=L)QcQHOzpT=~oe-z7|Bn#*8-yx>ki0_7ot!`vIs*=e;u=8{}sSn8^qSN46nVmoW?4`igj^c z4kZCKQ}Tqw)2_Hb9|BZdUX$tumHw{w`%`sXmV7FKKH$_#bn6!MJbj`g7jaQqL@phOl-lN~EU@z}9| z>@hwvHwknq1{h*ultGh|>bEi-DB4o#!gGlvq(alKX+Q5aLMEakyMTZtZzG-fBc zgCGUzc-(c}THFC8S7FE>3#NQP|6egKYd!L74zXIG?sOX z4Cn;;y<3APX40}WBOU%Nl%=qD?l^@X)Xumd6!tF z@)igh?MM^4wg7C^I%B}AR$=_?xdP9MYy3!TH{&Liy_3GSZ;StIXB!~!q@y@upxAM} z3ChVC%_%IYdTaFbJB$g!NU%T{Jz8*R-NqxkQx7!i4d6!4<&lT{&!dA zTc?_{SCu7;mUH!fmJR1P@I_HrekLhJqNboMvanCH*!<; zKSsZ0OC}`d=^#V(%MOV|l$P4cySsB8Zf(F$`hkeO8fJotOV+T5bYnYuL7YIHwM6M? z`vECgwvyDn!?bTemGF0~3xaGK18tB@^FQ9*_kZmv0opQ+{DCA#Xvyf+&!7VBEE+|> zE{-9pPcsSN={;+++;1b9hUH;zyq!Y|S%9`d!|lk(HnW8b#!PE2G2 z>PEGLMK={V&Ae~>Jwj-tC}B3A2byFYAsAq^4FfX%wcM($qh#b54OZnq0vOm`hYElS zOqD#3;i{BhI$b`}Cr>$>fk%iaseUW;I|e(IQp!G+dKwZYv?h5%RNwZpJziFbT1!+l zA>4gg2FB|s%CFTf^9k#L3T~7>vl?S`SpKNgxR8hpN8nYTnBOOr3(cNx;uo6`Iv+qG z2`rzAoC=ye5urQC6xPnm^gGXK^+`mR6yVlID*^4tDt%uhw7tr2lg?-Wwht3*hVqg9 z6`UOnXuDaOc6)e%8$mF3?{p0(8q4K|VLNu=DQCnG7WRzRcZyFceC5RN4VKOB+DWzW zLPu86gSEfpNx_TEm0fGFGtNj3Zv`MH0bLMc-rl?@B!Yy+ieGFRSG@5sFcEbcr2a9~ zmE0P=8zEmr<mjk{DtcN%G2xi5Jc1celaUHgYMrouuZG3e>GbwH~44uKr> zeH1lfrikxmiIS)*8)!g3545){UVf?~1aU9%OJJ*%OD+U$!C0wr~^2;i zDh!J>oGkTN@=nhT!k%mDm`_4tI_onWZ4u&O%SdH_B=D~@Nq%l_{3tv|oAH>cwP-{T zBLT(cym{!zSI^q{q$Zk0;>>irYB-@RJBRLTWWtz6!PhPobz3YduEe6gDp6AN4LoW6 zhF>NwvQ-4*csXnPyb|daQj=VW$t@zGp^=QJh+dVbAxSK?aBov7VMIY^VI1ROF_DHl zQe+i}{5{mWciX(?TQtLREWU6R38sOfcd{E(H)@N?UV7099S;h_T~=Bg%+7iYt8b~E z6RA{xp0H6X<%9s##mo9@AL(?WVF{ya_IBUFf#U?K5Jo>-!5)hK@HI{%CFLSNMaG`4 zqmg%rM-Iz}-dKMXwrq+6o5G;u(2R7W)VvAqjqgOCeHje|3Nh+YBCkq6mgA|HYm9#k z>s^QgD9w9-mU~yn^kt-Eh55AJu6AA0Vd|gc4N$qlZ*O^Wx1f81isukm!7Gh#L zaBh&c+;_Zr+FQ4C5f|i1?`_BWC`wIDBi=mnQ}|u57do;C*&~(czzIG;-En?{kr0LH zZI-mAAy9Qf+bgjUo>~eIt&V6D!F~Os<-zLqmjrP?sh6e!`E4Lfxj+nX7h| zYZ2fY0I_0M5*4!q24ysaI4+6BWmt#w#&@F+1uysX1i~(k`>Cq-M&mji2|oTf3zOF5 z1rntW^zvMK`Y`XRnM3m>qC`p^KZM+ch}Dt{hJJ4_*zTX|(j!TpOm@Lqx0}Jdp)<_h z#l_ab8%5CMPV2SS%DQX)F~WzmnzcfK z)>~&XPHIe5mt?Y61L6ke<#)Vkr-!oVGWKMEge7&q5UF;t=CVc-&ciBa|F+{>tw=yT zxx)~T`Mmmv`LN~!`yCEl*DtwxXBt@(1jc~udTyk~`n%uINA(NMyU5qQdS9pf>`9Me zgKoF@%wp)Up~;OX5-!5sjInr#Wku$-&~j&`t3a1!7q=w*}r zo_8}-)I+6Cr(E%a;ztb#I9sX&2>#F4_$cqVe$Xh3iivFi1?~aG3R&Jq3rjPs&A3#D z)qd#R`s{_LXEZPpq&?JcM1A|ndP>&E!7FrV0SCrmUxdyVyj^e!BR(2rKN<-1!+axQ z_Jd1To2<*_OZyB~{=FV3x*LssN??Co#TElbfAYEBf&0!o1AWGf2kIxG(b5gdH|uXa z)lU5J+f{l*At>gzdNS*wauvEO-D?u#mb7_ffEbI)5NAd~^iTu+7i4I=J3)Vs@wEKcmraOM>q2^sD0vYhN*N2FA8)H!Ao2x*6KM89? z^1dW+kqBF5p=&V|9y2{raTk49huHzRNR%6GK7Md6G*XjNvB=Syunz@07|;Osc)h(p zWb%|)KzPVum~PzxXOGdV=mQSnLJluKqMBm%5pjcUg*6O#vt9M@d%8O%q=cbc0g-NS=x#>3OS-!RL2Bp_ z5Trr6hi*h9q#H!KdtZ3(=Y8M%*q`_Qv_E;^%pBKwUT3bg{_D5ezH8>Nbffn5;Szpmv6S0Vx?~RU{tLEC>$%s$LK=HbCAvVPblixn!Wc81z zOuBuz1ri6Yj`66d4x+f_{aj2DMk6Y%9!rNyt=)yg? zuSNE9V5eX1DYRcPyS9vz#+S!)GrvA$*E$%>*z#~A6{oro#30<9QJM5&mWUt1gEvQD z)>nbvRZgTrvK~-9)rWA2;Pt4TjXg{_g2^v~I%QJ6XnfXLll@JdNNa8jaV9?L@xl9* z0$jz{EjVx_vU@TfE@>b3aN^BrQ551tx{7}Fj@iM#=4b0jV{d+xnJ#EVPMM*;O)0_c zjc5=k=l+it;EzSVfV+%q?c!18Bu4(yRctR@U+`TO5h{~P3Y#v%k0Qm8$jD%R>1eLg z9-aeMaD{I_Qc#_#(P^+X!*Wgz%FHCz4+)v6`vo&{Ej1;|&|8_>!xW<{82#<{FGFus zXbS{={nwrkjIcuc$3(X`-y)|(%pAl#TdDw@jky{o$cYy1P5mba-=7bkhm8>%ahHVf zivX&Sa6rZ<4pgy#Cv5=)4+gPfYj>mZ->RIZ5$4H``_$Ri1AkOZJ__NW=TvHrgcD(7 zht}Bk!!5L+4i0++8^`hl<_vyZpa{R0kq{I{D%r!8Sm+PuFC6#?y+(n!_=rIu!R{}X zcM-(P_aqKRk>vC}0jhKDg-EXFOVLC*t+}i;E}>CTL<^}t1jifG-DkWMN9P36s%#587 z6wa)g5_dg{S!7?{(fo|h;1>Qa`ldGE)eCZorrfV8Z(6xEu$XlO>d#=?VCahFRX}VN ziVE+Ycqbe7gD>B;@m7s;GCsJfto0?X665pmg*!VYt#8j(vYC@5Y$Oub>}h_97O9+~ z{Hbom!};>jt%(zS6+8a@vn(x8;oDq(y3!bzUBCX@wA)QK+Kv(pWC@glpFj<1qY5j= zAFs-IT%b6{n`mS)%W1;l_x7up?OUV142@S(T+!Q>FKca*EHEfRM$|pmzsDR$=B)km z{HRoygy-=!u84X5>{Qj&__Wwb4a{*7J>WCDRJ%2lPHemIAWs@c7%Fyhm`Ecn4o%AX zgfJN3i@~fvqP*;k%$8>_8d$}+9I^Kqv*QCJQP`k*5;r~>qpNeE+h-#^oFF$riN2`w z_^Z>5_#D+LPXn9z4jWdqL!mIm+I3V5U9J!p&KHAF=Pn*k`f}dZc(h*SgifbUEcAyF zTs}XiMXLe9%Dc|5Rwe>cLZY|`d+!0e5fTRZU3Hf$fENWPNv0V(*cF1>EBsaCj`3D3#}gZ*CaO+6FH@y%=_+cQ~kmm6rt zRDEE8-@7amLTZ2g2K{7}3adnz>}%N1u&c6puKg>n$kA5KfrCWz=5EYnOttSiE3u0o z_3ij8I7^ZyBHP-6FbRy{mp@gR1erRL)?mb9;K9E^*O>LeGzr@semmqSeA2x$5n*q2L1Uw<%SAwhw%r5xmyo~%1x?um-Y`kK1018+;zI|g*nPa?K~10con?NN_Fo; z(R_ihg{IL|3m;9z)QVE}XUa@&DHzssL(!5c&;M=z7H|5(uTg}hOodk>M+))|ilY=^ zjxgAv9G)bKVL4Vsf)QAp8i4f3!k)$?KmQ6<&100CRYk=FSQ!IIA&zMX33NOtuleJ+ z;gh`Hh0HNSX0zi{5NR7X#ir}aFk5K+k7F$6B?0gHl1d*0!ecgoH{0S-*(jT=;Qs6f zmP3+&#m`?O79G{+pviTcUja@64vd6MY6(1z7xY$miv&f_DNrmw;Yg-pR7yBMs#Wb| zuIA6uph(CCz+4kds`piOtdwg zgPWF?1~PXJ(r9woEW5}6hQt!@s#xnyapR*q?TjW)Ko?TaB5rE+p^8+}!!M1hQw9EI zC^i}DW5^_c;|_=gLMZD}z0Af8ptInKF~!+SsAiX)Oo83kWHiJB++m)aSHrMEt*|dn zxm;;Z7Q9)hbSfoS_*Frjp*ReALx$b&%yv~B^msNv20^W!_7_amc~K-WBr#vnL=uQs ze#x0b2p#}$3I4Qv#waGCk!Yj5BibPkax}^_oiEP~Fy^*Bcp&i<0lj6wy^M+Gu9Rh* zZ&0u!CJN)Nt-5bM3m0TVyN(!1)C{_ooRfwnZ!0VLE2xm9B@!08CPt5;fd_i$FvWSk zw<%q9@Y_QR8@EE3j9i8S;n-<`qnLPULaNw6{*5IOhHxoXP`!D!B@FS4g|Panrf1E_ zDc6k&E|5NUGJz2}+>EUL5!?Ji=4VD%=wJ$kXs!38>E*s6FebG3Bg3oXljEbW+Fh@; zSLUKCtIz3~?h14XH9lQmUu)eeB1Z=DqV5JPw z)qy5|tP_F?v90T@KLKyw^{Ta~gc8M+!NF_lS3mjU6@@Ft4 z{Q4q*7mLt90$T3;#k#DwZ#@jfInu-K{t;c$AKsZf%?m&U=WSuexMR27rEXbEcN^HS7hPr>L22X z3<;*o{gY3-qAxf!TQJWgFin(w!C>$DbMetsVoTv;3CJ)iZI(i%(ZJ@hS{KsInsjS{ zc6+SXkrw6@DtyY-17`9(RT>-hY<&0ymd;RR0;7;G&4!?6rFzSfUY}|pL}jQzpKaX@ ze?0oK9C7qzDddgo8}Y4xUmeNABz^Ed^~47orX?p2A43X8hklI0e#M1Hx&w#Astq&F z$WK{#e2oUeD@}WJ0V<(HI?fy~|5Q0bnoh6`GVhCL0l;Dqzd1hV!=#Evq`fPP?^bW> zF)YzPNgc+7#0GMSSGrQqVFiiMRvrDJzZf#U4$9BdPHspu=~@o}Lje*d>zQd9=5A10 z(&Hj2=iwtVsgP`DGs?nBW!{Yv52S=&n2-h#N+cm_q<9jchvgkM8PCdQ%PaO`yq{V zfeZhO=VqUAy>8^dyfhHmMbe=K?4VLkztI{3I@G0&Q!FH44VP@MD5OIWdoj9R<+@9~ zFqJ_R?FOS-2&?FVDeRtSDl2KvJPmo^>vlI==YDh9R zWMq#_ORq>?;+n5x)vgc=!zikxAz2kOj|`kfdS~evD8LIt@|zLjBm6X-o69sy`BN>! zcUVR;S#e$QchXG(Zm8%fmw^>Ol}kmx?!MZ!BepH9qWvcMXH5!*)MqZTsf4k&5lm|4 zbSh9FmI}Ie5HTesbw^%{pYhzuVxrB;$)ST0tq&v z?o>D%g}}Vsf9717oRiyvqvyC-n;>j>HsA(>ObXvz9*Rbm zCn42|j&uMFzU^$I!fx@=;{1wnEoG*US%K4D6%LiQ5jPs<_2D$uoZeEV@W?_^6%Dwo zE~K3iScGX}us-EcjKoOyLwJlt{aRHh5xBqPOq$Ltdl|dIf=o(QtS&{xX2LtsrS+h2 z6**%%ncU7(U|?@`+zPW-dJPm}rCJkqj_|b}u7ur4Nl$x8vXwU0&T-zKX^EUMWlby; zJx*3G(Fkwy8)?Bq8 zUhR@Ozewa zOGTU#I(tEZKN^n)VmmNIR5uSFeUU#sW+IjZJX*OWG^f`y;Yi80IlaCl29a(fR`6DD ziaGNXb@>|hqDOy{f5hRMlV8%yaQ>~^Foak}{P{2{a2Q_<;^LvS>SoZI=WqTw?VmPVfj zlNIuVNN&Mu0LnB6^JW4U)O)_RHuO4tkxhl1@SMq%j(`~Gy21Tg+qRgMdbh*T6-vdd zd5BKt$}G|0t9d3ZwTB1%cd6uj^RGO5THA?&f0Ip()Dr{DQmy0 z8x@vy=BemHFzf-|L)7C%gM5-nM6;GJVsj!_wU=E<@G1I2b`#FQ7#}CND$HEekBlzI z7R7S@LzrFAE4t*=)^0)f>cednHa0W>RgdFULiWcaK~m4SptXsI0mUs4N}KhZTigv0 z{;AO5eT11M)^J5hK_>lQE@WD>fWMB&1p-l5UW`!rUD`eH&~Y?*ai|AKur;-%JEm9#U>%Z=!;1uf{EuQOFyyV)|%PM1g?SN_tr%A5S^`;D?Gp=T|K zTUj?~lP>CZ7vJmD0zBJYRxk7B?IKz5;In|vSK}Ov)ZYb1#7EeQZ5}QMg&JSL>Mlw2 z?IFnwhgDfC>m-gLlhi`vJ@S9n?44XsFiPF#6#M-kuHqoG=#D!iDWOZW+SK|-wuQSh z%YnB_{I<0lxp8>BNq{T-J&P0&w8HCHRhQ#p1PivJs_x5n$C4Gn3Vwq_xSEbNgSeP^ z-)G4;4cxko<1*uc%$NAk2#s(9Wb}`j!2YZQ&;fghFcC;ip9x%*`K_XLK7k1=sMaS% zPs!FB)O6!(z!CQq2{P&}er4)dZEYZs#cJJeAh*Hef*0BTMe85;lYM;gfIv=8zT-|>DzUM^gHjTzLa_*%P0 zyH1Hg*xii)B3LrARpU7EUpx)LyijI2y%5yO{=1XkIWmE<_6(BjAZ?w$*~M&PQvaDH zRAEu8qmYxQT)`@rO$8wP5;Zi8cg?~K1#!|b?QML0PSy58Cu_pjc_oV1@Gz%kMy0!?MhV@k@-;~6 zlOksoXk#s#i@|$nglH*DUULntCE z=UA;ztCQf&ACe5X$A<0B^B0T+IGs^Fh)yd9e2s~DOW_fVtR3i_8r7|G!`Xc5&*^es z1!WkQ2kG;cV-niQO3jC>cpZpE>er!Ge0b2REz(Tmk2JU3kX`sK>|~zQ<+#)R3LbeR zpzVufG#NzA1`Q+8@}(^!83lRiLtM-_F#8SaBu=`TdTsJbsqQ^NZEf9n3x~X;xtysJcd5$=ZKb9-5nwAmy zoHPJ1O8q?V#`cXcmXOdPHCnnA1tbUGC|J)?gz-kP!2YYqgI|OP)66u+p`kv=U9e;>}%4NTRcY(RSS zI;hc&30%Y+9Q8XCW=3T2xJIT9;o%>n0gLdPanE7z9=-bemK>!i>{EUExk`h8WmY;K z8`6laPV3)BYnpF{xyKEmtL`0^-y3>^{}V!A)=rEKIQ=Z%=zXSHmCX`#TLB7_&9KwZ z(b3UxbF1Nm^uv@;jx2w^QUA8%8ZlI8Hk;j3c|j1@>GEO{F{SEwB(@C;-+syL zouoJYp+1gBz(b&)#@h8D-gMcQ!Amkr4@3;&oHb(IKZ}{msr%&P_Jve)X#Csf)g?Zc z>1`;MpY`W8`sc#lcpP9|9xVQ7BLhMzDLFaFqQ;rFq+BDPM(mrUx%F#ShX}5AVRkJf z5a?9uqr!e5hs%`%E5m2r>(&5>PL!|&|+Kd?tNpw861wq~?z^vH! zGppF_3r>lXCF;2!&O#~zNYhsE&X>rj{v;2Pt3PX6sUAMmA6X<>ngJV_aLx_}sD8q~ zY1MXCv1%&f{U5-k>|zXE*`Ax5>#J0SzCTf~Zl3McM!O#Wfs6_KJvcE3VA8 zzpKN*FtJi~V1<^HUBSGNST)HcoNe!GUi(gvvr232iQWrU3hz|Hr?{?2X)aTN`j{J%49AQ!Iqq8bhc__b`qR$qL0Z)aM~nH=0m5cDMaA>?KvF|4RZxrV7bXnC z);zW)Yx+?B0Q`fpXNYNhB86I&VM8ehLo*?tghAqy+0?Y|_b*M0Wc0`#<=(z0vws(e znRIvOt9QpiYs!IR_q;#w#Etg!x3g(oZaRA|%nzQdWvnWt1cuhVT*8_|R)j!uC=`vJ z$jTHrPGmN=S#M{L$yrH+dhu(rG&@pkBA*=)EvZ4-ZXLr%VFhL;y2YgDT*J|nG0T<6 z%9?ihT)Ud~#Vqv$X!sL}ops{@L3_sK)$`^@h{U-!u!gP-35HPdx~d5L~iFYzT+D z(N}nfz6!vnOTP?NcO>GRk{Y>6^6YzQy7u+n$-QNyH|X;|2B2X$Aa4YgTRE+%nsb3T z)F9j482X+JnLz~#4nUctCnJ>gJH0u0h6<<%*+tae{4-MbhpamO{zW91` z8HFOtiP0<6zw2=LpVBtoo+?EyZ$N2QQHHStNQOSrK=OxHKH<~R&^n=p0cA_Vph8tg z91DhTun(3x(%x%YF&q`u56cN$dq=@w1*a`4pF*Q1CemUM^M*(;R^OMgP}^JXNfLN) zMqHI+=8lwx-}X~1;{+A+-P7hvE2m_&938KNesofv+Y^Bt{&)YVXub$FVeov*YSc@*^SKj>lts; z@Im8GH{^-Y-XJ+cTXO{=-so6-jt|;`PSrwvzZPE&-R|ntI_}@DF@YD@aO9vm6q<9F zqM5e{DOouXQ}#=yrPALjJL-OoJ}*=B4pRfkve7quWlw!}QNWtHQ%) zFd3Grc*WVs|Q0S7LCwO)ZHA)VZEsN9;yiu|G;;zpV)|USOE@ z8)*WL0WXInR{llvUqI~fn-xtT36+9rQJ5$Z=zs*39&Gm^R2}`X^T;pz^|Fsv47LjU z$a`KIienXLx)maX)S~iG)*P>5F0x}v4f2v@3o9iuF&99sC>FRFU%{No?JS|L;i0=m zcG*I!`8+sRJQ`74MUeOfm{M=}oLrWj4gmp)L+PH3L-7&}QQI{n0*zy; zN+6eI_>c^v+Lo41I=WBj@|VX}GP%KofnJRA6@jJo51d$i*>+MYU< zSc_3MKN0h({Jy`qEpq?Dgfj*D4HYH;#T|eC^*7-U@Vxv@)&u@177GlG!oV9u0&@5M z^;sM~4P@<^=vT%6`s&}DyvRTdBspj?Tmtx?Kc8~`{`uX?Gx+}}kHEMa%Y2Q<*?NRP z`PwDa?~O?(+|^nPCk@v`69qnpVKdRq*{*tVFOYH_Ov0`wK!Sd0YiVZId9>P{r&+2s z(ck6i$n@~a!y z_2&eq`#yV5`w6*5=Tp89WUrCps6=~3aIw(v97s8t_7jEgYd7$22j`AW%y73-o%!F}ugnPAq4@Ki4|{=T$h}Ub#>f(QtoTJ}(U+d_kjr5E zs3-50;xHL5h26b^t8n{|79f3)V&U>4=@#Q+KE>r~f06TF-Z1u5o)u<#CqJ0n7BB10WH8>_%QJqYbES# ztNcwLg^_<=Plo@io=BpUa#H)UVd=y(I{S}Y3H6AMVh;;##>t`@7iaSHfk}PhB3g9r z#aO@;3Ezu6%R>|SHhXo8Q>pj7m->bEGf{q(3A8A{tMH0BGMHc9Siw6K+;ejrHp#yp$g&z?spe+a^ zSM;v4hR}eB9t7f{H~~ij)8i@*NJFAq2;>TU&LqGqSXr>xK6>`G_c8UaLxt+zKDYQMC~64TRB`jxY| zAF2~D2_Mb~_m0b8-`Bs)5vmFuS?lhR;MQJ<&Rko2+ONo6yiVy|4$y6wyXuE~6lfLo zXSvP3lwZ<1FugA-2bpuf>2~xVImexEUTL9DyBGD?58){^HTnL>I_x2mMYt{b@w?`4 zeha_&xn17JE@PjEa0udU*Xn1+4|5E@3NfY^06YP5-mD1Xx1AhG6SB3i&)mU!flvV! zOXoJySxq{S+kzN3;{$LhcHZJGg z4E4h|BX2{o7VQcnehYAc^7!%1P-VuXV7WdYufY!^FV%BY*$N^5eEV7u*fRyAKkU(d zh3uT!cr0wT5y@&Hm+IE1v+~e0*1yc07Y!ZVVx8&c5^i16Td^($NKPhn^*>& z4Hr;{&E0jJmsN+VV{g8JbGS7Zy2^iDs$I!vAyQ$`1S^V#gckv*KG4O+iZl;s7DcE^ zc;~5>dl@xQ4jhz5Z2kIMjD7cGHD$f!#z4YTwRuABu;q1e_113!3;g4vKH)|tV{+GXlHyz1DJex*R8n%%A?z@<)e zD>GBSHCbz|4FQ6L1#VQq<`GJosQfmr!HDp{@x^gHJ$M6mt-$=aneCo}{vO8^v43k7 zggxh`a>GA5xr|scvGOB=eN-y`X#lyjuDTgUFd)K0f_1j)JL36m>PdBRFMx}^G=
0<=kv#0;|~8mwiuEiaX!P3flXiJTp)B= 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 palworld-dns.json +{ + "Comment": "Fargate Public IP change for Palworld Server", + "Changes": [ + { + "Action": "UPSERT", + "ResourceRecordSet": { + "Name": "$SERVERNAME", + "Type": "A", + "TTL": 30, + "ResourceRecords": [ + { + "Value": "$PUBLICIP" + } + ] + } + } + ] +} +EOF +aws route53 change-resource-record-sets --hosted-zone-id $DNSZONE --change-batch file://palworld-dns.json + +## Check for RCON readiness +echo "Checking for RCON readiness..." +if ! /usr/bin/rcon-cli -a $RCON_ADDRESS:$RCON_PORT -p $RCON_PASSWORD "info"; then + echo "Failed to connect to RCON. Exiting." + exit 1 +fi + +echo "RCON is ready. Proceeding with server monitoring." + +## Send startup notification message +send_notification startup + +echo "Checking every 1 minute for active players, up to $STARTUPMIN minutes..." +COUNTER=0 +while [ $COUNTER -lt $STARTUPMIN ] +do + # Run the RCON command to show players and count the number of lines + # Assumes each player is listed on a new line in the RCON output + echo "Checking for players, minute $COUNTER out of $STARTUPMIN..." + PLAYER_COUNT=$(/usr/bin/rcon-cli -a $PUBLICIP:$RCON_PORT -p $RCON_PASSWORD "ShowPlayers" | wc -l) + # Subtract 1 from the count to account for the header line in the output + PLAYER_COUNT=$((PLAYER_COUNT - 1)) + + if [ $PLAYER_COUNT -gt 0 ]; then + echo "Detected $PLAYER_COUNT player(s). Resetting startup counter." + COUNTER=0 + else + COUNTER=$((COUNTER + 1)) + if [ $COUNTER -ge $STARTUPMIN ]; then + echo "$STARTUPMIN minutes elapsed without players. Shutting down." + zero_service + fi + fi + + sleep 60 +done + +echo "Player activity detected, switching to shutdown watcher." +COUNTER=0 +while [ $COUNTER -le $SHUTDOWNMIN ] +do + # Run the RCON command to show players and count the number of lines + # Assumes each player is listed on a new line in the RCON output + echo "Checking for players, minute $COUNTER out of $STARTUPMIN..." + PLAYER_COUNT=$(/usr/bin/rcon-cli -a $PUBLICIP:$RCON_PORT -p $RCON_PASSWORD "ShowPlayers" | wc -l) + # Subtract 1 from the count to account for the header line in the output + PLAYER_COUNT=$((PLAYER_COUNT - 1)) + + if [ $PLAYER_COUNT -lt 1 ]; then + echo "No active players detected, $COUNTER out of $SHUTDOWNMIN minutes..." + COUNTER=$((COUNTER + 1)) + else + echo "Detected active players. Resetting shutdown counter." + COUNTER=0 + fi + + sleep 60 +done + +echo "$SHUTDOWNMIN minutes elapsed without players. Shutting down." +zero_service \ No newline at end of file